MyBatis 的插件机制是其核心扩展点之一,基于 动态代理 和 责任链模式 实现,允许开发者在 SQL 执行的关键节点插入自定义逻辑(如性能监控、SQL 改写、数据加密等)。以下从原理到实践全面解析:
一、核心机制:动态代理 + 拦截器链
-
动态代理(JDK Proxy) MyBatis 通过动态代理为四大核心组件生成代理对象:
-
Executor
:执行 SQL(增删改查、事务) -
StatementHandler
:处理 SQL 语法(预编译、参数设置) -
ParameterHandler
:处理 SQL 参数 -
ResultSetHandler
:处理结果集映射插件通过代理对象拦截目标方法,在方法执行前后插入自定义逻辑。
-
-
拦截器链(InterceptorChain)
-
所有配置的插件存储在
InterceptorChain
中,按配置顺序形成责任链。 -
创建四大对象时,调用InterceptorChain.pluginAll(),对原始对象层层包装代理(类似“洋葱模型”):
// 简化代码:Configuration 中创建 Executor public Executor newExecutor(Transaction tx) { Executor executor = new SimpleExecutor(this, tx); executor = interceptorChain.pluginAll(executor); // 生成代理链 return executor; }
每个插件的plugin()方法调用Plugin.wrap()生成代理对象。
-
-
拦截器接口(Interceptor) • 定义:插件需实现Interceptor接口,包含三个核心方法:
intercept(Invocation invocation):拦截目标方法,执行自定义逻辑。
plugin(Object target):生成代理对象,包装目标对象。
setProperties(Properties properties):设置插件属性(通过 XML 配置传递参数)。
• 作用:拦截器是插件的核心逻辑载体,决定何时触发拦截及如何处理。
二、可拦截的组件与方法
拦截点 | 拦截场景 | 典型方法 |
---|---|---|
Executor | SQL 执行流程(最常用) | update() , query() , commit() , rollback() |
StatementHandler | SQL 预编译与执行 | prepare() , parameterize() (设置参数) |
ParameterHandler | 参数处理 | setParameters() (填充 PreparedStatement 参数) |
ResultSetHandler | 结果集映射 | handleResultSets() (将 ResultSet 转为 Java 对象) |
⚠️ 注意:仅能拦截这四类接口的方法,其他组件无法拦截。
三、插件实现:Interceptor 接口
自定义插件需实现 Interceptor
接口,并标注拦截签名:
@Intercepts({
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})
})
public class MyPlugin implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 1. 前置逻辑(如记录 SQL 开始时间)
System.out.println("Before method: " + invocation.getMethod().getName());
// 2. 执行原始方法(必须调用!)
Object result = invocation.proceed();
// 3. 后置逻辑(如计算 SQL 耗时)
System.out.println("After method");
return result;
}
@Override
public Object plugin(Object target) {
// 生成代理对象(固定写法)
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
// 读取配置文件中的属性(可选)
}
}
-
@Intercepts
:声明拦截目标方法签名(类、方法名、参数类型)。 -
intercept()
:核心方法,通过Invocation.proceed()调用原始逻辑。 -
Plugin.wrap()
:检查目标对象是否匹配拦截条件,匹配则生成代理对象。
四、工作流程
-
初始化阶段:
-
解析mybatis-config.xml中的<plugins>,将插件加入InterceptorChain
-
-
代理对象生成:
-
创建四大对象时,调用interceptorChain.pluginAll(),按顺序应用所有插件的plugin()方法。
-
-
方法执行时:
-
代理对象方法被调用 → 触发插件的intercept()→ 执行自定义逻辑 → 调Invocation.proceed()进入下一层代理或原始方法。
-
五、插件开发示例
-
日志记录插件
@Intercepts({ @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}) }) public class SqlLogInterceptor implements Interceptor { @Override public Object intercept(Invocation invocation) throws Throwable { MappedStatement ms = (MappedStatement) invocation.getArgs()[0]; Object parameter = invocation.getArgs()[1]; String sql = ms.getBoundSql(parameter).getSql(); System.out.println("Executing SQL: " + sql); long start = System.currentTimeMillis(); Object result = invocation.proceed(); System.out.println("Execution time: " + (System.currentTimeMillis() - start) + "ms"); return result; } @Override public Object plugin(Object target) { return Plugin.wrap(target, this); } }
-
分页插件(简化版)
@Intercepts({ @Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class}) }) public class PaginationInterceptor implements Interceptor { @Override public Object intercept(Invocation invocation) throws Throwable { StatementHandler handler = (StatementHandler) invocation.getTarget(); MappedStatement ms = handler.getMappedStatement(); Object parameter = handler.getParameterHandler().getParameterObject(); // 动态改写 SQL 添加分页逻辑(如 LIMIT/OFFSET) BoundSql boundSql = handler.getBoundSql(); String originalSql = boundSql.getSql(); String pageSql = originalSql + " LIMIT 0, 10"; // 示例分页 // 通过反射修改 BoundSql 中的 SQL Field field = boundSql.getClass().getDeclaredField("sql"); field.setAccessible(true); field.set(boundSql, pageSql); return invocation.proceed(); } }
六、应用场景总结
场景 拦截点 典型功能 关键 API SQL 性能监控 StatementHandler.query/update
记录执行时间、慢 SQL 告警 System.currentTimeMillis()
SQL 重写 StatementHandler.prepare
添加注释、分页优化、数据权限过滤 BoundSql.getSql()
+ 反射修改参数预处理 ParameterHandler.setParameters
参数加密、格式转换 PreparedStatement
设参结果集后处理 ResultSetHandler.handleResultSets
数据脱敏、缓存结果、日志统计 强转结果为 List<?>
后处理