news 2026/10/26 18:40:47

Spring Boot 3.2 与 MyBatis-Plus 3.5.5 整合避坑

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Spring Boot 3.2 与 MyBatis-Plus 3.5.5 整合避坑

简介:面向Java Web开发者的Spring Boot 3与MyBatisPlus整合示例工程,适合希望快速理解两者协同配置、精简持久层代码的初中级开发者参考,也可用于课程设计、练手项目或旧项目持久层改造前的技术验证。压缩包共24个文件,约85KB,其中7个Java源码及对应class文件便于查看实现与编译结果,yaml、properties和xml承担依赖与数据源配置,另附jar、md说明等辅助材料,覆盖依赖管理、数据源配置、Mapper接口、实体注解、Service与Controller分层等关键环节。已有1435人学习下载,说明这套整合思路具有一定参考价值。读者可从中获得结构完整的整合模板,直接观察自动配置如何把MyBatisPlus接入Spring容器,并借鉴条件构造器、基础增删改查和测试写法,减少重复模板代码,提升项目搭建与排错效率。目录按源码、测试、构建脚本组织,便于对照版本匹配、配置类与扫描路径边看边改。

1. 版本匹配:Spring Boot 3.2 与 MyBatis-Plus 3.5.5 先对齐再动手

Spring Boot 3 加 MyBatis-Plus,最容易出问题的环节是版本匹配。上周有人照着一篇 Boot 2 时代的帖子抄配置,pom 里写的是mybatis-plus-boot-starter,编译全部通过,mvn spring-boot:run一跑就抛NoClassDefFoundError,堆栈指向javax.servlet。Spring Boot 3 把 javax 整体换成了 jakarta,旧 starter 的自动配置类根本注册不上,这跟业务代码没有关系,却能让人耗掉一整个下午。所以这篇笔记不从 CRUD 写起,先按我的习惯把 JDK、Spring Boot、MyBatis-Plus 三者的版本对齐确认清楚,再依次处理依赖、配置、实体映射和验证。不管是新搭一个订单查询模块,还是把老工程从 Boot 2 升到 Boot 3,这个顺序都能照着复现。

2. 依赖落地:pom.xml 里三行坐标决定后面少踩一半坑

工程能不能启动,在写第一行 Java 代码之前基本就定了。这一章把版本表、工程骨架和 starter 依赖三件事一次说透,后面配置和实体映射才有落脚点。

2.1 三条版本线先对齐

我一般会先列一张表,把版本固定下来,再动键盘。Spring Boot 3 系列对 JDK 的下限是 17,这一点没有商量余地;MyBatis-Plus 从 3.5.3.1 开始才提供面向 Boot 3 的 starter,3.5.5 是我目前用得最稳的一版。

组件版本关键说明
JDK17Spring Boot 3 最低要求,21 也能跑
Spring Boot3.2.53.1.x 亦可,3.2.x 对 Jakarta 生态兼容更好
MyBatis-Plus3.5.5必须用 boot3 专用 starter
MySQL 驱动随 Boot 管理坐标已从mysql:mysql-connector-java换成com.mysql:mysql-connector-j

JDK 的选择上有个细节:如果团队还停留在 JDK 11,那就别碰 Spring Boot 3,先升级 JDK,再谈框架。反过来,JDK 21 跑 Boot 3.2 是没问题的,但要注意部分编译插件和 Lombok 旧版本对 21 的支持,Lombok 建议跟到 1.18.30 以上。这些版本号我不建议随手改,改一次就要重跑一遍启动验证,代价比想象中大。

2.2 工程骨架和 pom 片段

目录结构我习惯按下面这样分,实体、Mapper、Service、配置各归各位,后面排查问题时一眼就能定位到文件:

src/main/java/com/example/demo ├── DemoApplication.java # 启动类,带 @MapperScan ├── config/MybatisPlusConfig.java # 分页插件等拦截器 ├── entity/Order.java # 实体,对应数据库表 ├── mapper/OrderMapper.java # 继承 BaseMapper └── service/ ├── OrderService.java └── impl/OrderServiceImpl.java src/main/resources ├── application.yml └── mapper/OrderMapper.xml # 自定义 SQL 放这里

pom.xml 的核心部分是这样,父工程负责统一版本,业务依赖只留三个:

<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.2.5</version> <relativePath/> </parent> <properties> <java.version>17</java.version> </properties> <dependencies> <!-- Web 层,验证接口要用 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- 关键:boot3 专用 starter,不是 mybatis-plus-boot-starter --> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-spring-boot3-starter</artifactId> <version>3.5.5</version> </dependency> <!-- MySQL 8 驱动,坐标已变更 --> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency> </dependencies>

spring-boot-starter-parent的版本一改,驱动的版本会跟着变,所以驱动不用手写版本号;scope设成runtime是因为编译期用不到 JDBC 实现类,只在运行期加载。MyBatis-Plus 的版本必须写死,它不在 Spring Boot 的依赖管理范围内。

2.3 starter 名字不是笔误

mybatis-plus-spring-boot3-starter这个长名字经常被人当成写错,改回mybatis-plus-boot-starter,然后启动就报javax.servlet.Filter找不到。两个包的区别在于内部依赖的mybatis-spring版本:老包依赖 2.x,走 javax 命名空间;新包依赖 3.x,走 jakarta。想确认自己引的是哪个,跑一条命令就够:

# 只看 mybatis-spring 的传递依赖版本 mvn dependency:tree -Dincludes=org.mybatis:mybatis-spring

输出里如果是org.mybatis:mybatis-spring:3.0.x,说明引对了;如果看到 2.x,八成是某个老依赖把它带进来了。常见的情况是项目里同时引了mybatis-spring-boot-starter,两个 starter 抢着注册SqlSessionFactory,这时候要么删掉多余的那个,要么在依赖里显式 exclude,不要指望 Spring 自己挑一个。

还有一个版本细节值得提前说:MyBatis-Plus 近几个版本把 JSqlParser 从核心包里拆了出去,用到分页或复杂条件构造时需要额外引mybatis-plus-jsqlparser。3.5.5 这个版本还没拆,跟着上面配不会缺包;如果后面升到更新的版本,启动报NoClassDefFoundError: net/sf/jsqlparser/...,先想到这条。

3. 配置落地:application.yml 与 MybatisPlusConfig 的分工

依赖引对之后,下一道坎是配置。很多人把所有东西一股脑塞进 yml,结果分页插件没注册,查出来的数据量永远等于全表行数。这一章把数据源、Mapper 扫描、分页拦截器三块拆开讲。

3.1 数据源和连接池参数

我一般先用最朴素的配置把连接跑通,再谈连接池调优:

server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://127.0.0.1:3306/demo_order?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true username: demo password: demo123 hikari: maximum-pool-size: 10 minimum-idle: 2 connection-timeout: 30000

serverTimezone=Asia/Shanghai这一项不写,MySQL 8 在部分时区设置下会把LocalDateTime读成差 8 小时的值;allowPublicKeyRetrieval=true是为了兼容 MySQL 8 默认的caching_sha2_password认证方式,本地开发环境不加上它,经常会看到Public Key Retrieval is not allowed。useSSL=false只在本地和内网可信环境用,线上该开还是开。连接池参数先用 Hikari 的默认值跑通,maximum-pool-size先设 10,等压测有数据再调,凭感觉写 100 反而容易把数据库连接数打满。

3.2 mapper-locations 与 @MapperScan 的分工

这两个东西经常被混为一谈,其实管的是不同的事:mapper-locations告诉 MyBatis 去哪里找 XML,@MapperScan告诉 Spring 去哪里找 Mapper 接口。

mybatis-plus: mapper-locations: classpath*:/mapper/**/*.xml type-aliases-package: com.example.demo.entity configuration: map-underscore-to-camel-case: true cache-enabled: false log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: id-type: assign_id table-underline: true

classpath*:前面这个星号是多模块项目的关键,它会把所有 jar 里的mapper目录都扫一遍,只写classpath:在单模块下没问题,一旦拆成多模块,子模块里的 XML 就会扫不到。map-underscore-to-camel-case打开后,order_no自动映射到orderNo,实体里就不用到处写@TableField了。log-impl设成StdOutImpl是为了在控制台直接看到执行的 SQL 和参数,上线前一定记得关掉,否则日志量能翻好几倍。

接口扫描放在启动类上:

@SpringBootApplication @MapperScan("com.example.demo.mapper") public class DemoApplication { public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); } }

如果 Mapper 接口上已经逐个标了@Mapper,@MapperScan可以省,但两三百个接口一个个标不现实,我一般统一用@MapperScan,接口上就不重复写了。

3.3 分页插件必须手动注册

这一条是新手最容易漏的。MyBatis-Plus 的分页不是开箱即用的,不注册拦截器,selectPage不会报错,只是默默把所有数据都查回来,然后告诉你 total 等于 list 的长度:

@Configuration public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); // 分页插件,DbType 一定要和实际数据库对上 PaginationInnerInterceptor page = new PaginationInnerInterceptor(); page.setDbType(DbType.MYSQL); // 单页最多 500 条,防止前端传个 99999 把库拖死 page.setMaxLimit(500L); // 页码超出总页数时返回空列表,而不是回到第一页 page.setOverflow(false); interceptor.addInnerInterceptor(page); return interceptor; } }

setDbType决定拼分页 SQL 时用哪种方言,MySQL 是LIMIT,Oracle 是ROWNUM,配错了拿到的结果虽然不报错,但行数不对。setMaxLimit是我强加的一道防护,线上出过前端传size=100000把接口拖到超时的血泪事故,加上限之后这类请求直接被截断。

4. 代码落地:实体、Mapper、Service 三层最小可跑通结构

配置就位,接下来是把表映射到代码。这一章的三个小节对应三层结构,每一层都给出能直接跑的最小写法,再说明哪些地方容易写歪。

4.1 实体类与字段映射

先看一个订单表对应的实体:

@Data @TableName("t_order") public class Order { // 雪花算法生成 19 位 Long 主键 @TableId(value = "id", type = IdType.ASSIGN_ID) private Long id; // 数据库列 order_no,开启驼峰映射后可省略 @TableField private String orderNo; private Integer status; @TableField(fill = FieldFill.INSERT) private LocalDateTime createTime; }

@TableName只在表名和类名对不上时才需要写,比如类叫Order、表叫t_order,不加就会去查order表,然后收到一条Table 'demo_order.order' doesn't exist。IdType有四种常用取值:AUTO交给数据库自增,ASSIGN_ID用雪花算法,INPUT由调用方自己塞,NONE跟随全局配置。雪花算法生成的是 19 位 Long,直接返回给前端 JavaScript 会丢精度,这一点后面排错章节还会再说。@TableField(fill = ...)要求项目里有一个MetaObjectHandler实现类,没有实现类的话这个注解不会有任何效果。

4.2 BaseMapper 能省掉哪些 SQL

Mapper 接口继承BaseMapper之后,单表增删改查一行 SQL 都不用写:

@Mapper public interface OrderMapper extends BaseMapper<Order> { // 自定义查询,对应 XML 里的 selectByStatus List<Order> selectByStatus(@Param("status") Integer status); }

带条件的查询用LambdaQueryWrapper,字段名走方法引用,改字段名时编译器会报错,比字符串拼写安全:

LambdaQueryWrapper<Order> wrapper = Wrappers.lambdaQuery(); wrapper.eq(Order::getStatus, 1) .like(StringUtils.hasText(keyword), Order::getOrderNo, keyword) .orderByDesc(Order::getCreateTime); List<Order> list = orderMapper.selectList(wrapper);

.like(condition, ...)这种带条件的重载很实用,第一个参数是布尔值,条件不成立时这一段直接不拼进 SQL,省掉一堆if判断。需要注意LambdaQueryWrapper拼的是列名而不是属性名,类上加的@TableField映射它能识别,但如果你在 XML 里手写 SQL,就得自己维护映射关系。

4.3 IService 与批量操作

Service 层继承ServiceImpl之后,常用的save、updateById、page都能直接调:

public interface OrderService extends IService<Order> { } @Service public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements OrderService { }

批量插入用saveBatch,但有两个细节必须注意:一是它内部靠 JDBC 批处理实现,批大小默认 1000,数据量大时建议显式传 500;二是saveBatch不开事务的话,中途失败会留下一半数据,方法上要加@Transactional:

@Transactional(rollbackFor = Exception.class) public void batchImport(List<Order> orders) { // 第二个参数是批大小,500 是我在多数业务里试出来的折中值 this.saveBatch(orders, 500); }

updateById有个反直觉的行为:它只更新非 null 字段,想把某个字段改成 null 用它是改不掉的,得换LambdaUpdateWrapper加set方法。这个差异我在迁移老项目时被坑过一次,明明传了 null,数据库里还是旧值,查了半天以为是缓存。

5. 避坑记录:启动报错、分页失效、主键错乱的 5 条排查

下面这五条是我自己或身边人真实踩过的,按「现象 → 原因 → 解决」记下来,遇到类似的直接对号入座。

5.1 Invalid bound statement (not found)

现象:启动正常,一调 Mapper 方法就抛BindingException: Invalid bound statement (not found): com.example.demo.mapper.OrderMapper.selectByStatus。

原因排在第一位的是 XML 没被打包。Maven 默认只把src/main/resources下的文件复制到target/classes,放在src/main/java目录里的 XML 会被直接忽略。其次是 namespace 写错、方法名和 XML 里的id对不上、mapper-locations路径写错,这几种报错信息一模一样,得逐个排除。

解决:先去target/classes/mapper下看一眼 XML 在不在,不在就在 pom 里补资源目录配置:

<build> <resources> <resource> <directory>src/main/java</directory> <includes> <include>**/*.xml</include> </includes> </resource> <resource> <directory>src/main/resources</directory> </resource> </resources> </build>

在的话再核对namespace是不是全限定接口名、id是不是方法名。

5.2 分页查出了全表数据

现象:调selectPage,传了current=1, size=10,返回的 list 有一千多条,total 也是一千多。

原因:MybatisPlusInterceptor没注册,或者注册了但没addInnerInterceptor。这种情况下 MyBatis-Plus 退化成普通查询,把整个结果集返回给你,也不报错,特别隐蔽。

解决:确认配置类被@Configuration标注、在启动类所在包或子包下、并且interceptor.addInnerInterceptor(page)这一行真的执行到了。打个断点在mybatisPlusInterceptor()方法里,启动时没停下来就说明这个 Bean 压根没被加载。

5.3 表名或列名对不上

现象:SQLSyntaxErrorException: Table 'demo_order.order' doesn't exist,或者Unknown column 'orderNo' in 'field list'。

原因:前者是@TableName没写或者写错,后者是驼峰映射没开,MyBatis 拿属性名当列名拼进了 SQL。

解决:表名对不上就补@TableName("t_order");列名对不上先检查map-underscore-to-camel-case: true有没有配,配了还不行就看实体字段上的@TableField是不是写反了。我一般会在开发阶段把log-impl打开,直接看控制台打印的 SQL,比猜快得多。

5.4 主键是 0 或者前端拿到的是错误 id

现象:插入成功,但返回对象的 id 是 0;或者后端日志里 id 是1783920154875633665,前端收到的是1783920154875633700。

原因:前者是IdType配成了AUTO但数据库表的主键没设自增,或者配成INPUT却没赋值;后者是 JavaScript 的 Number 精度只有 53 位,19 位 Long 超出范围后被四舍五入。

解决:主键生成策略统一在global-config.db-config.id-type里配assign_id,别每个实体单独写;返回给前端的 Long 主键统一转成 String,可以全局配 Jackson 序列化,也可以在 VO 里手动转。这个问题不改,前端永远查不到刚插入的那条数据。

5.5 启动报 jakarta 相关的类找不到

现象:java.lang.ClassNotFoundException: jakarta.servlet.Filter或者NoSuchMethodError,堆栈里能看到mybatis-spring的影子。

原因:工程里混进了面向 Boot 2 的老 starter,或者某个中间件依赖把mybatis-spring2.x 带了进来。

解决:mvn dependency:tree -Dincludes=org.mybatis:mybatis-spring看清楚版本,再用exclusion把老的排掉:

<dependency> <groupId>com.xxx</groupId> <artifactId>某个老中间件</artifactId> <exclusions> <exclusion> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> </exclusion> </exclusions> </dependency>

排完记得重启验证,光看依赖树通过不代表运行期没问题。

6. 验证跑通:一个查询接口把整条链路串起来

装完不验证,等于没装。我习惯写一个最小的 Controller,从 HTTP 入口一路打到数据库,再打回来,链路哪一段断了立刻就能看出来。

@RestController @RequestMapping("/order") public class OrderController { @Resource private OrderService orderService; // 分页查询,验证插件是否生效 @GetMapping("/page") public Object page(@RequestParam(defaultValue = "1") long current, @RequestParam(defaultValue = "10") long size) { Page<Order> page = new Page<>(current, size); return orderService.page(page, Wrappers.<Order>lambdaQuery() .orderByDesc(Order::getCreateTime)); } // 单条插入,验证主键生成策略 @PostMapping("/add") public Object add(@RequestBody Order order) { orderService.save(order); return order.getId(); } }

启动之后先看控制台,出现Started DemoApplication in x.xxx seconds说明上下文加载完成;如果在这一步就挂了,回到第 5 章逐条对。然后跑两条命令:

# 先插一条,看返回的 id 是不是 19 位 curl -X POST http://127.0.0.1:8080/order/add \ -H "Content-Type: application/json" \ -d '{"orderNo":"T20240101001","status":1}' # 再查分页,返回的 records 长度应该等于 size curl "http://127.0.0.1:8080/order/page?current=1&size=10"

判断标准有三条:插入返回的 id 不是 0 也不是 null;分页接口返回的records数组长度不超过 10,total是真实总数而不是数组长度;控制台里能看到Preparing: SELECT ... LIMIT ?这样的 SQL,说明分页插件真的把LIMIT拼进去了。三条全过,这套整合就算通了。

再补一个我常做的自检:把size改成 1000 再调一次,看结果是不是被maxLimit截到 500。能截住,说明拦截器配置确实生效,不是碰巧查对了。

从那以后我每次配 MyBatis-Plus,都会先把「插一条、分一页、看 SQL」这三步走完,再去写业务代码。省下来的调试时间,比多写几行配置划算得多。希望帮到你。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/26 18:39:36

基于LabVIEW的汽车扭矩主动分配测控系统与1 kHz实时台架标定

简介&#xff1a;围绕LabVIEW在汽车扭矩主动分配测控中的应用&#xff0c;这份全国虚拟仪器大赛作品面向虚拟仪器、车辆电子与控制算法学习者&#xff0c;提供一套可参考的赛题级实现方案&#xff0c;用于理解驱动轮扭矩实时分配、信号采集与闭环控制的工程落地。压缩包共5个文…

作者头像 李华
网站建设 2026/10/26 18:39:36

JetBrains AI编程代理插件化功能解析与优化

1. JetBrains IDE AI编程代理插件化功能深度解析 JetBrains全家桶用户最近迎来一个重要更新——AI编程代理&#xff08;ACP&#xff09;终于能以标准插件形式安装使用了。这个改变看似只是安装形式的调整&#xff0c;实则彻底重构了开发者与AI编程助手的交互方式。作为长期使用…

作者头像 李华
网站建设 2026/10/26 18:39:30

DLMS/COSEM通信协议库实战:OBIS对象、HDLC链路与A-XDR解析

简介&#xff1a;DLMS协议库以C语言源码形式提供一套可直接研读的DLMS/COSEM通信协议实现&#xff0c;面向电表、水表、能源计量终端及采集设备的嵌入式开发、协议移植、一致性测试与维护人员&#xff0c;帮助读者理解蓝皮书、绿皮书、黄皮书所定义的通信模型、对象接口和访问流…

作者头像 李华
网站建设 2026/10/26 18:39:01

智能证件阅读器核心技术解析与机场应用实践

1. 证件阅读器在机场的核心价值解析现代机场每天需要处理数万旅客的证件核验工作。传统人工查验方式存在效率低下、易出错等问题&#xff0c;而智能证件阅读器的出现彻底改变了这一局面。这类设备通过光学字符识别&#xff08;OCR&#xff09;、射频识别&#xff08;RFID&#…

作者头像 李华
网站建设 2026/10/26 18:36:47

Qt自绘电池进度条:从QWidget到paintEvent的完整实现

简介&#xff1a;这份资源面向Qt初学者与界面开发爱好者&#xff0c;围绕paintEvent事件处理机制&#xff0c;提供一个可运行的自定义电池进度条控件实例&#xff0c;用于解决图形界面中电量状态可视化与动态反馈的问题。压缩包共8个文件&#xff0c;约7KB&#xff0c;以3个cpp…

作者头像 李华
网站建设 2026/10/26 18:36:01

餐饮数据智能分析平台:Django+Hadoop+Spark+LSTM实战

1. 项目概述&#xff1a;餐饮数据智能分析平台的设计初衷 这个毕业设计项目的核心目标&#xff0c;是构建一个能够处理海量餐饮数据的智能分析系统。我在实际开发中发现&#xff0c;传统餐饮行业的数据分析存在三个明显痛点&#xff1a;一是评价数据多为非结构化文本&#xff0…

作者头像 李华