ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Spring Boot 3 整合 MyBatis-Plus 实战:依赖选型、分页插件与避坑指南

Spring Boot 3 整合 MyBatis-Plus 实战:依赖选型、分页插件与避坑指南

简介:这份资源是面向Java后端开发者与Spring初学者的整合实战示例,聚焦Spring Boot 3与MyBatisPlus的协同使用,帮助读者理解如何用最新版框架搭建简洁高效的数据访问层,解决传统MyBatis配置繁琐、CRUD模板代码重复的问题。压缩包共24个文件,约85KB,包含7个Java源码、7个class编译文件、2个yaml配置、1个properties、1个pom.xml及mvnw构建脚本等,覆盖依赖管理、数据源配置、Mapper接口、实体注解与Service、Controller分层结构,目录组织清晰,可直接导入IDE运行调试。目前已有1435人学习下载,适合希望快速上手Spring Boot 3整合MyBatisPlus、掌握条件构造器与通用CRUD用法的开发者参考,也可作为课程设计或小型项目的起步骨架,便于在此基础上扩展监控、权限等模块。

1. 从一次翻车的升级说起:Spring Boot 3 配 MyBatis-Plus 到底难在哪

去年把公司一个老后台从 Spring Boot 2.7 往 3.x 上抬,本来以为改改pom.xml的版本号就完事,结果启动直接报NoClassDefFoundError,翻日志才发现是javax.annotation换成了jakarta.annotation,MyBatis-Plus 的老版本压根没适配。这就是 Spring Boot 3 整合 MyBatis-Plus 最典型的坑:不是配置写错,是整条依赖链的命名空间变了。这份资源包给的就是一套能直接跑起来的整合骨架,包含父工程、启动类、Mapper 扫描、分页插件、代码生成器配置,以及一份能对照的application.yml。它解决的是「版本对不上、插件不生效、分页查出来总数是 0」这类反复折腾的问题,适合正在做新项目选型、或者手上有 2.x 项目要迁移的 Java 后端。下面我按自己实际搭一遍的顺序拆,参数和坑都标出来。

2. 依赖选型与版本对齐:为什么你的 MP 一升就崩

2.1 Spring Boot 3 带来的两个硬性变化

Spring Boot 3 最核心的变化是基线抬到了 Java 17,并且把整个javax.*体系迁到了jakarta.*。这意味着任何还在用javax.servlet、javax.annotation的第三方库,只要没出新版本,就会在启动时炸掉。MyBatis-Plus 从 3.5.3 开始才正式提供对 Spring Boot 3 的支持,而且它的 starter 名字也变了——不是mybatis-plus-boot-starter,而是mybatis-plus-spring-boot3-starter。这两个包名差一个spring-boot3,但导错了就是找不到自动配置类,启动日志里连 Mapper 都注册不上。

我一般会先把版本关系锁死再动手,避免边写边试。下面这张表是我在几个项目里验证过能稳定跑的组合,直接抄:

组件推荐版本说明
Spring Boot3.2.x3.0/3.1 也能用,3.2 对 Java 21 支持更好
MyBatis-Plus3.5.5+3.5.3 起支持 Boot3,3.5.5 修了几个分页 bug
JDK17 或 21编译和运行都别低于 17
MySQL 驱动8.0.33+用com.mysql.cj.jdbc.Driver

提示:如果你用的是 Spring Boot 3.0.x,MyBatis-Plus 至少 3.5.3;3.2.x 建议 3.5.5 以上,否则分页插件在count查询时可能返回空。

2.2 一份能直接用的 pom 依赖片段

父工程用spring-boot-starter-parent管版本,子模块只引 starter,别自己指定 MyBatis-Plus 的版本,交给父工程或dependencyManagement统一管。下面是我实际项目里抠出来的依赖段:

<!-- 父 pom 的 dependencyManagement 里锁定 MP 版本 --> <dependencyManagement> <dependencies> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-spring-boot3-starter</artifactId> <version>3.5.5</version> </dependency> </dependencies> </dependencyManagement> <!-- 子模块实际引入 --> <dependencies> <!-- Web 场景,Boot3 自带 jakarta 命名空间 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- 注意 artifactId 带 spring-boot3 --> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-spring-boot3-starter</artifactId> </dependency> <!-- MySQL 驱动,scope 用 runtime --> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency> <!-- Lombok 可选,但实体类写起来省事 --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> </dependencies>

这段配置的逻辑是:父工程锁死 MP 版本,子模块只声明「我要用」,避免多模块下版本漂移。mysql-connector-j是 MySQL 8 之后的新坐标,老教程里的mysql-connector-java在 Boot3 里虽然还能拉下来,但驱动类名和时区参数容易出问题。Lombok 用optional是为了不传递到下游模块,减少编译期依赖冲突。

参数上唯一要盯的是 MP 版本号,别用3.5.2或更早,那些版本里MybatisPlusAutoConfiguration还引着javax.annotation,Boot3 启动直接ClassNotFoundException。如果你在离线环境,先把mybatis-plus-spring-boot3-starter和它的传递依赖mybatis-plus-core、mybatis-plus-extension一起下全,否则会缺com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor。

3. 配置与插件装配:分页不生效多半是这里漏了

3.1 application.yml 里必须写对的几项

MyBatis-Plus 在 Boot3 下的配置前缀没变,还是mybatis-plus,但数据源部分要按 Boot3 的spring.datasource写。下面这份是我调通后的最小配置,直接复制改库名就行:

spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://127.0.0.1:3306/demo_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false username: root password: your_password mybatis-plus: # Mapper XML 位置,多模块下用 classpath* 避免只扫到一个 jar mapper-locations: classpath*:/mapper/**/*.xml # 实体扫描包,代码生成器生成的类放这里 type-aliases-package: com.example.demo.entity configuration: # 下划线转驼峰,数据库 user_name -> 实体 userName map-underscore-to-camel-case: true # 开发期打印 SQL,上线记得关 log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: # 主键策略,ASSIGN_ID 是雪花算法,AUTO 是数据库自增 id-type: assign_id # 逻辑删除字段配置,没这需求可删 logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0

这里几个参数值得单独说。mapper-locations用classpath*:而不是classpath:,是因为多模块打包后 XML 可能分散在多个 jar 里,单星号只会扫到第一个。map-underscore-to-camel-case默认其实是true,但有些团队在 2.x 时代手动关过,迁移时忘了打开,结果查出来字段全是 null。id-type选assign_id还是auto取决于建表语句:如果主键是bigint且没设AUTO_INCREMENT,用assign_id;如果数据库自增,用auto,否则插入时 MP 会自己塞一个雪花 ID 进去,和自增列打架。

3.2 分页插件和 Mapper 扫描的装配

分页查出来total是 0,九成是没注册MybatisPlusInterceptor。Boot3 下配置类写法没变,但要注意别用@MapperScan扫错包。下面是我常用的配置类:

@Configuration @MapperScan("com.example.demo.mapper") // 指向 Mapper 接口所在包 public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); // 分页插件,指定数据库类型为 MySQL PaginationInnerInterceptor pagination = new PaginationInnerInterceptor(DbType.MYSQL); // 单页最大条数,防止前端传 size=100000 拖垮库 pagination.setMaxLimit(500L); // 溢出总页数后是否处理,false 表示请求超页返回空 pagination.setOverflow(false); interceptor.addInnerInterceptor(pagination); return interceptor; } }

逻辑上,MybatisPlusInterceptor是个拦截器链,分页、乐观锁、防全表更新都往里面加。PaginationInnerInterceptor必须指定DbType,不指定的话它靠 JDBC URL 猜,某些连接池下猜不准,count语句就生成错了。setMaxLimit是血泪经验:不加这个,前端把size传成 10 万,数据库直接被打满。setOverflow(false)表示请求第 999 页但总共只有 3 页时返回空列表,而不是回到第一页,接口语义更清晰。

Mapper 接口本身不用加@Mapper注解,只要@MapperScan的包路径对,MP 会自动代理。但如果你在多个模块里都有 Mapper,@MapperScan可以写多个,或者用basePackages数组。常见翻车是启动类在com.example.demo,Mapper 在com.example.demo.mapper,扫描路径写成com.example.demo.mapper.*,多了一层通配反而扫不到,去掉.*才对。

4. 实体映射与 CRUD 落地:从建表到第一个接口

4.1 实体类和表结构的对应关系

MP 的实体映射靠注解,最常用的是@TableName、@TableId、@TableField。下面这张表是我建user表时的字段对照,实体类按它写就不会错:

数据库列类型实体字段注解
idbigintid@TableId(type = IdType.ASSIGN_ID)
user_namevarcharuserName靠驼峰自动映射,无需注解
ageintage无
create_timedatetimecreateTime@TableField(fill = FieldFill.INSERT)
deletedtinyintdeleted@TableLogic

对应实体类:

@Data @TableName("user") // 表名和类名不一致时必须写 public class User { @TableId(type = IdType.ASSIGN_ID) private Long id; private String userName; // 自动映射 user_name private Integer age; @TableField(fill = FieldFill.INSERT) // 插入时自动填充 private LocalDateTime createTime; @TableLogic // 逻辑删除标记 private Integer deleted; }

@TableName在表名和类名一致时可以省,但类名User对应表user时,某些数据库大小写敏感会出问题,我一般显式写上。@TableId的type要和全局id-type一致,否则以注解为准,容易和配置打架。@TableField(fill = ...)只是声明「这个字段要填充」,真正填充逻辑得自己写MetaObjectHandler,下面补上。

4.2 自动填充和基础 CRUD 验证

自动填充要单独实现一个处理器,否则createTime永远是 null:

@Component public class MyMetaObjectHandler implements MetaObjectHandler { @Override public void insertFill(MetaObject metaObject) { // 严格填充:字段有值时不覆盖 this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now()); } @Override public void updateFill(MetaObject metaObject) { this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now()); } }

strictInsertFill和setFieldValByName的区别在于,前者只在字段为 null 时填,避免覆盖业务手动设的值。写完这个,Mapper 继承BaseMapper<User>就能直接用:

public interface UserMapper extends BaseMapper<User> { // 自定义分页查询,XML 里写 SQL IPage<User> selectUserPage(IPage<User> page, @Param("age") Integer age); }

测试时先跑insert再跑selectById,确认createTime有值、deleted是 0。分页查询用Page<User> page = new Page<>(1, 10);再调selectPage,看返回的total是否等于表里实际条数。如果total是 0 但records有数据,回去检查第 3 章的拦截器有没有注册。

5. 避坑排查:整合路上最容易翻车的五件事

5.1 启动报 jakarta 相关 ClassNotFound

现象:启动日志里出现java.lang.NoClassDefFoundError: javax/annotation/PostConstruct或类似javax找不到。原因:引了老版本 MyBatis-Plus 或某个传递依赖还带着javax包。解决:把 MP 换成mybatis-plus-spring-boot3-starter3.5.3 以上,然后mvn dependency:tree搜javax.annotation,把带进来的老库排掉。

5.2 分页 total 恒为 0

现象:selectPage返回的records有数据,total却是 0。原因:没注册PaginationInnerInterceptor,或者注册了但没指定DbType。解决:按 3.2 节加拦截器,显式传DbType.MYSQL;如果用了多数据源,每个数据源都要单独配。

5.3 Mapper 注入失败提示找不到 Bean

现象:Field userMapper in ... required a bean of type 'UserMapper'。原因:@MapperScan路径写错,或者启动类不在 Mapper 的父包。解决:确认@MapperScan的值是 Mapper 接口所在包,不带.*;多模块时把启动类往上提一层,或者显式写多个包路径。

5.4 逻辑删除后查不出已删数据

现象:调deleteById后数据还在库里,但selectById查不到了。原因:@TableLogic生效了,MP 自动把delete改成update deleted=1,查询自动加deleted=0。解决:这是预期行为,要查全部得写自定义 SQL 绕过;如果不想用逻辑删除,把注解和配置里的logic-delete-field都去掉。

5.5 雪花 ID 前端精度丢失

现象:返回的id是1234567890123456789,前端 JS 接收后变成1234567890123456800。原因:JS 的Number精度只有 53 位,雪花 ID 是 64 位。解决:实体里id字段加@JsonSerialize(using = ToStringSerializer.class),或者全局配置 Jackson 把Long转String。

6. 代码生成器与多数据源:把重复劳动压到最低

6.1 用 FastAutoGenerator 生成一套 CRUD

MP 自带的代码生成器在 Boot3 下也能用,但依赖要单独引mybatis-plus-generator和模板引擎。我一般用FastAutoGenerator,几行就能出 entity、mapper、service、controller:

FastAutoGenerator.create( "jdbc:mysql://127.0.0.1:3306/demo_db?serverTimezone=Asia/Shanghai", "root", "your_password") .globalConfig(builder -> builder .author("dev") // 别写真实姓名 .outputDir(System.getProperty("user.dir") + "/src/main/java") .disableOpenDir()) // 生成后不自动打开文件夹 .packageConfig(builder -> builder .parent("com.example.demo") .entity("entity") .mapper("mapper") .service("service") .controller("controller")) .strategyConfig(builder -> builder .addInclude("user") // 只生成 user 表 .entityBuilder() .enableLombok() .logicDeleteColumnName("deleted") .controllerBuilder() .enableRestStyle()) // 生成 @RestController .execute();

addInclude里写表名,不写就生成全库,表多的时候会刷屏。enableLombok省掉 getter/setter,logicDeleteColumnName让生成的实体自动带@TableLogic。disableOpenDir在服务器上跑时有用,不然会尝试调图形界面报错。生成完别急着用,先检查controller里的路径有没有和现有接口冲突,生成器默认用类名小写做路径。

6.2 多数据源下 MP 的配置要点

多数据源时不能只靠spring.datasource,得用@DS注解或手动配SqlSessionFactory。我一般用dynamic-datasource-spring-boot3-starter,配合 MP 时注意分页插件要注册到每个数据源上。验证方法是:在 Service 方法上标@DS("slave"),然后看日志里 SQL 走的是哪个连接。如果分页在多数据源下失效,检查PaginationInnerInterceptor是不是只加到了主数据源的SqlSessionFactory上。

从那以后我每次搭新项目,都先把依赖树打出来确认没有javax残留,再跑一个分页查询看total对不对,这两步过了才往下写业务。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进