1. MyBatis核心架构与工作原理
MyBatis作为Java生态中最受欢迎的ORM框架之一,其设计哲学与Hibernate等全自动框架有着本质区别。我在实际项目中使用MyBatis已有五年时间,发现很多开发者虽然能写Mapper XML,但对框架的底层机制理解不足。让我们从架构层面拆解MyBatis的核心组件:
1.1 基础组件交互流程
MyBatis的运行时架构主要由以下几个核心组件构成:
- SqlSessionFactory:通过XML或Java配置构建的工厂对象,线程安全
- SqlSession:代表一次数据库会话,非线程安全
- Executor:SQL语句执行器,处理缓存和事务
- MappedStatement:封装了SQL映射信息
- TypeHandler:处理Java类型与JDBC类型转换
这些组件的协作流程是这样的:当调用SqlSession.selectOne()时,框架会通过MappedStatement获取对应的SQL定义,经由Executor执行查询,期间可能涉及一级缓存检查,最后通过TypeHandler完成结果集映射。
关键点:每个SqlSession都持有一个独立的Executor实例,这解释了为什么一级缓存无法跨会话共享
1.2 配置文件加载机制
MyBatis的配置体系采用分层设计:
xml复制<!-- 典型配置结构 -->
<configuration>
<properties resource="db.properties"/>
<settings>
<setting name="cacheEnabled" value="true"/>
</settings>
<typeAliases>
<typeAlias alias="User" type="com.example.User"/>
</typeAliases>
<mappers>
<mapper resource="com/example/UserMapper.xml"/>
</mappers>
</configuration>
配置加载过程有几个容易踩坑的地方:
- properties文件的加载顺序会影响变量替换
- typeAliases必须在mappers之前声明
- 同一个namespace下的mapper文件如果重复定义会报错
我在实际项目中遇到过因配置文件顺序错误导致的别名失效问题,排查了半天才发现是typeAliases声明位置不对。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 动态SQL实战技巧
2.1 条件分支处理
MyBatis提供了强大的动态SQL能力,但很多开发者只停留在基础使用层面。以最常见的条件查询为例:
xml复制<select id="findUsers" resultType="User">
SELECT * FROM users
<where>
<if test="name != null">
AND name like #{name}
</if>
<if test="age != null">
AND age = #{age}
</if>
<choose>
<when test="orderBy == 'name'">
ORDER BY name
</when>
<otherwise>
ORDER BY id
</otherwise>
</choose>
</where>
</select>
这里有几个实用技巧:
<where>标签会自动处理AND/OR前缀问题- 参数判断使用
!= null而非!= ''更安全 <choose>比多个<if>更清晰表达互斥条件
2.2 批量操作优化
处理批量插入时,这种写法性能极差:
java复制for(User user : userList) {
sqlSession.insert("insertUser", user);
}
应该改用批量Executor:
java复制SqlSession batchSession = sqlSessionFactory.openSession(ExecutorType.BATCH);
try {
for(User user : userList) {
batchSession.insert("insertUser", user);
}
batchSession.commit();
} finally {
batchSession.close();
}
实测10万条数据插入,批量模式比单条模式快20倍以上。但要注意:
- 批量操作的事务要么全成功要么全失败
- 某些驱动需要配置rewriteBatchedStatements参数
- 内存消耗会随批量增大而增加
3. 缓存机制深度解析
3.1 一级缓存陷阱
MyBatis的一级缓存(本地缓存)默认开启,但有几个反直觉的特性:
- 同一个SqlSession内重复查询会命中缓存
- 执行任何INSERT/UPDATE/DELETE操作都会清空缓存
- 手动调用
clearCache()也会清空
这会导致一个典型问题:在事务方法中先查询后更新再查询,第二次查询可能返回旧数据。解决方案:
java复制@Transactional
public void updateUser(User user) {
User dbUser = userMapper.selectById(user.getId()); // 第一次查询
userMapper.update(user); // 更新操作
dbUser = userMapper.selectById(user.getId()); // 这里可能返回缓存数据
SqlSessionUtils.getSqlSession(sessionFactory).clearCache(); // 强制清缓存
}
3.2 二级缓存配置
二级缓存需要显式开启:
xml复制<cache eviction="LRU" flushInterval="60000" size="512"/>
实际使用中要注意:
- 实体类必须实现Serializable
- 关联查询需要特别处理缓存同步
- 分布式环境建议禁用或使用集中式缓存替代
我曾经遇到过因二级缓存导致的生产事故:两个服务共享同一个Mapper命名空间,结果缓存互相污染。最终采用<cache-ref namespace="..."/>明确指定缓存引用关系才解决。
4. 插件开发实战
4.1 拦截器原理
MyBatis插件基于JDK动态代理实现,可以拦截以下方法:
- Executor (update, query, flushStatements等)
- ParameterHandler (getParameterObject, setParameters)
- ResultSetHandler (handleResultSets, handleOutputParameters)
- StatementHandler (prepare, parameterize等)
开发插件的标准流程:
- 实现Interceptor接口
- 用@Intercepts注解指定拦截目标
- 在mybatis-config.xml中注册插件
4.2 分页插件案例
下面是一个简化版分页插件实现:
java复制@Intercepts(@Signature(
type= Executor.class,
method = "query",
args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}))
public class PaginationInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
Object[] args = invocation.getArgs();
RowBounds rb = (RowBounds) args[2];
if(rb == RowBounds.DEFAULT) {
return invocation.proceed();
}
// 修改SQL加入分页逻辑
MappedStatement ms = (MappedStatement) args[0];
BoundSql boundSql = ms.getBoundSql(args[1]);
String newSql = boundSql.getSql() + " LIMIT " + rb.getOffset() + "," + rb.getLimit();
// 创建新的MappedStatement
BoundSql newBoundSql = new BoundSql(...);
MappedStatement newMs = copyMappedStatement(ms, newBoundSql);
args[0] = newMs;
args[2] = RowBounds.DEFAULT;
return invocation.proceed();
}
}
这个插件有几个关键点:
- 只处理非默认RowBounds的查询
- 通过修改SQL语句实现物理分页
- 需要正确处理参数映射关系
5. 与Spring Boot深度集成
5.1 自动配置原理
Spring Boot对MyBatis的自动配置主要通过以下类实现:
- MybatisAutoConfiguration:核心配置类
- MybatisProperties:配置属性绑定
- SqlSessionFactoryBean:构建SqlSessionFactory
常见的配置项包括:
yaml复制mybatis:
mapper-locations: classpath*:mapper/**/*.xml
type-aliases-package: com.example.model
configuration:
map-underscore-to-camel-case: true
default-fetch-size: 100
5.2 事务管理整合
Spring事务与MyBatis整合需要注意:
- 确保使用
@Transactional注解而非手动commit - 事务管理器要配置正确:
java复制@Bean
public PlatformTransactionManager transactionManager(DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
常见问题排查:
- 检查是否同时引入了spring-tx和mybatis-spring
- 确认@Transactional注解的类被Spring代理
- 查看日志中是否有"Creating new transaction"提示
6. 性能调优实战
6.1 语句执行分析
使用MyBatis Log Free插件可以格式化SQL日志:
code复制2023-01-01 10:00:00.123 DEBUG - ==> Preparing: SELECT * FROM user WHERE id=?
2023-01-01 10:00:00.456 DEBUG - ==> Parameters: 1(Integer)
2023-01-01 10:00:00.789 DEBUG - <== Total: 1
关键性能指标:
- 预处理语句耗时
- 参数绑定耗时
- 结果映射耗时
6.2 连接池配置
推荐使用HikariCP配置示例:
yaml复制spring:
datasource:
hikari:
maximum-pool-size: 20
minimum-idle: 5
connection-timeout: 30000
idle-timeout: 600000
max-lifetime: 1800000
监控重点:
- 活跃连接数
- 等待获取连接的线程数
- 连接获取平均耗时
7. 常见问题解决方案
7.1 XML转义问题
MyBatis XML中的特殊字符需要转义:
xml复制<!-- 错误写法 -->
<if test="age < 18">...</if>
<!-- 正确写法 -->
<if test="age < 18">...</if>
完整转义对照表:
| 字符 | 转义写法 |
|---|---|
| < | < |
| > | > |
| & | & |
| " | " |
| ' | ' |
7.2 字段为空判断
动态SQL中判断字符串空值的正确方式:
xml复制<if test="name != null and name != ''">...</if>
<!-- 或者 -->
<if test="@org.apache.commons.lang3.StringUtils@isNotBlank(name)">...</if>
注意直接使用name != ''在某些OGNL版本会有问题。
8. MyBatis与MyBatis-Plus对比
8.1 核心功能差异
功能对比表:
| 特性 | MyBatis | MyBatis-Plus |
|---|---|---|
| CRUD接口 | 手动 | 自动生成 |
| 条件构造器 | 无 | 强大支持 |
| 分页插件 | 需扩展 | 内置 |
| 代码生成器 | 无 | 提供 |
| 性能分析插件 | 需扩展 | 内置 |
8.2 迁移注意事项
从MyBatis迁移到MyBatis-Plus需要:
- 修改Mapper接口继承BaseMapper
- 调整XML中的SQL语句(可选)
- 重构条件查询使用QueryWrapper
- 重新配置分页插件
我在重构若依框架项目时发现,最大的工作量其实是在重构那些复杂的动态SQL条件。建议先在新功能中使用MyBatis-Plus,逐步替换旧代码。
