MyBatis 整合
MyBatis 把 SQL 的编写权留给开发者,只接管参数映射、SQL 执行、结果映射这三件重复劳动。Spring Boot 用一个 starter 完成 SqlSessionFactory 构建、Mapper 接口注册、SqlSession 托管的全套装配。本文沿"自动配置 → Mapper 代理 → SqlSession 与事务"这条主线拆解整合原理,再落到实践规范。自动配置的通用机制见Spring Boot Startup。
核心架构
组件与生命周期
| 组件 | 职责 | 生命周期 |
|---|---|---|
SqlSessionFactoryBuilder | 解析配置,构建工厂 | 方法局部,用完即弃 |
SqlSessionFactory | 生产 SqlSession | 应用级,单例 |
SqlSession | 执行 SQL 的会话 | 请求/事务级,非线程安全 |
| Mapper 接口 | 声明 SQL 操作 | 应用级(实际是代理对象) |
原生 MyBatis 要求手工管理 SqlSession 的创建与关闭;Spring 整合后这一层被 SqlSessionTemplate 接管,生命周期问题随之消失。
Spring Boot 整合
依赖与配置
1 | |
starter 3.0.x 对应 Spring Boot 3.2–3.5、Java 17+。
1 | |
自动配置做了什么
MybatisAutoConfiguration 的两个前提条件:
1 | |
满足后注册两个核心 Bean:
- SqlSessionFactory:由
SqlSessionFactoryBean构建。启动时解析mapper-locations指定的全部 XML,把每条 SQL 连同参数映射、结果映射编译成MappedStatement,以namespace.方法名为 key 存入 Configuration - SqlSessionTemplate:SqlSession 的线程安全门面,所有 Mapper 代理最终调用的都是它
flowchart TB
classDef boot fill:#e1f5fe,stroke:#0288d1,color:#01579b
classDef scan fill:#f3e5f5,stroke:#7b1fa2,color:#4a148c
classDef bean fill:#fff3e0,stroke:#f57c00,color:#e65100
classDef ready fill:#e8f5e9,stroke:#388e3c,color:#1b5e20
subgraph S1["自动配置"]
D["SqlSessionFactoryBean<br/>解析 mapper XML"]:::boot
E["MappedStatement<br/>SQL 与映射的元数据"]:::boot
F["SqlSessionTemplate<br/>线程安全会话门面"]:::bean
D --> E
end
subgraph S2["Mapper 扫描"]
A["@MapperScan / @Mapper"]:::scan
B["ClassPathMapperScanner<br/>扫描指定包"]:::scan
C["每个接口注册为<br/>MapperFactoryBean"]:::scan
A --> B --> C
end
G["容器就绪:注入接口<br/>即注入代理对象"]:::ready
E --> G
C --> GMapper 接口的代理机制
整合的核心问题:接口没有实现类,为什么能被注入并执行 SQL?
扫描与注册
@MapperScan 触发 MapperScannerRegistrar 注册 MapperScannerConfigurer,ClassPathMapperScanner 扫描指定包,把每个接口注册为一个 MapperFactoryBean 的 BeanDefinition,并注入 sqlSessionFactory。
@Mapper 走同一个扫描器,只是入口换成自动配置里的 AutoConfiguredMapperScannerRegistrar,默认扫描主类所在包。二者机制等价,选一即可:包结构固定时 @MapperScan 一次圈定全部接口;多模块或类路径不确定时用 @Mapper 显式标记。
运行期调用链
MapperFactoryBean.getObject() 调用 getSqlSession().getMapper(interface),MyBatis 用 JDK 动态代理生成 MapperProxy。调用接口方法的完整链路:
flowchart TB
classDef app fill:#e1f5fe,stroke:#0288d1,color:#01579b
classDef proxy fill:#f3e5f5,stroke:#7b1fa2,color:#4a148c
classDef session fill:#fff3e0,stroke:#f57c00,color:#e65100
classDef exec fill:#e8f5e9,stroke:#388e3c,color:#1b5e20
A["userService.findById(id)"]:::app
B["MapperProxy.invoke()<br/>JDK 动态代理"]:::proxy
C["MapperMethod<br/>定位 MappedStatement"]:::proxy
D["SqlSessionTemplate"]:::session
E["SqlSessionInterceptor<br/>绑定事务会话"]:::session
F["Executor<br/>预编译参数,执行 SQL"]:::exec
G["ResultSetHandler<br/>结果映射为对象"]:::exec
A --> B --> C --> D --> E --> F --> GMapperMethod 以 接口全限定名.方法名 定位 MappedStatement,把接口参数转成 SQL 参数,交给 SqlSession。注解 SQL 与 XML SQL 在 MappedStatement 这一层完全统一,差别只是元数据的来源。
SqlSession 与事务
SqlSessionTemplate 内部所有调用都经过 SqlSessionInterceptor,它决定真实会话的来源与归宿:
- 无事务:每次 Mapper 调用新建 SqlSession,执行完立即关闭。一级缓存形同虚设,同一查询调两次就查两次库
- 有
@Transactional:从TransactionSynchronizationManager取当前事务绑定的 SqlSession,事务内复用,提交或回滚后才关闭。一级缓存生效
由此得到两个实践推论:
- 依赖一级缓存去重的逻辑必须包在同一事务内,否则不生效
- 事务内的缓存可能读到过期数据(其他连接已修改未提交前读取的数据不会刷新),敏感场景将缓存作用域降为语句级:
1 | |
实战规范
实体与 Mapper
简单单表 CRUD 用注解足够;动态 SQL、多表关联、复杂结果映射交给 XML。
1 | |
1 | |
动态 SQL
1 | |
<sql> 片段集中维护列清单,避免 SELECT * 在表结构变更时连带破坏映射。
映射方式的选择
map-underscore-to-camel-case 开启后,单表查询用 resultType 直接映射实体,无需 resultMap。resultMap 留给三种场景:列名与属性名无法靠驼峰规则对齐、多表关联需要 association/collection 嵌套映射、需要 discriminator 多态映射。不要为"规范"而写 resultMap——多数 resultMap 只是驼峰规则的手工复写。
批量插入
1 | |
1 | |
foreach 拼接的是单条多值 SQL,条数控制在几百级别,超过会顶到 max_allowed_packet 与 SQL 长度限制;更大批量分批提交,或改用 JDBC batch + rewriteBatchedStatements=true。
分页
1 | |
1 | |
PageHelper 通过拦截器改写 SQL 追加 LIMIT。startPage 与目标查询必须紧邻:它把分页参数塞进 ThreadLocal,下一条 SQL 会被消费掉,中间插入任何查询都会被错误分页。
事务边界在 Service
1 | |
rollbackFor = Exception.class 是必须项:默认只回滚 RuntimeException 与 Error,受检异常会静默提交。
HikariCP 连接池
Spring Boot 默认使用 HikariCP。数据库连接的建立要经过 TCP 握手、认证、会话初始化,代价高到必须复用;连接池维护一组常驻连接,用租借语义摊薄成本。
| 参数 | 说明 | 推荐值 |
|---|---|---|
maximum-pool-size | 池上限 | CPU 核数 × 2 + 有效磁盘数,通常 10–20 |
minimum-idle | 空闲下限 | 与 maximum 相同,固定大小池 |
connection-timeout | 取连接等待超时 | 30s,按业务容忍度调整 |
max-lifetime | 连接最大存活 | 默认 30min,须小于数据库 wait_timeout |
池不是越大越好:连接过多只会加剧数据库侧的锁竞争与上下文切换,瓶颈在磁盘与网络而非应用线程数。
监控接 Actuator 后直接看 hikaricp_connections_active 等指标;代码侧走 MXBean:
1 | |
常见陷阱
#{} 与 ${}
#{} 生成预编译占位符 ?,参数走 PreparedStatement,类型安全;${} 是字符串直接拼接,存在 SQL 注入风险。${} 仅用于无法预编译的位置——动态表名、排序列名,且值必须来自白名单校验后的枚举,绝不接受用户输入。
MySQL 8 小时断连
MySQL 默认 wait_timeout=28800,空闲连接 8 小时后被服务器单方面关闭。HikariCP 默认 max-lifetime 为 30 分钟,会在服务器超时前主动淘汰连接,默认配置即已规避。只有手动调大 max-lifetime、或数据库侧把 wait_timeout 调小时,才需要保证 max-lifetime < wait_timeout。故障特征是隔夜后第一批请求抛 CommunicationsException。
连接泄漏
连接借出未还,池逐渐耗尽,最终报 Connection is not available, request timed out。开启泄漏检测,HikariCP 会打印超时连接的持有栈:
1 | |
Spring 整合下正常业务代码不会泄漏——SqlSessionInterceptor 总会归还连接;泄漏通常来自绕过框架手工开的 Statement、ResultSet 或数据源连接。
相关阅读
- Spring Boot Startup:自动配置机制的完整拆解
- Spring Boot Bean 使用规范:MapperFactoryBean 之外的 Bean 实践
- Spring Boot 常用注解:注解速查与对比


