写这篇东西的时候Spring Boot 3.0 刚发布那阵子我正在把一个老项目从 2.7 往上升级当时最大的感觉就是“网上资料鱼龙混杂照着抄全是坑”。尤其是 Mybatis Plus 这块很多人还在用老的mybatis-plus-boot-starter依赖一启动直接报错或者明明配好了分页却死活不生效。今天这篇就把我这几个月折腾 Spring Boot 3.0 集成 Mybatis Plus 的完整过程、版本选型、踩坑记录全部倒出来给准备升级或者正在踩坑的朋友一份能直接照着做的参考。1. 整体设计思路与版本选型拆解1.1 Spring Boot 3.0 到底变了什么要搞清楚 Mybatis Plus 为什么在 Spring Boot 3.0 里会有一堆兼容性问题先得明白 Spring Boot 3.0 本身动了哪些“地基”。最大的变化就是JDK 基线提到了 17这意味着你本地如果还在用 JDK 8 或者 11第一关就过不去。其次是Jakarta EE 9 的命名空间迁移以前我们 import 的javax.servlet、javax.persistence全部变成了jakarta.servlet、jakarta.persistence这个变化直接导致了大量第三方框架需要重新适配。我当时第一次升级的时候pom 里依赖还是老的坐标启动直接抛ClassNotFoundException: javax.servlet.Filter说白了就是 Spring Boot 3.0 的容器已经不认javax那一套了。另一个容易被忽略的点是Spring Framework 6.0对 AOTAhead-Of-Time和 GraalVM Native Image 的支持虽然大部分人用不上但框架内部对字节码和反射的处理方式变了所以很多老版本的三方库在 3.0 下会出现“能编译但跑不起来”的诡异问题。理解了这几点你再看 Mybatis Plus 的适配版本就顺理成章了。1.2 Mybatis Plus 适配版本的判断逻辑Mybatis Plus 官方针对 Spring Boot 3 专门出了一个新的 starter 坐标叫做mybatis-plus-spring-boot3-starter这个很容易被忽视。老项目用的mybatis-plus-boot-starter是给 Spring Boot 2.x 用的内部的自动配置类很多直接引用了javax.annotation之类的旧 API在 Spring Boot 3.0 下要么编译报错要么运行时出现各种奇奇怪怪的 Bean 加载异常。判断自己该用哪个版本的逻辑其实很简单Spring Boot 2.x JDK 8/11 用老的mybatis-plus-boot-starterSpring Boot 3.x JDK 17 用新的mybatis-plus-spring-boot3-starter。我当时查了不少资料发现很多文章还是四月之前的老内容误导性极强所以我干脆把这个问题写清楚。这里建议直接在 pom.xml 里确认一下版本号我的实际搭配是Spring Boot 3.0.2 mybatis-plus-spring-boot3-starter 3.5.3.1跑起来非常稳。1.3 为什么选 Mybatis Plus 而不是 MyBatis 原生或 JPA其实在 Spring Boot 3.0 环境下可供选择的 ORM 方案不少比如官方推荐的 Spring Data JDBC、JPA、以及 MyBatis 原生。我最终选了 Mybatis Plus核心原因是它在保留了 MyBatis 灵活 SQL 控制能力的同时把单表 CRUD、分页、逻辑删除这些高频操作全部封装好了。对于中小型项目来说这套组合可以大幅减少基础 Mapper XML 的编写量比 JPA 那种重度封装容易排查问题也比纯 MyBatis 少写一半样板代码。拿我实际开发的一个订单模块举例业务里有大量的单表条件查询、分页、状态更新操作。如果用原生 MyBatis每个操作都要写 XML 映射和接口方法折腾下来光一个订单表就多出好几十行配置但 Mybatis Plus 的BaseMapper直接提供了selectPage、updateById、selectList等方法。更关键的是它内置的逻辑删除和乐观锁插件在 Spring Boot 3.0 下通过几个配置类就能搞定不需要动业务代码层面这在升级改造的时候优势特别明显。2. 核心配置细节与关键依赖解析2.1 依赖引入的正确姿势这一步是整个集成过程里最容易出问题的地方我直接给出验证过的完整依赖坐标组合。以 Spring Boot 3.0.2 为例pom.xml 里除了常规的spring-boot-starter-web最核心的两段是 Mybatis Plus 和数据库驱动。Mybatis Plus 官方对 Spring Boot 3 提供了独立分支注意不要用错了dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-spring-boot3-starter/artifactId version3.5.3.1/version /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency有两个细节值得留意。第一mysql-connector-j是新版的依赖坐标旧版mysql-connector-java在 Spring Boot 3.0 里虽然也能用但官方已经不建议使用了。第二如果你的项目会用到代码生成器、动态 SQL 等高级功能还需要补上mybatis-plus-extension但纯集成的话上面的两个依赖就够用了。2.2 application.yml 中的配置陷阱配置文件的写法也和 2.x 时代有些区别主要在于Mybatis Plus 的配置项在 Spring Boot 3.0 下必须使用mybatis-plus前缀不能再混用mybatis前缀。下面是我放在生产环境验证过的配置spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/test_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: root mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl map-underscore-to-camel-case: true global-config: db-config: id-type: assign_id logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0 mapper-locations: classpath*:/mapper/**/*.xml这里我吃了好几次亏。第一个坑是mapper-locations如果你在 resources 目录下新建了mapper文件夹存放 XML 文件但忘了配置这个路径运行时会报Invalid bound statement (not found)这个错排查起来挺浪费时间。第二个坑是map-underscore-to-camel-case这个配置它在 Spring Boot 3.0 下必须放在mybatis-plus.configuration下面而不是直接放在mybatis-plus.global-config下面层级写错了的话属性根本读取不到。2.3 Mapper 扫描注解的处理启动类上的MapperScan注解是固定操作但有一点需要特别注意如果你没有加MapperScan那么在 Spring Boot 3.0 下即便你的 Mapper 接口写了Mapper注解也可能遇到 Bean 注入失败的报错。这算是一个典型的细微差别归根结底在于 Mybatis Plus 3.5.x 之后有些注册逻辑的调整。我习惯保留启动类上的MapperScan(com.example.demo.mapper)这样接口不会被遗漏并且写 XML 的时候也不用每个文件都加 namespace 之外的东西。3. 完整实操从零搭建一个可运行的项目3.1 项目结构和数据库准备我这次搭建的示例是一个用户管理模块涉及用户的增删改查和分页列表。数据库表结构很简单核心字段包括id、name、age、email、deleted、create_time。其中deleted字段用作逻辑删除标识create_time用于测试自动填充功能。建表语句就不展开了大概就是一张标准的用户表字段类型以 BIGINT 和 VARCHAR 为主。项目的包结构我给大家一个可以直接复用的模板启动类放在com.example.demo实体类放在entity包Mapper 接口放在mapper包Service 和 Controller 各自独立分包。这样可以避免包扫描时踩到 Spring 的 Bean 装配顺序问题也能让后续的代码生成器直接使用这套结构。3.2 实体类和 Mapper 接口编写实体类最好直接继承 Mybatis Plus 提供的Model类好处是后续可以调用实例方法执行一些简单的数据的 CRUD 操作比如user.insertById()这种写法规整又直观。核心的注解是TableName(t_user)和TableId(type IdType.ASSIGN_ID)前者解决表名和类名不一致问题后者让你不用手写雪花算法生成主键TableName(t_user) public class User extends ModelUser { TableId(type IdType.ASSIGN_ID) private Long id; private String name; private Integer age; private String email; TableField(fill FieldFill.INSERT) private LocalDateTime createTime; TableLogic private Integer deleted; }Mapper 接口更加简单什么都不用写直接继承BaseMapperUser就有二十多个现成方法可用。这里有个我经常想吐槽的点很多人喜欢在 Mapper 里加各种自定义 SQL明明单表查询用selectListLambdaQueryWrapper就能解决非得去写 XML白白增加维护成本。等真正需要多表 join 或者复杂动态 SQL 的时候再上 XML 也不迟。Mapper 接口的写法如下Mapper public interface UserMapper extends BaseMapperUser { }3.3 Service 层和 Controller 层的实现Service 层我习惯直接继承ServiceImpl这是 Mybatis Plus 最实用的封装之一。它把BaseMapper和通用 Service 逻辑做了一次集成让你在 Service 里可以直接调用save、list、page等方法不需要绕一圈去调用userMapper.insert那么繁琐。这段核心代码如下Service public class UserService extends ServiceImplUserMapper, User { public IPageUser selectUserPage(PageUser page, User query) { LambdaQueryWrapperUser wrapper new LambdaQueryWrapper(); wrapper.like(StringUtils.hasText(query.getName()), User::getName, query.getName()) .ge(query.getAge() ! null, User::getAge, query.getAge()) .orderByDesc(User::getCreateTime); return this.page(page, wrapper); } }Controller 层其实就是一个薄薄的请求转发层把基础 CRUD 和分页查询暴露成 REST API。我在项目里习惯返回统一的结果结构Result不过这里为了减少无关代码就用最简单的Map代替了。核心是分页参数的传递方式current和size分别表示页码和每页条数Mybatis Plus 会自动帮我们把total统计出来不需要手写SELECT COUNT(*)。3.4 分页插件配置类的完整代码分页插件在 Spring Boot 3.0 下和旧版最大的区别在于必须使用MybatisPlusInterceptor配合PaginationInnerInterceptor的方式不能再用旧版的PaginationInterceptor。这个改动让很多人升级后分页失效查出来的居然是全表数据。配置类代码如下Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }这个配置类有一个必须注意的参数DbType.MYSQL。如果你用的是 PostgreSQL 或者其他数据库这里一定要改成对应的DbType否则分页 SQL 的方言生成错误。PaginationInnerInterceptor内部会根据数据库类型生成不同的LIMIT或OFFSET语句用错了数据库类型轻则分页结果不对重则直接抛 SQL 语法异常。3.5 启动测试与基础 CRUD 验证做完以上步骤启动服务后可以通过几个简单的测试用例来验证集成是否成功。我习惯直接写一个集成测试类或者使用 Controller 的 API 进行 Postman 测试。比如调用/user/list接口正常情况下会返回 JSON 数组调用/user/page?current1size2接口返回的records应该是两条数据同时带total总数。我实际跑了一遍之后控制台会输出完整的 MyBatis 日志。这里要注意配置里我开了StdOutImpl日志输出用它来看实际执行的 SQL 语句非常方便生产环境建议换成Slf4jImpl并调整日志级别。确认基础增删改查没问题后集成工作就基本完成了。4. 常见问题与排查技巧实录4.1 启动时提示找不到 SqlSessionFactory 或 SQL 会话工厂相关错误这个错误在 Spring Boot 3.0 下出现的频率很高尤其是当你把老的mybatis-plus-boot-starter误用到 3.0 项目里时。排查思路很直接第一步坚持mvn dependency:tree看项目里是否引入了重复的 mybatis 相关依赖第二步确认用的是mybatis-plus-spring-boot3-starter而不是老版本第三步如果你的 Maven 仓库里有老版本的 jar 包缓存建议mvn clean之后强制更新-U参数重新拉取依赖。我之前碰到过一种情况代码里明明引入了新坐标但打包时依然报错最后发现是 IDEA 的 Maven 依赖索引没刷新重启 IDE 之后解决。这类问题其实不难难的就是排除掉环境脏数据带来的干扰。4.2 分页查询失效且查出全表数据这个问题十个人里有九个会遇到。原因几乎全是漏掉了前面说的MybatisPlusInterceptor配置或者配置了但没把它加入 Spring 容器。Spring Boot 3.0 之后旧版的PaginationInterceptor不再生效必须使用新的MybatisPlusInterceptor方式。如果确认配置类已经加好了还不行检查一下启动类上有没有SpringBootApplication默认扫描不到你的配置包这种情况把启动类的位置调整到项目的顶层即可。4.3 Mapper XML 文件无法加载提示 Invalid bound statement这个问题的排查点集中在两个地方。第一mapper-locations是否配置正确我上面提供的classpath*:/mapper/**/*.xml这个写法是验证过的而且classpath*:带不带星号差别很大带星号才能扫描到 jar 包内的 XML 文件。第二XML 文件里的namespace是否和 Mapper 接口的全限定名完全一致。这俩检查完基本就没有意外了。另外补充一个实用技巧在测试阶段把 XML 文件放到src/main/resources/mapper目录下编译器会自动复制到 target 目录真不放心可以编译后打开 target 目录检查一下 XML 是否真的被复制进去了。4.4 LocalDateTime 序列化后格式不符合预期这个问题严格说和 Mybatis Plus 关系不大但在升级 Spring Boot 3.0 后容易被一并触发。默认情况下接口返回LocalDateTime字段时可能会显示成一长串时间戳数字非常不直观。解决方式是给字段加JsonFormat注解或者全局配置 Jackson 的时间格式。我推荐全局配置这样所有接口的时间格式都会统一Configuration public class JacksonConfig { Bean public Jackson2ObjectMapperBuilderCustomizer jacksonCustomizer() { return builder - builder .serializerByType(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(yyyy-MM-dd HH:mm:ss))) .deserializerByType(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(yyyy-MM-dd HH:mm:ss))); } }4.5 常见问题速查表错误现象可能原因解决方案启动报 ClassNotFoundException (javax.*)使用了适配 Spring Boot 2.x 的老依赖改用 mybatis-plus-spring-boot3-starter分页返回全部记录缺少 MybatisPlusInterceptor 分页拦截器配置新式分页插件 BeanInvalid bound statementMapper XML 未扫描到或 namespace 错误检查 mapper-locations 路径与 namespaceBean 注入失败启动类无法扫描 Mapper 接口添加 MapperScanSQL 时区异常JDBC URL 缺少 serverTimezone设置 Asia/Shanghai 时区表名映射失败实体类与表名不一致使用 TableName 指定5. 生产环境下的进阶实践心得5.1 逻辑删除与自动填充的配置心得逻辑删除是个特别实用的功能但配置不当会留下隐患。我在application.yml里定义了logic-delete-field: deleted然后实体类的deleted字段加了TableLogic注解。这样做的好处是Mybatis Plus 会自动在SELECT时追加WHERE deleted 0在DELETE操作时自动改写为UPDATE语句。但这条规则对自定义 SQL 不生效如果你在 XML 里手写 SQL必须自己拼上deleted 0条件这是我踩过坑后才明白的。自动填充也让create_time这类公共字段省心不少。实现MetaObjectHandler接口在insertFill方法里设置创建时间再配合实体类上的TableField(fill FieldFill.INSERT)注解新增数据就不用手动 set 时间了。这个配置在 Spring Boot 3.0 下没有变化直接平滑迁移。5.2 防全表更新与更新操作的安全加固Mybatis Plus 有一个默认的保护机制就是执行 update 或 delete 时如果没有带条件会直接报错拒绝执行这是 3.x 之后新增的block-attack特性。开发时我建议把这个特性打开避免手滑把整张表的数据清空或覆盖。配置方法是在application.yml里mybatis-plus: global-config: banner: false db-config: where-strategy: not_emptywhere-strategy: not_empty的含义是当更新或删除操作的条件值为空字符串时会被过滤掉进一步防止误操作。生产环境我还会配合审计日志记录每次操作的用户信息这样一旦出现问题能快速定位到责任人。5.3 复杂查询该用 LambdaQueryWrapper 还是 XML我从实践角度给个判断标准单表查询、条件不固定且状态字段较多时优先用LambdaQueryWrapper多表 join、需要子查询或动态拼接大段复杂 SQL 时老老实实写 XML 并用Param传参。LambdaQueryWrapper的优势是类型安全字段名写错会在编译期直接暴露且代码可读性高但碰到复杂的多条件分支组合时会出现难以维护的长链式调用。Mybatis Plus 3.5.x 提供了QueryWrapper和LambdaQueryWrapper两套 API实际项目里我两种都用把握住“简单用封装、复杂走 XML”的原则即可。5.4 升级后代码生成器的调整之前用 Mybatis Plus 代码生成器AutoGenerator的时候在 Spring Boot 3.0 下有一个资源路径的通配符问题需要把模板引擎的类路径稍做调整。我建议直接从官方文档拉最新的生成器代码重点看引入的VelocityTemplateEngine初始化部分老代码里有个路径前缀classpath:/在新版本里可能会不识别。生成出来的代码结构基本一致但 Controller 和 Service 的一些默认注释风格可以根据团队规范微调。6. 性能优化与 SQL 日志排查经验6.1 慢 SQL 定位方法集成完成只是第一步生产环境里 SQL 的性能问题往往比集成本身更棘手。Mybatis Plus 提供了内置的 SQL 性能分析插件可以通过拦截器打印每个 SQL 的执行时间。在 Spring Boot 3.0 下用法和分页插件类似增加一个PerformanceInterceptor不过要注意旧版的PerformanceInterceptor在新版本中已拆分到mybatis-plus-extension模块里。实际项目里我比较少长期开启这个插件因为它对性能有轻微损耗线上更推荐在数据库层面开启慢查询日志比如 MySQL 的slow_query_log双管齐下定位效率更高。6.2 大批量插入与批处理优化用 Mybatis Plus 的saveBatch做大批量插入时很多人反应速度很慢其实不是框架问题而是 JDBC 连接串没开启批量处理。在application.yml的数据源 URL 后面追加两个参数能明显提升效率url: jdbc:mysql://localhost:3306/test_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghairewriteBatchedStatementstrueuseCompressiontruerewriteBatchedStatementstrue是关键MySQL 驱动会在 JDBC 层把多条插入语句重写成批量插入性能提升十分明显。我做过一次简单测试一万条数据的插入时间从原来的七八秒缩短到两秒以内这个优化值得任何人都做一下。6.3 热部署与自动重启的兼容提醒Spring Boot 3.0 开发时如果配合spring-boot-devtools使用偶尔会出现 Mybatis Plus 的 Mapper 接口代理失效的奇怪报错。原因是 devtools 默认使用两个类加载器导致 Mybatis 的 mapper 注册进了一个和你的接口不同的 classloader 实例里。虽然在新版本中官方已经做了兼容处理但我个人建议在开发阶段关闭 devtools 的自动重启或者只保留spring-boot-starter-tomcat的 jrebel/热部署插件看起来省事实际排查问题时会省下大量时间。7. 总结一些我个人的实操感受这一套 Spring Boot 3.0 集成 Mybatis Plus 的方案是我在多个项目上反复验证过的核心思路可以归纳成三句话用对依赖坐标、配好分页拦截器、写清 mapper-locations。这三个点占掉了日常 80% 的报错原因。其它技巧类的功能比如逻辑删除、自动填充、防全表更新是锦上添花在架构设计时提前引入比后期改造划算得多。最后分享一个我踩过的真实教训升级不是一锤子买卖把老项目从 Spring Boot 2.7 升到 3.0 的时候千万不要只改版本号就完事一定要全局搜索javax.开头的包名逐个替换成jakarta.同时把第三方依赖的版本全部梳理一遍尤其是数据库驱动、连接池、以及 Mybatis Plus 这类基础框架。把这一步做踏实了后续的集成才会顺利。希望这篇实操记录能帮你少走几个弯路遇到具体问题欢迎在评论区交流。