资讯动态

Spring Boot + MyBatis 整合实践:从自动装配到事务与连接池调优

发布时间:2026/10/13 10:29:02 来源:尧图企业网站定制
简介这套Spring Boot与MyBatis的整合模板面向需要快速搭建后端数据接口的Java工程师也适合作为项目开发的基础骨架。项目利用Spring Boot的自动配置简化依赖管理与启动流程将MyBatis作为持久层框架通过Mapper接口和XML映射文件处理数据库读写同时集成Druid数据源连接池借助其监控能力实时查看SQL执行效率并引入Swagger生成可在线调试的API文档减少前后端联调时的沟通成本。压缩包共159个文件主要涵盖Java源码、编译后的class文件、Java后端配置、前端页面脚本与样式资源整体仅1.81MB目录按业务功能划分导入开发环境后即可运行学习。已有159人浏览学习包内包含用户管理、文件上传、Redis缓存配置、Swagger集成等示例能够帮助初中级开发者在实际代码中理解框架组合方式也可作为微服务或Web应用开发时的精简起始模板。1. 从“能跑就行”到“敢改敢拆”Spring Boot MyBatis 到底该怎么搭我见过太多新项目的第一步就是把 spring-boot-starter-web 和 mybatis-spring-boot-starter 往 pom 里一塞然后照着网上的片段写完 Controller 调 Service、Service 调 Mapper跑通一个 select 列表就宣布脚手架完成。这不算错但你只看到了 Spring Boot MyBatis 联合使用时的拼装表面没看到它在生产环境里真正容易被击穿的地方事务边界穿透、连接池耗尽、二级缓存失效、多数据源下 SqlSession 串了上下文。这恰恰是这套组合在实战里价值最高也最容易踩坑的部分。Spring Boot 解决的是「配置和装配的自动化」MyBatis 解决的是「SQL 与 Java 方法的映射自由」。二者结合的好处不只是少写一大坨 DAO 实现类而是让你在拿到慢 SQL 或复杂报表需求时可以精准地在 XML 里调优语句而不需要为了一个 join 去动 Java 代码。但反过来如果你想当然地以为 Spring Boot 会自动帮你托管一切那事务注解失效、Mapper 扫描不到、下划线字段映射成 null 这类问题就会接踵而至。这篇文章我会从零到落地把整条链路拆开讲先厘清 MyBatis 在 Spring Boot 下的运行原理和依赖选型然后给你一份可以直接抄的工程骨架和参数清单再进入业务开发完整案例、性能优化与安全加固、高频坑排查最后收在几个能大幅降低线上事故率的习惯上。全文按一套我能直接复制到空项目里跑通的标准来写命令、配置、踩坑记录都是实际可验证的。这套技术方向确实值得投入但它不是搭完脚手架就结束的东西真正的投入发生在你开始调连接池、控事务、盯慢 SQL 之后。2. 核心组件与依赖选型为什么你的 MyBatis 在 Spring Boot 下「半自动」很多人把「Spring Boot 整合 MyBatis」理解为「加个依赖就能用」但 MyBatis 在 Spring Boot 环境里的运行机制其实是三块拼图SqlSessionFactory 负责解析配置与 XML 映射文件SqlSessionTemplate 负责线程安全的会话管理MapperFactoryBean 负责把 Mapper 接口注入到 Spring 容器。如果你对这三块没有概念遇到「Mapper 接口明明写了 Mapper 却注入失败」「XML 修改了不生效」「多数据源时 SQL 执行到错误的库」这类问题就只能靠玄学重启。2.1 Spring Boot 下 MyBatis 的自动装配原理从依赖到 SqlSessionTemplate我一般会在 pom 里这样引依赖这是当前稳定且主流的选择dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version2.3.1/version /dependency如果你使用的是 Spring Boot 3.x那注意 2.3.x 系列不再适配需要改用 3.0.x 版本的 mybatis-spring-boot-starter。这个版本差异看似无关紧要实际会让你的应用在启动时报错或 Mapper 无法注入。Spring Boot 的自动配置会在启动时读取SqlSessionFactoryBean的配置项包括 mapper-locationsXML 路径、type-aliases-package别名包、configuration全局配置然后创建全局唯一的 DefaultSqlSessionFactory。这里的关键认知是这个自动装配机制是「约定大于配置」的产物但 SQL 映射对类名、字段名映射是敏感的。如果数据库字段是 order_no 而实体属性是 orderNo不开启 mapUnderscoreToCamelCase 就会返回 null如果 XML 目录不存在或路径写错启动不报错但运行时报「Invalid bound statement」。所以第一步依赖无所谓真正的坑在配置层。2.2 starter 版本选型与 Spring Boot 2.x/3.x 的适配边界选版本这件事我习惯用「非保险最新选发布超过一年且社区反馈持续活跃的」原则。以 mybatis-spring-boot-starter 为例2.2.x 适配 Spring Boot 2.5.x2.3.x 适配 2.7.x 内常用3.0.x 才适配 Boot 3 与 mybatis 3.5.x。不要用官网展示的 latest 直接干到生产。properties mybatis.starter.version2.3.1/mybatis.starter.version /properties参数说明如果你所在的项目组是 Boot 2.7 MyBatis starter 2.1.x那升级时就要检查DefaultDatabaseIdProvider是否配置了 databaseId避免多数据库兼容性 SQL 判断失效。这里还有个很容易忽略的地方starter 会默认引入 mybatis 与 mybatis-spring 的底层依赖如果你还在手动引 mybatis-spring 的老版本就可能冲突。检查依赖树用mvn dependency:tree -Dincludesorg.mybatis最直观。2.3 为什么说 MyBatis 是半自动 ORM对比 JPA/Hibernate 的选型分水岭在决定要不要用 MyBatis 之前必须先想清楚你依赖它解决什么问题。我的判断标准是如果你的 SQL 中动态条件占比超过三成、或者你希望 SQL 执行路径完全可控选 MyBatis如果业务模型简单且希望尽量减少 SQL 编写量选 JPA 更高效。MyBatis 给开发者保留的是 SQL 的控制权但代价是「字段映射、分页、复杂关联查询」都必须手动处理。比如要做一个订单列表带多表 join还要按多条件 if 判断MyBatis 的 XML 里写起来非常顺手但数据落到 Java 对象时你必须注意每张表的别名和映射字段一一对应。JPA 在这个场景下反而会因为懒加载和 N1 问题把性能拖垮。项目需要的是执行效率还是开发效率这个分水岭直接决定你的架构信心和后续排查成本。3. 工程搭建与核心配置最小可运行骨架到连接池与事务边界3.1 最小 Spring Boot MyBatis 工程一个能连上库返回用户列表的骨架先看最小文件结构这是我能给出的最简单骨架src/main/java/com/demo/ DemoApplication.java controller/UserController.java service/UserService.java mapper/UserMapper.java entity/User.java src/main/resources/ application.yml mapper/UserMapper.xml首先主类上加上 MapperScan 或在每个 Mapper 接口上标 Mapper。我习惯用 MapperScan 集中在启动类配置SpringBootApplication MapperScan(com.demo.mapper) public class DemoApplication { public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); } }关键解释MapperScan 会把指定包下所有接口动态代理注册为 Spring Bean这样你在 Service 里直接 Autowired 注入 UserMapper 才不报错。分页插件也要在配置里声明这个后面讲。3.2 application.yml 配置逐项说明从数据源到驼峰映射server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/demo?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue username: root password: 123456 hikari: minimum-idle: 5 maximum-pool-size: 20 connection-timeout: 30000 mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.demo.entity configuration: map-underscore-to-camel-case: true参数说明mapper-locations指向 XML 所在目录启动时若路径不存在会报错这个你务必先确认。type-aliases-package是为了在 XML 的 resultType 里直接写 User 而不是全限定名。map-underscore-to-camel-case必须置为 true否则所有下划线字段都要手动映射 resultMap。HikariCP 连接池是 Spring Boot 默认的我只是把最大连接数和最小空闲数调到了适合中小业务的数值。如果你开发期不改这些生产一压测就会出现 wait 获取连接超时。踩坑记录这里最典型的错误是密码里带特殊字符但没转义导致数据源初始化失败。用 url 连接数据库时若有特殊符号优先用spring.datasource.password原样配置别把密码拼到 url 里。3.3 Mapper 接口与 XML 映射Param 传参和 resultMap 的边界Mapper 接口与 XML 的对应关系是这套组合的魂。一个接口方法对应 XML 里一条 statementid 必须完全一致public interface UserMapper { ListUser selectByCondition(Param(status) Integer status, Param(name) String name); }XML 对应写法select idselectByCondition resultTypeUser SELECT id, name, status, order_no AS orderNo FROM user where if teststatus ! null AND status #{status} /if if testname ! null and name ! AND name LIKE CONCAT(%, #{name}, %) /if /where /select逻辑说明 标签自动处理首条条件前的 AND避免手写 where 11 这种脏方式。所有参数都带 #{} 走 PreparedStatement防止 SQL 注入。order_no 在数据库是下划线即使开了驼峰映射select 里也可以显式写别名保证万无一失。如果你不想写 XML 太多可以用注解 Select但复杂 SQL 一旦需要条件判断注解就是灾难。我的原则是超过三行 SQL 全部进 XML保持 Java 层干净。4. 从单表到复杂业务完整 CRUD 与动态 SQL 的实战案例4.1 单表分页查询用 PageHelper 还是手写 limit分页是最常见的需求我见过很多新手直接在手写 SQL 后面拼 limit然后一变页码就出 bug。PageHelper 是 MyBatis 生态里最常见的选择依赖如下dependency groupIdcom.github.pagehelper/groupId artifactIdpagehelper-spring-boot-starter/artifactId version1.4.7/version /dependency如果你用的是 Spring Boot 3.x那就改为 2.1.0 版本另外 PageHelper 的配置需要保持与 mybatis starter 版本兼容。我一般在 application.yml 里加pagehelper: helper-dialect: mysql reasonable: true support-methods-arguments: truereasonable设为 true 后页码超过总页数时自动回退到最后一页这能避免前端拿到空页时核心业务报异常。然后在 Service 层使用方式如下PageHelper.startPage(pageNum, pageSize); ListUser list userMapper.selectAll(); PageInfoUser pageInfo new PageInfo(list);这句 startPage 必须紧跟 Mapper 查询中间绝对不能有任何其他查询语句。如果你在 startPage 和 Mapper 执行之间插入了另一个 SQL 查询PageHelper 拦截的会是那个闲杂查询分页就乱了。这是 PageHelper 使用中最知名的一个坑逻辑上要当作红线来避。4.2 动态 SQL 的实战if、choose、foreach 与批量插入现实里的列表查询条件组合千奇百怪。我用动态 SQL 处理按角色和按时间范围筛选的实现方式如下select idselectFiltered resultTypeUser SELECT * FROM user where if testroleId ! null AND role_id #{roleId} /if choose when teststartTime ! null and endTime ! null AND create_time BETWEEN #{startTime} AND #{endTime} /when when teststartTime ! null AND create_time gt; #{startTime} /when otherwise AND create_time IS NOT NULL /otherwise /choose /where /selectchoose 相当于 Java 里的 switch只有第一个匹配的 when 生效。当所有条件都不满足时走 otherwise保证 SQL 语法完整。再说批量插入。新手最常用的办法是循环调用单条 insert这在数据量为几百时看不出问题几万条时连接池直接堆满。正确写法insert idbatchInsert INSERT INTO user (name, status, role_id, create_time) VALUES foreach collectionlist itemitem separator, (#{item.name}, #{item.status}, #{item.roleId}, NOW()) /foreach /insert这里的 foreach 明确要求参数是ListUser且方法上要加Param(list)或直接用 List 入参时 XML 写 collectionlist。另一个需要注意的边界是 MySQL 参数上限默认 65535批量插入一次别超过 2000 条否则会报参数过多。4.3 事务管理Transactional 失效的三个边界问题MyBatis 与 Spring 事务的配合大多数人就是简单地在 Service 方法上标 Transactional。但失效场景远比想象多第一同类内部调用。A 方法调本类的B 方法B 上有 Transactional此时事务不生效因为 A 调用的是 this.b() 不是代理对象。解决方式是把 B 方法放到另一个 Service 里或者使用 AopContext.currentProxy()。第二异常被吞掉。catch 了 RuntimeException 然后自己吞掉事务自然不会回滚。我见过最多的是catch (Exception e) { throw new RuntimeException(回滚, e); }这种写法才对或者把异常类型写在 rollbackFor 里。第三非 public 方法。Transactional 只能作用在 public 方法上protected 是无效的。这是 Spring 代理机制决定的不是 MyBatis 问题。Transactional(rollbackFor Exception.class) public void batchTransfer(ListInteger ids) { for(Integer id : ids) { userMapper.updateStatusById(id, 1); } }这个注解的意思是方法中只要抛出 Exception 及其子类整个事务全部回滚。如果不写 rollbackFor默认只回滚 RuntimeException而业务里常见的 IOException 或自定义 checked 异常会让数据落一半形成脏账。这道题的坑在线上踩过的人很多代价往往是重跑对账脚本。5. 性能调优与安全加固从连接池参数到防注入规范5.1 连接池参数与慢 SQL 日志让性能问题提前暴露连接池的参数不能照抄网上的统一模板它应该与你的并发量和单条连接耗时匹配。一般 HikariCP 的基础参数我会这样设spring: datasource: hikari: minimum-idle: 5 maximum-pool-size: 20 idle-timeout: 600000 max-lifetime: 1800000 connection-test-query: SELECT 1maximum-pool-size 不要设太大MySQL 默认连接数为 151你一个应用占掉 20再起三个实例就接近天花板。max-lifetime 建议比数据库 wait_timeout 稍短一些否则 MySQL 已经断开的连接被池子继续复用会出现偶发的 Communications link failure。慢 SQL 日志也要开起来。在 application.yml 里配置mybatis: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl上线前我会把开发期的 stdout 轮流使用避免敏感的 SQL 参数全量输出到生产日志。生产环境更规范的做法可以用官方带参数占位符的 logback 过滤器但最简单的第一步是「先看得到 SQL」再优化具体语句。5.2 二级缓存是提速神器还是脏数据温床MyBatis 的一级缓存是 SqlSession 级别的在 Spring 管理下基本等于每次请求都有新会话意义不大。二级缓存是 namespace 级别的可以跨 SqlSession 共享数据。我的建议是如果你的项目里存在「不同 Mapper 操作同一张表」的情况二级缓存默认不开。开了二级缓存后你会发现一个典型事故A 的 Mapper 更新了 user 表B 的 Mapper 查询 user 表时读到的还是旧数据因为二级缓存按命名空间隔离无法感知其他 Mapper 的写操作。特别是多表 join 的查询结果你缓存了但 join 的其中一张表被更新你无法自动失效之前的缓存脏数据问题很难排查。如果你只是对「字典表、配置表」做只读缓存那也可以开但要设合理的刷新间隔与淘汰策略。从控制成本和风险角度讲我一般用 Caffeine 做上层业务缓存MyBatis 二级缓存当成加速读多写少基础数据的手段即可。分页插件的使用对缓存影响也需要注意PageHelper 拦截了 SQL 后会在物理查询前自动处理 COUNT 查询二级缓存会把 count 结果固定的分页数据也缓存起来一旦底层数据变动频繁就会拿到过期页数据。5.3 SQL 注入与查询超时控制让 MyBatis 在安全边界内运行MyBatis 的 #{} 占位符天然防注入但总有人图省事在 XML 里写${}。这里的边界要讲清楚${} 是字符串拼接只适合表名、排序字段这种不能占位符化的场景。让用户传排序字段时必须做白名单校验choose when testsortColumn create_time ORDER BY create_time /when when testsortColumn id ORDER BY id /when otherwise ORDER BY id /otherwise /choose另外对耗时操作加上数据库查询超时避免一条慢 SQL 把连接池拖垮。我在 XML 里常用的方式是在 select 标签上加 timeout 属性配合 JVM 层的熔断逻辑让单条查询超过 2 秒就及时失败并告警而不是干等服务端线程把连接池耗尽。查询注入带来的另一个问题是死锁或长事务特别是订单批量处理这类场景连接池被一个超长事务占住后续请求全部堆积。我会在事务方法中提前校验数据量超过阈值就分批提交从源头上防止连接耗尽引发的级联故障。6. 常见坑与排查连接池耗尽、字段映射失败、事务不回滚的 5 个高频案例6.1 现象应用启动正常但 Mapper 方法执行时报 Invalid bound statement (not found)原因XML 没有被加载或 Mapper 接口没有绑定到 XML 的 id。解决检查三点。mapper-locations 里的 classpath 路径是否正确我经常是少写了mapper/*.xml里的斜杠导致匹配失败。XML 里的 namespace 是否与接口全限定名一致。接口方法名与 XML 的 id 是否完全一致包括大小写。这个坑属于排查十分钟、修复半分钟的类型但生产里出过一次核对起来也很费时间。6.2 现象查询出来字段全是 null数据库里明明有值原因数据库字段是下划线风格实体类是驼峰但 map-underscore-to-camel-case 没有开启或者你在 XML 的 resultMap 里手动映射时用了错误的 column 名。解决先全局开启驼峰映射如果个别自定义 SQL 里出现了别名就把别名改成驼峰或显式加 resultMap。经验写多表 join 查询时不要偷懒让 MyBatis 自动映射外键字段和同名列会互相覆盖导致数据错乱。显式定义 resultMap 时将同名不同表的字段用别名区分这是高复用性能老项目里保平安的基本操作。6.3 现象一个 Transactional 方法里更新了数据库异常后部分数据已提交原因事务方法内部 catch 了异常没有抛出或同类调用导致代理失效。解决异常必须抛出来才能触发 rollback捕获之后要 rethrow。同类调用时把事务方法移到另一个 Service Bean 中。检查 rollbackFor 是否包含你预期回滚的异常类型。建议在 Service 入口方法打印日志包含事务状态与执行耗时这样一旦数据对不上账你能从日志时间线第一时间圈出异常节点而不是重新跑全链路排查。如果要给写入操作人工加一层保护我会统一用TransactionTemplate而非注解它能让事务边界在代码里显式可见团队写规范代码时踩坑更少。6.4 现象压测时出现 Connection is not available, request timed out原因连接池最大连接数设置过小或者存在慢 SQL。解决调 HikariCP 的 maximum-pool-size 至 20 到 50 之间配合 minimum-idle。开慢 SQL 日志定位查询耗时排行。排查是否有事务方法里做了远程 RPC 调用导致连接持有时间过长。这是数据库连接池最常见事故调参只是治标治本是保证 SQL 单查时间低于阈值和在非事务里别持有连接做远程请求。6.5 现象MyBatis 的二级缓存配置了但总不生效原因未开启全局缓存开关或缓存作用域配置错误。解决在 mybatis 配置文件里设置settingssetting namecacheEnabled valuetrue//settings。在 XML mapper 里加cache evictionLRU flushInterval60000 size512 readOnlytrue/。确保缓存对应的实体类可序列化。如果项目里多表关联查询频繁我建议不依赖这个。只有只读字典场景才会开它节省数据库压力。这块我在之前带的项目里吃过一次脏数据的亏后续再排查缓存问题都会先关掉它再做系统性定位。7. 从能跑到能用的最后一公里固化下来的实操习惯与验证方法前面章节把架构、配置、实战和坑都拆开了但真正从能跑变成能用、可维护、可上生产还差最后一段距离。我个人的固化套路是这样每写一个 XML mapper先确认 select 对应的 resultType 与数据库物理字段之间有可靠映射。所谓可靠不是靠驼峰转换兜底而是强约束的关键字段显式列在 resultMap 中。每加一个事务方法只把真正需要保持一致的写操作放进去不要在事务里调用远程接口或做毫秒级无意义 sleep。每次压测前优先监控连接池活跃数与连接等待时间这两项能提前暴露连接泄漏。每引入一个新注解或新技术先做最小 Demo 验证与当前 starter 版本兼容性再上工程改造。每优化一条慢 SQL先在数据库客户端跑执行计划再回 XML 调整不要凭感觉加索引或改 SQL 结构。我还会在本地启动时加一条启动参数来校验 Mapper 绑定mvn spring-boot:run -Dspring-boot.run.arguments--mybatis.mapper-locationsclasspath:mapper/*.xml这样子在开发阶段就能立即发现 XML 路径写没写对避免把启动异常拖到集成测试阶段。生产上线前我会再做一个习惯性动作全局搜索所有${}的使用点逐一确认没有用户可控数据直接落入字符串拼接中。搜索起来很方便在 IDEA 中全局搜 XML 文件里${基本一条条过目就知道边界在哪儿。这些习惯多花不了多少时间但能帮你把「看起来在跑」提升到「敢接手、敢重构」的状态。我早期在这套技术栈上付出最多的不是学语法而是吃完一个又一个隐性问题之后形成的条件反射改配置必查版本写 SQL 必测执行计划加缓存必考虑失效链路。希望这些内容能帮你减少一些摸索成本把精力放到真正有价值的业务打磨上。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑