Appearance
MyBatis 插件机制
MyBatis 插件(Interceptor)是一个强大的扩展机制,允许你在 SQL 执行过程中拦截并修改执行行为。PageHelper 分页插件就是最经典的应用。
一、插件原理
MyBatis 插件基于 责任链模式 + 动态代理
插件拦截的四大对象:
Executor → 执行器(update/query/commit/rollback)
ParameterHandler → 参数处理器(设置参数)
ResultSetHandler → 结果集处理器(处理查询结果)
StatementHandler → 语句处理器(构建 SQL 语句)执行流程与拦截点:
Executor
│
├── [拦截点 1] Executor.update/query
│
▼
StatementHandler
│
├── [拦截点 2] StatementHandler.prepare(创建 Statement)
├── [拦截点 3] StatementHandler.parameterize(设置参数)
│
▼
ParameterHandler
│
├── [拦截点 4] ParameterHandler.setParameters
│
▼
ResultSetHandler
│
└── [拦截点 5] ResultSetHandler.handleResultSets二、自定义插件
2.1 实现步骤
java
// 步骤一:实现 Interceptor 接口
@Intercepts({
@Signature(
type = StatementHandler.class, // 拦截目标
method = "prepare", // 拦截方法
args = {Connection.class, Integer.class} // 方法参数类型
)
})
public class SqlLogInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
System.out.println("========== 拦截开始 ==========");
// 执行原始方法
long start = System.currentTimeMillis();
Object result = invocation.proceed();
long end = System.currentTimeMillis();
System.out.println("SQL 执行耗时:" + (end - start) + "ms");
System.out.println("========== 拦截结束 ==========");
return result;
}
@Override
public Object plugin(Object target) {
// 用 Plugin.wrap 创建代理对象
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
// 读取插件配置参数
}
}步骤二:注册插件
xml
<plugins>
<plugin interceptor="com.example.plugin.SqlLogInterceptor">
<!-- 可选:配置参数 -->
<property name="debug" value="true"/>
</plugin>
</plugins>2.2 拦截 SQL 并修改
java
@Intercepts({
@Signature(
type = StatementHandler.class,
method = "prepare",
args = {Connection.class, Integer.class}
)
})
public class SqlRewriteInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 获取 StatementHandler
StatementHandler handler = (StatementHandler) invocation.getTarget();
// 获取绑定的 SQL(通过反射)
MetaObject metaObject = SystemMetaObject.forObject(handler);
String originalSql = (String) metaObject.getValue("delegate.boundSql.sql");
// 修改 SQL
String newSql = originalSql + " /* 添加的注释 */";
metaObject.setValue("delegate.boundSql.sql", newSql);
System.out.println("原始 SQL:" + originalSql);
System.out.println("修改后 SQL:" + newSql);
return invocation.proceed();
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {}
}2.3 分页拦截器(简化版)
java
@Intercepts({
@Signature(
type = Executor.class,
method = "query",
args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}
)
})
public class SimplePageInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
Object[] args = invocation.getArgs();
MappedStatement ms = (MappedStatement) args[0];
Object parameter = args[1];
RowBounds rowBounds = (RowBounds) args[2];
// 如果没有分页参数,直接执行
if (rowBounds == RowBounds.DEFAULT) {
return invocation.proceed();
}
// 获取原始 SQL
BoundSql boundSql = ms.getBoundSql(parameter);
String originalSql = boundSql.getSql();
// 拼接分页 SQL(MySQL 语法)
String pageSql = originalSql + " LIMIT " + rowBounds.getOffset() + ", " + rowBounds.getLimit();
// 替换 SQL
MetaObject metaObject = SystemMetaObject.forObject(boundSql);
metaObject.setValue("sql", pageSql);
// 清空分页参数,避免重复处理
args[2] = RowBounds.DEFAULT;
return invocation.proceed();
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {}
}三、PageHelper 分页原理
PageHelper 是 MyBatis 最流行的分页插件,核心原理:
① 调用 PageHelper.startPage(pageNum, pageSize)
→ 通过 ThreadLocal 设置分页参数
② 拦截 Executor.query() 方法
→ 从 ThreadLocal 获取分页参数
③ 获取原始 SQL
→ 通过 BoundSql 获取
④ 拼接分页 SQL
→ MySQL:原始 SQL + LIMIT offset, limit
→ Oracle:嵌套查询 + ROWNUM
⑤ 执行分页 SQL 获取数据
⑥ 执行 COUNT 查询获取总数
→ 包装原始 SQL 为 SELECT COUNT(*) FROM (原始SQL) tmp
⑦ 封装 PageInfo 对象返回java
// PageHelper 使用
PageHelper.startPage(1, 10); // 第 1 页,每页 10 条
List<User> users = userMapper.selectAll();
PageInfo<User> pageInfo = new PageInfo<>(users);
System.out.println("总页数:" + pageInfo.getPages());
System.out.println("总条数:" + pageInfo.getTotal());
System.out.println("当前页:" + pageInfo.getPageNum());四、多个插件的执行顺序
多个插件形成责任链:
请求
│
▼
插件 A 拦截
│
▼
插件 B 拦截
│
▼
插件 C 拦截
│
▼
目标方法执行
│
▼
插件 C 后处理
│
▼
插件 B 后处理
│
▼
插件 A 后处理
│
▼
返回结果
执行顺序:按配置文件中 <plugins> 的顺序
如果多个插件拦截同一个方法,先配置的先执行前置处理,后执行后置处理五、拦截点选择指南
| 拦截对象 | 适用场景 | 可拦截方法 |
|---|---|---|
| Executor | 分页、缓存、事务 | update, query, flushStatements, commit, rollback |
| StatementHandler | SQL 改写、加密 | prepare, parameterize, batch, update, query |
| ParameterHandler | 参数加密、脱敏 | setParameters, getParameterObject |
| ResultSetHandler | 结果加密、脱敏 | handleResultSets, handleOutputParameters |
java
// StatementHandler 拦截(最常用,可获取完整 SQL)
@Intercepts({
@Signature(
type = StatementHandler.class,
method = "prepare",
args = {Connection.class, Integer.class}
)
})
// Executor 拦截(分页、缓存)
@Intercepts({
@Signature(
type = Executor.class,
method = "query",
args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}
)
})
// ResultSetHandler 拦截(结果处理)
@Intercepts({
@Signature(
type = ResultSetHandler.class,
method = "handleResultSets",
args = {Statement.class}
)
})六、MetaObject(反射工具)
MyBatis 内置的反射工具类,用于操作对象的属性
常用方法:
getValue("属性路径") → 递归获取属性值
setValue("属性路径", 值) → 递归设置属性值java
// 获取 BoundSql 中的 SQL
MetaObject metaObject = SystemMetaObject.forObject(statementHandler);
String sql = (String) metaObject.getValue("delegate.boundSql.sql");
// 获取参数值
Object parameterObject = metaObject.getValue("delegate.boundSql.parameterObject");
// 设置新 SQL
metaObject.setValue("delegate.boundSql.sql", newSql);七、速记总结
插件核心三要素:
① 实现 Interceptor 接口
② @Intercepts + @Signature 注解指定拦截目标
③ 通过 Plugin.wrap() 创建代理
四大拦截对象:
Executor → 执行器(update/query/commit/rollback)
ParameterHandler → 参数处理器(setParameters)
ResultSetHandler → 结果集处理器(handleResultSets)
StatementHandler → 语句处理器(prepare/parameterize)
PageHelper 原理:
ThreadLocal 存分页参数 → 拦截 Executor.query() → 改写 SQL 拼接 LIMIT
速记口诀:
四大对象都能拦,注解指定要有三,
type 定对象,method 定方法,args 定参数,
Plugin.wrap 做代理,MetaObject 反射取。八、面试要点
| 问题 | 答案要点 |
|---|---|
| 插件原理? | 责任链 + 动态代理,拦截四大对象 |
| 四大拦截对象? | Executor、StatementHandler、ParameterHandler、ResultSetHandler |
| PageHelper 原理? | ThreadLocal 存分页参数,拦截 Executor.query() 改写 SQL |
| 多插件执行顺序? | 按配置文件顺序,先配置的先执行 |
