MyBatis 作为 Java 主流 ORM 框架,多数开发者仅掌握基础 CRUD 与动态 SQL,但对其核心原理、插件开发、缓存机制、性能优化理解不足,导致代码冗余、查询效率低、扩展性差(如通用分页、数据脱敏无法统一实现)。
本文从 MyBatis 核心原理出发,深入讲解插件开发(拦截器)、缓存机制优化、SQL 执行优化、批量操作技巧,结合实战代码与场景示例,帮你突破 MyBatis 使用瓶颈,写出高效、可扩展的持久层代码。
一、核心认知:MyBatis 核心原理与执行流程
1. 核心架构
MyBatis 架构分为三层,职责清晰:
- 接口层:通过
SqlSession提供 CRUD 操作接口,面向开发者; - 核心层:包含配置解析、SQL 解析、参数映射、结果映射、插件拦截、缓存管理,是 MyBatis 核心;
- 基础层:包含数据源管理、事务管理、日志管理,为核心层提供支撑。
2. 核心执行流程(以查询为例)
- 加载配置:读取 MyBatis 配置文件(
mybatis-config.xml)、Mapper 接口与 XML 文件,解析为Configuration对象; - 创建 SqlSession:通过
SqlSessionFactory创建SqlSession,关联Executor(执行器); - 生成代理对象:Mapper 接口通过 JDK 动态代理生成代理对象,拦截接口方法;
- 解析 SQL:代理对象根据方法名匹配 XML 中的 SQL 语句,解析动态 SQL,生成最终执行 SQL;
- 执行 SQL:
Executor通过StatementHandler执行 SQL,ParameterHandler处理参数,ResultSetHandler处理结果集; - 缓存处理:查询结果先查缓存(一级 / 二级),无缓存则执行数据库查询,结果写入缓存;
- 返回结果:将结果集映射为 Java 对象,返回给调用者。
3. 四大核心组件(插件拦截目标)
MyBatis 插件通过拦截四大核心组件的方法,实现功能增强:
- Executor:执行器,负责 SQL 执行与缓存管理,可拦截
query、update、commit等方法; - StatementHandler:语句处理器,负责 SQL 预编译、参数设置、结果集处理,可拦截
prepare、parameterize等方法; - ParameterHandler:参数处理器,负责将 Java 参数映射为 SQL 参数,可拦截
setParameters方法; - ResultSetHandler:结果集处理器,负责将 SQL 结果集映射为 Java 对象,可拦截
handleResultSets方法。
二、实战:MyBatis 插件开发(拦截器)
MyBatis 插件本质是 JDK 动态代理与责任链模式,通过自定义拦截器,可实现通用功能(如分页、数据脱敏、日志增强、权限控制),无需侵入业务代码。
1. 插件开发核心规范
- 实现
org.apache.ibatis.plugin.Interceptor接口,重写intercept(核心拦截逻辑)、plugin(生成代理对象)、setProperties(读取配置参数)方法; - 用
@Intercepts与@Signature注解指定拦截的组件、方法与参数; - 插件需注册到 MyBatis 配置中(XML / 注解方式),才能生效。
2. 实战 1:通用分页插件(拦截 Executor)
传统分页需在 XML 中写LIMIT语句,通用性差,通过插件拦截Executor.query方法,自动拼接分页 SQL,实现通用分页。
(1)自定义分页拦截器
java
运行
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import java.util.Properties;
// 拦截Executor的query方法(两个重载方法)
@Intercepts({
@Signature(
type = Executor.class,
method = "query",
args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}
),
@Signature(
type = Executor.class,
method = "query",
args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}
)
})
public class PageInterceptor implements Interceptor {
// 核心拦截逻辑
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 1. 获取拦截参数
Object[] args = invocation.getArgs();
MappedStatement ms = (MappedStatement) args[0];
Object parameter = args[1];
RowBounds rowBounds = (RowBounds) args[2];
BoundSql boundSql = ms.getBoundSql(parameter);
// 2. 判断是否需要分页(自定义PageParam参数,包含pageNum、pageSize)
if (parameter instanceof PageParam pageParam) {
int pageNum = pageParam.getPageNum();
int pageSize = pageParam.getPageSize();
if (pageNum > 0 && pageSize > 0) {
// 3. 生成分页SQL(拼接LIMIT,适配MySQL)
String originalSql = boundSql.getSql();
int offset = (pageNum - 1) * pageSize;
String pageSql = originalSql + " LIMIT " + offset + ", " + pageSize;
// 4. 重写BoundSql的SQL语句
BoundSql newBoundSql = new BoundSql(ms.getConfiguration(), pageSql, boundSql.getParameterMappings(), parameter);
// 5. 替换原参数中的BoundSql(适配第二个重载方法)
if (args.length == 6) {
args[5] = newBoundSql;
}
// 6. 重写RowBounds为默认(避免MyBatis自带分页拦截)
args[2] = RowBounds.DEFAULT;
}
}
// 7. 执行原方法
return invocation.proceed();
}
// 生成代理对象(默认实现,无需修改)
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
// 读取插件配置参数(如数据库类型,适配MySQL/Oracle)
@Override
public void setProperties(Properties properties) {
String dbType = properties.getProperty("dbType", "mysql");
// 可根据数据库类型调整分页SQL拼接逻辑
}
}
(2)分页参数类(PageParam)
java
运行
import lombok.Data;
@Data
public class PageParam {
private int pageNum; // 页码(从1开始)
private int pageSize; // 每页条数
// 可扩展:排序字段、排序方向等
}
(3)注册插件(MyBatis 配置)
① XML 方式(mybatis-config.xml)
xml
<configuration>
<plugins>
<!-- 注册分页插件,配置数据库类型 -->
<plugin interceptor="com.example.plugin.PageInterceptor">
<property name="dbType" value="mysql"/>
</plugin>
</plugins>
</configuration>
② Spring Boot 注解方式
java
运行
@Configuration
public class MyBatisConfig {
@Bean
public PageInterceptor pageInterceptor() {
PageInterceptor interceptor = new PageInterceptor();
Properties properties = new Properties();
properties.setProperty("dbType", "mysql");
interceptor.setProperties(properties);
return interceptor;
}
}
(4)使用方式(业务代码)
java
运行
// Mapper接口
public interface OrderMapper {
List<OrderDO> selectOrderList(PageParam pageParam);
}
// XML文件(无需写LIMIT,插件自动拼接)
<select id="selectOrderList" parameterType="com.example.param.PageParam" resultType="com.example.entity.OrderDO">
SELECT id, user_id, product_id, count FROM `order`
</select>
// 服务层调用
@Service
public class OrderService {
@Resource
private OrderMapper orderMapper;
public List<OrderDO> getOrderList(int pageNum, int pageSize) {
PageParam pageParam = new PageParam();
pageParam.setPageNum(pageNum);
pageParam.setPageSize(pageSize);
return orderMapper.selectOrderList(pageParam);
}
}
3. 实战 2:数据脱敏插件(拦截 ResultSetHandler)
对敏感数据(如手机号、身份证号)进行脱敏处理(如手机号显示为 138****8000),通过拦截结果集处理方法,自动脱敏,无需修改业务代码。
(1)自定义脱敏注解(标记需要脱敏的字段)
java
运行
import java.lang.annotation.*;
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Sensitive {
// 脱敏类型(手机号、身份证、姓名)
SensitiveType type();
}
// 脱敏类型枚举
public enum SensitiveType {
PHONE, // 手机号
ID_CARD, // 身份证号
NAME // 姓名
}
(2)脱敏拦截器
java
运行
import org.apache.ibatis.executor.resultset.ResultSetHandler;
import org.apache.ibatis.plugin.*;
import java.lang.reflect.Field;
import java.sql.Statement;
import java.util.List;
import java.util.Properties;
@Intercepts({
@Signature(
type = ResultSetHandler.class,
method = "handleResultSets",
args = {Statement.class}
)
})
public class SensitiveInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 1. 执行原方法,获取结果集
List<?> resultList = (List<?>) invocation.proceed();
// 2. 对结果集进行脱敏处理
for (Object result : resultList) {
desensitize(result);
}
return resultList;
}
// 脱敏处理核心逻辑
private void desensitize(Object result) throws IllegalAccessException {
Class<?> clazz = result.getClass();
// 遍历所有字段,判断是否有@Sensitive注解
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
if (field.isAnnotationPresent(Sensitive.class)) {
field.setAccessible(true); // 允许访问私有字段
Sensitive sensitive = field.getAnnotation(Sensitive.class);
Object value = field.get(result); // 获取字段值
if (value instanceof String strValue && !strValue.isEmpty()) {
// 根据脱敏类型处理
String desensitizedValue = switch (sensitive.type()) {
case PHONE -> desensitizePhone(strValue);
case ID_CARD -> desensitizeIdCard(strValue);
case NAME -> desensitizeName(strValue);
default -> strValue;
};
field.set(result, desensitizedValue); // 设值脱敏后的值
}
}
}
}
// 手机号脱敏:保留前3位+后4位,中间替换为****
private String desensitizePhone(String phone) {
if (phone.length() == 11) {
return phone.replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2");
}
return phone;
}
// 身份证脱敏:保留前6位+后4位,中间替换为****
private String desensitizeIdCard(String idCard) {
if (idCard.length() == 18) {
return idCard.replaceAll("(\\d{6})\\d{8}(\\d{4})", "$1********$2");
}
return idCard;
}
// 姓名脱敏:保留姓,名替换为*
private String desensitizeName(String name) {
if (name.length() >= 2) {
return name.substring(0, 1) + "*".repeat(name.length() - 1);
}
return name;
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {}
}
(3)使用方式
java
运行
// 实体类标记敏感字段
@Data
public class UserDO {
private Long id;
@Sensitive(type = SensitiveType.NAME)
private String userName; // 姓名脱敏
@Sensitive(type = SensitiveType.PHONE)
private String phone; // 手机号脱敏
@Sensitive(type = SensitiveType.ID_CARD)
private String idCard; // 身份证脱敏
}
// 注册插件(同分页插件,添加到MyBatis配置)
@Bean
public SensitiveInterceptor sensitiveInterceptor() {
return new SensitiveInterceptor();
}
// 调用后自动脱敏:phone=13800138000 → 138****8000
三、实战:MyBatis 性能优化
1. 缓存机制优化(减少数据库查询)
MyBatis 提供两级缓存,合理使用可大幅减少数据库查询次数,提升性能。
(1)一级缓存(SqlSession 级别,默认开启)
- 原理:同一
SqlSession内,相同 SQL 查询会缓存结果,再次查询直接返回缓存,SqlSession关闭 / 提交后缓存失效; - 优化点:
- 避免频繁创建
SqlSession(如 Spring 整合 MyBatis 时,SqlSession由 Spring 管理,默认单例); - 读写分离场景下,避免一级缓存导致脏读(可关闭一级缓存,
localCacheScope=STATEMENT)。
(2)二级缓存(Mapper 级别,需手动开启)
- 原理:多个
SqlSession共享同一 Mapper 的缓存,缓存存储在MapperStatement中,默认存储在内存(可配置第三方缓存如 Redis); - 开启方式:
- 全局配置开启(mybatis-config.xml):
xml
<settings>
<setting name="cacheEnabled" value="true"/> <!-- 默认true,可省略 -->
</settings>
- Mapper XML 开启(在对应 XML 中添加
<cache>标签):
xml
<mapper namespace="com.example.mapper.OrderMapper">
<!-- 开启二级缓存,配置缓存参数 -->
<cache
eviction="LRU" <!-- 缓存淘汰策略:LRU(最近最少使用) -->
flushInterval="60000" <!-- 缓存刷新间隔(毫秒) -->
size="1024" <!-- 缓存最大条目数 -->
readOnly="true"/> <!-- 只读缓存,提升性能 -->
</mapper>
- 优化点:
- 仅对高频查询、低频更新的数据开启二级缓存(如商品基础信息);
- 避免缓存大对象(如 List<Order>),占用内存过多;
- 复杂查询(多表联查)不建议开启二级缓存,易导致脏读;
- 整合 Redis 作为二级缓存,解决内存缓存容量有限、集群环境缓存不一致问题。
2. SQL 执行优化
(1)优化动态 SQL,避免冗余
- 用
<where>替代WHERE 1=1,自动处理 AND/OR 逻辑,减少无效 SQL; - 用
<choose><when><otherwise>替代多个<if>,逻辑更清晰,避免冗余判断; - 批量操作使用
<foreach>,避免循环单条执行(如批量插入、批量更新)。
(2)避免SELECT *,仅查询需要的字段
- 减少数据传输量与结果集映射时间,同时避免触发不必要的字段脱敏、转换;
- 示例:
SELECT id, user_id, product_id FROMorder替代 `SELECT * FROM `order。
(3)优化分页查询,避免LIMIT offset过大
- 参考 JVM 调优中分页优化逻辑,用主键定位替代偏移量分页,结合 MyBatis 实现:
xml
<select id="selectOrderByPage" parameterType="map" resultType="OrderDO">
SELECT id, user_id, product_id FROM `order`
WHERE id < #{lastId} <!-- 上一页最后一条数据的ID -->
ORDER BY id DESC
LIMIT #{pageSize}
</select>
3. 批量操作优化
(1)批量插入(MyBatis 批量插入技巧)
- 低效方式:循环调用单条插入(频繁与数据库交互,性能差);
- 高效方式:使用
<foreach>拼接批量插入 SQL,减少数据库交互次数:
xml
<insert id="batchInsertOrder" parameterType="java.util.List">
INSERT INTO `order` (user_id, product_id, count, create_time)
VALUES
<foreach collection="list" item="item" separator=",">
(#{item.userId}, #{item.productId}, #{item.count}, #{item.createTime})
</foreach>
</insert>
- 进阶优化:设置 JDBC 批量提交参数(
rewriteBatchedStatements=true),MySQL 会优化批量插入性能:
yaml
spring:
datasource:
url: jdbc:mysql://localhost:3306/test?rewriteBatchedStatements=true
(2)批量更新(避免循环单条更新)
- 方式 1:
<foreach>拼接 CASE WHEN 语句,单条 SQL 完成批量更新:
xml
<update id="batchUpdateOrder" parameterType="java.util.List">
UPDATE `order`
<set>
<foreach collection="list" item="item" separator=" ">
WHEN id = #{item.id} THEN
count = #{item.count}, update_time = #{item.updateTime}
</foreach>
</set>
WHERE id IN
<foreach collection="list" item="item" open="(" close=")" separator=",">
#{item.id}
</foreach>
</update>
- 方式 2:使用 MyBatis-Plus 的
updateBatchById方法(简化批量更新代码)。
4. 其他性能优化点
- 复用
SqlSession:Spring 整合 MyBatis 时,默认使用SqlSessionTemplate,自动管理SqlSession生命周期,无需手动创建; - 优化参数映射:使用
@Param注解明确参数名称,避免参数位置错误,提升映射效率; - 减少反射开销:MyBatis 默认使用反射映射结果集,可通过 ASM 字节码框架优化(MyBatis 3.4 + 已默认优化);
- 定期清理缓存:二级缓存设置合理的刷新间隔,避免缓存数据过期导致脏读。
四、避坑指南
1. 坑点 1:二级缓存导致脏读
- 表现:多表联查时,关联表数据更新后,二级缓存未刷新,返回旧数据;
- 解决方案:1. 复杂联查不开启二级缓存;2. 配置
flushCache="true",更新时刷新缓存;3. 用 Redis 缓存替代二级缓存,手动控制缓存失效。
2. 坑点 2:插件拦截方法错误,导致 SQL 执行异常
- 表现:自定义插件拦截方法后,SQL 执行报错(如参数丢失、SQL 拼接错误);
- 解决方案:1. 明确拦截的组件与方法签名(参数类型、顺序必须与原方法一致);2. 拦截逻辑中保留原方法核心逻辑,仅做增强;3. 调试时打印 SQL 与参数,定位问题。
3. 坑点 3:批量操作 SQL 过长,导致数据库报错
- 表现:批量插入 / 更新时,拼接的 SQL 过长(超过数据库
max_allowed_packet参数),触发报错; - 解决方案:1. 拆分批量数据(如每 100 条分一批);2. 增大数据库
max_allowed_packet参数(如设置为 16M)。
4. 坑点 4:忽视 ResultMap 优化,导致性能损耗
- 表现:使用
resultType="map"或自动映射,导致字段类型转换频繁、冗余字段映射; - 解决方案:自定义
ResultMap,明确字段映射关系,避免自动映射的冗余开销,同时解决字段名与属性名不一致问题。
5. 坑点 5:一级缓存导致读写分离脏读
- 表现:主库更新数据后,从库未同步,一级缓存返回旧数据;
- 解决方案:1. 关闭一级缓存(
localCacheScope=STATEMENT);2. 读写分离场景下,查询走从库,更新走主库,且更新后清空对应缓存。
五、终极总结:MyBatis 进阶的核心是 "理解原理 + 按需扩展"
MyBatis 的强大之处在于其灵活性与可扩展性 ------ 基础 CRUD 满足日常需求,插件机制可实现通用功能扩展,缓存与 SQL 优化可大幅提升性能。
落地时需记住:
- 先懂原理:理解 MyBatis 执行流程与四大核心组件,再开发插件与优化;
- 插件按需开发:通用功能(分页、脱敏)用插件,避免过度开发导致维护成本上升;
- 性能优化优先:缓存优化、SQL 优化、批量操作优化是提升持久层性能的核心;
- 避坑关键:关注缓存一致性、插件拦截正确性、SQL 合理性,避免引入新问题。
通过本文内容,可突破 MyBatis 基础使用瓶颈,实现从 "会用" 到 "精通" 的跨越,写出高效、可扩展、稳定的持久层代码。