
把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在开发效率和可维护性之间取得了很好的平衡。维度原生MyBatisSpring Data JPAMyBatis Plus单表CRUD手写SQL和XML接口方法自动实现继承BaseMapper即可复杂SQL完全可控需要Query或Specification完全可控同MyBatis学习曲线中等偏高很低动态查询需要自己拼SQLCriteria API较繁琐QueryWrapper/LambdaWrapper分页手写PageHelper或InterceptorPageable内置内置分页插件当然没有框架是万能的。如果你的项目需要极度复杂的动态查询、频繁跨多表操作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.xJDK至少是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/11mybatis-plus-boot-starter使用3.5.x版本即可Spring Boot 3.x JDK 17mybatis-plus-spring-boot3-starter注意坐标带boot3Gradle项目implementation同上坐标不变换用Gradle语法原生Spring MVC非Bootmybatis-plus不带starter需手动配MyBatis2.2 在pom.xml中引入依赖以一个标准的Spring Boot项目为例。我用的是Maven所以直接在pom.xml的dependencies里加入dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.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 groupIdcom.baomidou/groupId artifactIdmybatis-plus/artifactId version3.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?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/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_nameJava实体属性叫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 BaseMapperUser { }你没看错一个接口完事了。这个UserMapper继承了BaseMapperUser而BaseMapper帮我们内置了几十个通用方法不用写任何SQL直接注入调用int insert(T entity)插入一条记录int deleteById(Serializable id)按主键删除int updateById(T entity)按主键更新T selectById(Serializable id)按主键查询ListT selectList(WrapperT queryWrapper)条件查询IPageT selectPage(IPageT page, WrapperT queryWrapper)分页查询我见过一个真实案例一个权限管理项目菜单、角色、用户三张表的CRUD全部用BaseMapper自带方法完成Mapper XML文件数量为0。整个持久层代码量少了一大半而且因为每张表的基础操作都是同一套方法团队里任何人接手都能立刻上手。如果你需要对某张表做批量插入BaseMapper还提供了insertBatchSomeColumn需要自定义注入方法或者直接用IService里的saveBatch。批量操作在大数据量导入场景下性能提升非常明显。4.2 Service层和IService的配合使用实际业务中Controller不会直接调Mapper通常还会隔一层Service。MP在这里也给了现成的模板类。public interface UserService extends IServiceUser { } Service public class UserServiceImpl extends ServiceImplUserMapper, User implements UserService { }你只要让Service接口继承IServiceUser实现类继承ServiceImplUserMapper, 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 ListUser searchUsers(String name, Integer minAge, Integer maxAge) { return userMapper.selectList(new LambdaQueryWrapperUser() .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实体里用了StringMP虽然能尽力转换但一些特殊格式下配置不完整时会返回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 17MP的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 deleted1查询自动追加deleted0条件。这比你自己在每个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扫描、分页插件这三个老坑。我的习惯是每集成一个新项目就顺手把这段流程沉淀成团队的初始化文档之后新同学上手基本不用我多说。