把MyBatis Plus加进项目,听起来就是个简单到不能再简单的操作:pom里塞个坐标,配置文件写几行,重启完事。但我在帮别人看代码、排查问题的时候发现,光是"怎么把mybatis plus加入到项目"这一步,就能拦住不少人——版本号选哪个、为什么启动报错、为什么Mapper一直扫不到、为什么分页不生效,各有各的坑。这篇文章就把我从零集成MyBatis Plus的全过程拆开讲一遍,适合两种人:一是刚在Spring Boot项目里第一次用MyBatis Plus的新手;二是已经在用,但被各种配置异常、版本冲突折腾过的老手。我会把每一步背后的原因也讲清楚,不只是丢给你几个步骤。
1. 为什么要把MyBatis Plus加进项目
1.1 MyBatis Plus到底解决了什么问题
先说个背景。原生MyBatis是一个特别灵活的持久层框架,SQL让你自己写,映射规则也让你自己配。灵活性高,但带来的问题是:一个简单的单表增删改查,你得写Mapper接口、写XML、写实体类映射,五六个文件折腾下来,只为查一张表。项目一多,这种重复劳动非常磨人。
MyBatis Plus(下文直接叫MP)就是在这个痛点之上做了一层封装。它在不改变MyBatis核心能力的条件下,把单表的CRUD、分页、条件构造、逻辑删除、自动填充这些高频操作全部内置了。你只要继承它提供的BaseMapper接口,不用写一行SQL,就能拿到insert、deleteById、selectById、selectList这些现成方法。我说的直白一点:MP是给MyBatis"装上了自动驾驶",但方向盘还在你手上——复杂SQL场景随时写XML,系统不会限制你。
这也是它在国内项目里普及度极高的原因。我见过很多团队从JPA迁回MyBatis,又因为受不了重复SQL而引入MP。它本质上不是替代MyBatis,而是把MyBatis的开发效率往上拉一个台阶。
1.2 和JPA、JDBC Template比,优势在哪
很多人在选型时会纠结:为什么不直接用Spring Data JPA?为什么不干脆用Spring JDBC Template?
我的看法是:JPA在动态查询和复杂关联上有不少学习成本,它的Hibernate底层自动生成的SQL不一定符合团队预期,遇到性能问题排查起来也比较绕。JDBC Template倒是原生SQL,但很多样板代码还得自己写,分页、主键回填这些事情也只是半自动。
MP恰好站在中间位置。它保留了MyBatis的SQL可控性,又用"继承BaseMapper"的方式解决了大量单表操作冗余。你写SQL还是那套MyBatis风格,团队上手几乎零成本。对于大多数业务系统,比如后台管理、中台服务、企业应用,MP在开发效率和可维护性之间取得了很好的平衡。
| 维度 | 原生MyBatis | Spring Data JPA | MyBatis Plus |
|---|---|---|---|
| 单表CRUD | 手写SQL和XML | 接口方法自动实现 | 继承BaseMapper即可 |
| 复杂SQL | 完全可控 | 需要@Query或Specification | 完全可控,同MyBatis |
| 学习曲线 | 中等 | 偏高 | 很低 |
| 动态查询 | 需要自己拼SQL | Criteria API较繁琐 | QueryWrapper/LambdaWrapper |
| 分页 | 手写PageHelper或Interceptor | Pageable内置 | 内置分页插件 |
当然,没有框架是万能的。如果你的项目需要极度复杂的动态查询、频繁跨多表操作,MP的优势会被削弱,那种场景下直接用XML写SQL更合适。但绝大多数项目里,单表CRUD占据80%以上,这80%交给MP能明显提速。
2. 环境准备:版本选型和依赖引入
2.1 Spring Boot 2.x和3.x的版本坑
把MP加入项目,第一步就是引入依赖。但这里有一个几乎每个新手都会踩的坑:版本选错。
如果你是Spring Boot 2.x项目,JDK一般对应8或11,那么引入的是mybatis-plus-boot-starter。如果你用的是Spring Boot 3.x,JDK至少是17,此时必须引入mybatis-plus-spring-boot3-starter。这两个坐标长得非常像,但内部依赖的Spring Boot版本兼容性完全不同。
我为什么特别强调这个?因为我见过太多人直接把Spring Boot 2.x时代的依赖复制到Spring Boot 3.x项目里,启动时报一堆ClassNotFoundException或者MyBatis版本冲突。还有人是反过来的,在Spring Boot 2.x里用了boot3的starter,结果依赖解析直接失败。
版本号方面,我的建议是尽量使用当前比较新的稳定版本。MP的版本迭代比较快,3.5.x系列是目前的主线,持续修复了分页插件、多数据源、SQL注入等一堆问题。遇到具体的版本选择,去Maven中央仓库看一眼最新release,比网上随便抄一个旧版本靠谱得多。
| 项目场景 | 推荐依赖 | 说明 |
|---|---|---|
| Spring Boot 2.x + JDK 8/11 | mybatis-plus-boot-starter | 使用3.5.x版本即可 |
| Spring Boot 3.x + JDK 17+ | mybatis-plus-spring-boot3-starter | 注意坐标带boot3 |
| Gradle项目 | implementation同上 | 坐标不变,换用Gradle语法 |
| 原生Spring MVC(非Boot) | mybatis-plus | 不带starter,需手动配MyBatis |
2.2 在pom.xml中引入依赖
以一个标准的Spring Boot项目为例。我用的是Maven,所以直接在pom.xml的<dependencies>里加入:
<dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.7</version> </dependency>如果你是Spring Boot 3.x项目,把artifactId换成mybatis-plus-spring-boot3-starter。
这里有一个非常重要的细节:引入MP的starter之后,不要再额外引入mybatis-spring-boot-starter。因为MP的starter已经内置了MyBatis和mybatis-spring的相关模块,你再加一份原生MyBatis的启动依赖,很容易出现重复Bean定义或者版本冲突。启动时控制台报一堆NoSuchBeanDefinitionException,多半就是这么来的。
另外,如果你的项目里已经有mybatis相关的依赖(比如之前用过原生MyBatis),我建议先把它们统一移除,再引入MP。保留两份MyBatis依赖是我见过的最混乱的集成状态。
2.3 不依赖Spring Boot的引入方式
有些老项目用的是原生Spring MVC,不是Spring Boot。这种情况下没有人帮你做自动配置,你需要引入:
<dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus</artifactId> <version>3.5.7</version> </dependency>然后自己在Spring配置类里手动构建SqlSessionFactory和MapperScannerConfigurer。具体来说,要把mybatis-plus的配置类(比如MybatisSqlSessionFactoryBean)替换原生MyBatis的SqlSessionFactoryBean,同时注册MapperScannerConfigurer来扫描Mapper接口。
这一套手动配置比Spring Boot麻烦不少,但原理是一样的:先把MyBatis环境准备好,再让MP接管SqlSessionFactory的创建。如果你的项目还在用Spring MVC老架构,加MP前建议先确认团队里有没有人熟悉这套手动配置,否则排查问题时容易卡住。
3. 整合配置:让MyBatis Plus在项目里跑起来
3.1 数据源和基础配置
依赖加好之后,接下来是配置。我没有用任何花哨的配置中心,就用最简单的application.yml做演示。
spring: datasource: url: jdbc:mysql://localhost:3306/your_db?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai username: root password: your_password driver-class-name: com.mysql.cj.jdbc.Driver mybatis-plus: configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: id-type: auto mapper-locations: classpath*:/mapper/**/*.xml这里逐行解释一下,因为每一行背后都可能藏一个问题。
map-underscore-to-camel-case: true是开启数据库下划线字段到Java驼峰属性的自动映射。比如数据库里有个字段user_name,Java实体属性叫userName,开启这个配置后MP会自动帮它们对应上。MP默认就开了这个开关,但我还是建议显式写出来,因为如果哪天你和其他持久层框架混用,显式配置能减少歧义。
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl是在控制台打印SQL日志。开发阶段务必开,方便你确认MyBatis Plus生成的SQL长什么样。生产环境记得关掉,否则日志量太大而且影响性能。
id-type: auto是全局主键策略,对应数据库的自增主键。MP的默认主键策略其实是ASSIGN_ID,也就是雪花算法生成一个19位Long型ID。如果你的表主键是MySQL自增int,不配置这一项,插入数据时MP会主动把一个超长数字ID塞进insert语句里,轻则数据怪异,重则直接报错。
mapper-locations: classpath*:/mapper/**/*.xml是XML文件扫描路径。即使你刚开始只用BaseMapper的通用方法不写XML,我也建议先把这一行配上。因为后续只要写一个自定义SQL拦截或复杂查询,就需要XML文件,到时候再加配置容易忘记。
3.2 主类扫描与Mapper注册
Spring Boot项目里,最标准的注册方式是在启动类上加@MapperScan注解:
@SpringBootApplication @MapperScan("com.example.project.mapper") public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } }@MapperScan的作用是告诉Spring:"去这个包底下找所有Mapper接口,帮我把它们的代理实现创建出来。"没有这个注解,你的Mapper接口就算写在包下面,Spring容器里也不会有对应的Bean。
如果你不想用@MapperScan,也可以在每一个Mapper接口上加@Mapper注解,效果一样。但一个项目里Mapper少说也有几十个,每个接口都加注解比较啰嗦。我推荐用@MapperScan一次性解决。
这里还有一个细节:@MapperScan扫描的包不要只写到com.example这一层,如果层级太深或上下文里存在多个Datasource,容易出现"部分Mapper没扫到"。我建议精确到具体的mapper包。
3.3 实体类字段与主键的对应配置
配置对象是application.yml,但真正干活的是实体类。很多人以为MP能自动识别所有数据库表结构,这是个误解,它仍然需要你告诉它表和类的关系。
比如有一张user表,主键id是自增的,字段有user_name和email,对应实体类就是:
@TableName("user") public class User { @TableId(type = IdType.AUTO) private Long id; @TableField("user_name") private String userName; private String email; }这里的@TableName("user")是表名映射。如果你的类名和表名一致(比如类名是User,表名是user),其实可以省略。但为了防止表名加了下划线前缀(比如t_user)的情况,建议显式标注。
@TableId是主键标识。type = IdType.AUTO表示主键由数据库自动生成,插入时不需要MP给主键赋值。如果你表主键是另一个字段名,比如uid,就在注解里指定value = "uid"。实体类里没有加@TableId注解的普通字段,MP默认以属性名映射到同名字段,下划线转换规则由之前的map-underscore-to-camel-case决定。
如果你的字段名和数据库字段名差异很大,比如Java属性叫userName,数据库字段叫name,那就必须用@TableField("name")显式指定。依赖自动映射可以省事,但别把它当万能。
4. 从零快速实现第一个增删改查
4.1 继承BaseMapper获取通用CRUD
环境配置好了,实体类也建好了,接下来是最爽的一步:写一个Mapper接口。
@Mapper public interface UserMapper extends BaseMapper<User> { }你没看错,一个接口,完事了。这个UserMapper继承了BaseMapper<User>,而BaseMapper帮我们内置了几十个通用方法,不用写任何SQL,直接注入调用:
int insert(T entity):插入一条记录int deleteById(Serializable id):按主键删除int updateById(T entity):按主键更新T selectById(Serializable id):按主键查询List<T> selectList(Wrapper<T> queryWrapper):条件查询IPage<T> selectPage(IPage<T> page, Wrapper<T> queryWrapper):分页查询
我见过一个真实案例:一个权限管理项目,菜单、角色、用户三张表的CRUD全部用BaseMapper自带方法完成,Mapper XML文件数量为0。整个持久层代码量少了一大半,而且因为每张表的基础操作都是同一套方法,团队里任何人接手都能立刻上手。
如果你需要对某张表做批量插入,BaseMapper还提供了insertBatchSomeColumn(需要自定义注入方法),或者直接用IService里的saveBatch。批量操作在大数据量导入场景下性能提升非常明显。
4.2 Service层和IService的配合使用
实际业务中,Controller不会直接调Mapper,通常还会隔一层Service。MP在这里也给了现成的模板类。
public interface UserService extends IService<User> { } @Service public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService { }你只要让Service接口继承IService<User>,实现类继承ServiceImpl<UserMapper, User>,就自动拥有了save、saveBatch、getById、lambdaQuery、page等一串方法,连ServiceImpl都不用写。
这些方法背后是怎么实现的?ServiceImpl内部维护了一个BaseMapper引用,通过泛型注入到子类里。所以你在业务代码里直接注入UserService,就能调用Service层封装好的CRUD、批量操作甚至链式查询,Controller层代码可以非常干净。
这里有个容易踩的坑:如果你的Mapper是自定义的,里面有自己写的方法,IService里是拿不到的。ServiceImpl只暴露BaseMapper通用方法,自定义Mapper方法仍然要走userMapper.xxx()。所以别指望Service把一切都包圆了,复杂查询还是需要把Mapper单独注入进来。
4.3 条件构造器:QueryWrapper和LambdaQueryWrapper
这是MP最核心的杀手锏。以前用原生MyBatis写动态查询,要在XML里拼<if>标签,条件一多XML就变得很长。MP的Wrapper可以让你用Java代码直接构造查询条件。
public List<User> searchUsers(String name, Integer minAge, Integer maxAge) { return userMapper.selectList(new LambdaQueryWrapper<User>() .like(StringUtils.hasText(name), User::getUserName, name) .ge(minAge != null, User::getAge, minAge) .le(maxAge != null, User::getAge, maxAge) .orderByDesc(User::getId)); }这里用LambdaQueryWrapper的好处是:直接用User::getUserName这种方法引用,不涉及硬编码字段名。如果哪天实体类属性改名,编译器就能直接帮你发现错误。而老的QueryWrapper是用字符串写列名(比如"user_name"),一旦和数据库字段不一致,编译不报错,运行才报,排查起来相当痛苦。
Wrapper的逻辑就是一套链式条件方法:eq等于、like模糊、ge大于等于、le小于等于、between区间、in集合、isNull为空、orderByDesc倒序排序。每个方法的第一个参数是布尔值,只有为真时才追加这个条件。这就是"动态查询"的实现机制——条件成立就拼进SQL,不成立就跳过,从根本上告别了一堆<if>标签。
如果你担心Wrapper拼接出来的SQL不够直观,我建议开发阶段一定把log-impl打开,控制台会打印最终执行的SQL和参数,一眼就能看出条件构造是不是符合预期。
5. 加入项目后必踩的坑:常见问题与排查实录
5.1 Mapper接口为什么一直扫不到
报错类型通常是Invalid bound statement (not found)或启动时Field userMapper in XxxService required a bean of type 'UserMapper' that could not be found。
这个问题90%的原因是@MapperScan没有扫描到Mapper接口所在的包。比如Mapper接口放在com.example.project.mapper,但启动类上写的是@MapperScan("com.example.project")或者完全没写。注意,@MapperScan的扫描机制和Spring Boot的自动扫描不是一回事,你不写它或者路径不对,Spring容器里就不会有Mapper的代理Bean。
还有少部分原因是Mapper接口没有继承BaseMapper。没有继承的接口在MP里不会被识别成MP的Mapper,即使加上@Mapper注解,也无法调用通用方法。
排查思路很简单:先把启动类上的@MapperScan路径改成精确的Mapper包路径,然后在Mapper接口上再补一个@Mapper注解,看看能不能启动。如果还不行,检查是不是在多个模块之间出现了重复扫描。
5.2 分页插件不生效,查出来的还是全表数据
MP的分页不是自动开启的,需要注册一个内部拦截器,也就是MybatisPlusInterceptor并添加分页插件。如果不注册,调用selectPage时MP虽然能接收Page对象,但生成的SQL只是普通查询,不会自动拼接LIMIT。
正确的配置方式如下:
@Configuration public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }DbType.MYSQL要和你实际的数据库类型一致。如果你用的是PostgreSQL、Oracle、SQL Server,这里也要对应改。分页方言不同,生成的物理分页SQL也不同,配错了虽然有时候也能跑,但结果可能不对。
分页不生效还有一种隐蔽情况:有些项目里同时存在多个SqlSessionFactory或者多数据源,拦截器只注册到了主数据源上。此时副数据源的分页完全不生效。这种场景建议单独为每个数据源注册各自的拦截器,或者把所有数据源的SqlSessionFactory统一指向同一个MybatisPlusInterceptor。
5.3 主键策略冲突:雪花ID还是自增ID
表现是:插入数据成功后,数据库里主键是一串超长数字,或者干脆报主键重复、主键不能为null等错误。这多半是主键策略没配对。
MP默认使用雪花算法分配ID,适用于分布式场景下不依赖数据库自增主键的系统。但如果你的表主键是MySQL自增int或bigint,默认的雪花策略就不合适。解决方式有两种:
第一种是全局配置,在application.yml里:
mybatis-plus: global-config: db-config: id-type: auto第二种是局部配置,在实体类主键字段上加:
@TableId(type = IdType.AUTO) private Long id;我个人的习惯是:全局配置设成auto,因为绝大多数单库单表项目都用自增主键。如果某个表确实要用分布式ID,再在实体类上单独覆盖。两种方式优先级上,实体类注解高于全局配置,所以不会冲突。还有一个容易遗漏的点:如果主键字段在实体类里叫id,但数据库主键叫uid,那么@TableId里还要加上value = "uid"。
5.4 查询结果某些字段一直是null
数据库字段是user_name,实体属性是userName,查询返回后userName却是null。这是映射失效的典型症状。
虽然前面说过map-underscore-to-camel-case默认开启,但有一种常见翻车方式:这个配置被MybatisPlusConfig里的自定义Configuration覆盖或者被手动置为false了。还有一种是字段类型不匹配,比如数据库是datetime,实体里用了String,MP虽然能尽力转换,但一些特殊格式下配置不完整时会返回null或报转换异常。
排查顺序:先确认map-underscore-to-camel-case值为true;再检查实体字段上有没有手写的@TableField冲突;实在不行,在SQL日志里看查询列名和实体属性是否对得上。另外,某些团队小伙伴习惯在实体上用Lombok的@Data,如果字段名写错JetBains插件又不报错,运行起来就是null,敲代码的时候多留个心眼。
5.5 版本冲突一锅端:JDK17、Spring Boot 3和其他MyBatis依赖
把MP加进一个已有的Spring Boot 2.x项目通常不会有大问题,但如果是升级到Spring Boot 3.x再引入MP,下面几个问题我建议提前检查:
第一,确保引入的是mybatis-plus-spring-boot3-starter,不是老的boot starter。第二,检查项目里有没有残留的mybatis-spring-boot-starter、pagehelper-spring-boot-starter这类和MyBatis强相关的依赖,很可能和MP内部的MyBatis版本打架。第三,JDK版本。Spring Boot 3要求JDK 17,MP的boot3 starter也基于JDK 17编译,如果你的开发环境还在JDK 8,依赖都编译不过。
还有一个小众但真实存在的情况:项目里同时用了MyBatis Plus和ShardingJDBC或Seata这类分布式组件,它们的版本兼容性问题会让MP启动直接失败。处理这类问题没有捷径,先把MP单独跑一个demo排除自身问题,再和中间件组合排查,效率最高。
| 问题现象 | 大概率原因 | 处理方式 |
|---|---|---|
| Mapper Bean找不到 | @MapperScan路径不对或没写 | 精确扫描到mapper包,必要时加@Mapper |
| 分页查全表 | 没注册PaginationInnerInterceptor | 配置MybatisPlusInterceptor并加分页插件 |
| 主键是超长数字 | 默认雪花策略和自增主键冲突 | 全局或实体配置IdType.AUTO |
| 字段还是null | 驼峰映射被关或字段映射错误 | 检查配置,显式标注@TableField |
| 启动报MyBatis相关异常 | starter版本不对或依赖重复 | 换成对应boot3 starter,清理老MyBatis依赖 |
6. 集成后的下一步:推荐几个必学操作
集成完成、基础CRUD跑通之后,MP的进阶功能才是拉开效率差距的地方。
第一个是逻辑删除。业务系统里物理删除的坑太多,审计、恢复、数据一致性都有隐患。MP的配置方式很简单,实体类加@TableLogic注解,配置logic-delete-field,之后MP生成的所有删除操作都会自动变成UPDATE ... SET deleted=1,查询自动追加deleted=0条件。这比你自己在每个SQL里手写条件安全得多。
第二个是字段自动填充。比如表的创建时间create_time、更新时间update_time,如果你还要在每个Service里手动set时间,就太浪费MP这个功能了。写一个MetaObjectHandler实现类,在insert时自动填充创建时间和更新时间,在update时自动更新版本号和时间,所有实体类统一生效。
第三个是乐观锁。配置一个OptimisticLockerInnerInterceptor,实体类加@Version注解,MP会在更新时自动拼接version条件。对于并发修改场景,能有效避免脏更新问题,代码量几乎为零。
第四个是代码生成器。等你把MP的基础配置都吃透了,再用mybatis-plus-generator反向生成实体、Mapper、Service,进一步减少重复工作。但我建议不要一上来就用生成器,因为生成的代码是你的,但框架的原理你还没掌握,出了问题自己都不知道去哪查。
7. 习惯养成:加MP前做足这三件事
最后分享几条我在几个真实项目里体会出来的实操准则。
第一,加MP之前,先在本地写一个最小可运行的demo。哪怕只有一个Controller、一个Mapper、一张表,先把依赖、配置、启动链路跑通,再把它并进正式项目。很多人是直接在正式项目里加依赖,边加边改,改到一半发现项目启动不了,回滚也不是,继续改也不是,非常被动。
第二,严格区分全局配置和局部注解的边界。全局配置管通用逻辑,比如id自增策略、日志开关、逻辑删除字段名;局部注解管特殊情况,比如某个表的自定义主键、某字段的特殊映射。不要全局配置一把梭,也不要每个字段都靠注解标注,找到那个平衡点会让代码清爽得多。
第三,把SQL日志当成调试伙伴。MP自动生成的SQL往往和你手写的思路不完全一样,尤其是Wrapper构造的动态查询。只有看到了真实SQL,你才能判断条件构造是否正确、是否走了预期索引。生产环境再关掉,开发环境一直开着。
你问我"怎么把mybatis plus加入到项目",其实核心就是三步:选对依赖版本、配置好数据源和扫描路径、继承BaseMapper跑通一个CRUD。剩下的分页、逻辑删除、自动填充这些高级功能,都是在这个闭环之上慢慢叠加的。按我上面这套顺序走一遍,半天之内你就能在一个新项目里正常用MP了。以后再遇到什么奇怪报错,先看自己是不是踩了版本冲突、Mapper扫描、分页插件这三个老坑。我的习惯是每集成一个新项目,就顺手把这段流程沉淀成团队的初始化文档,之后新同学上手基本不用我多说。