说说 Mybatis 插件的运行原理?
结论先行:MyBatis 插件本质是基于 JDK 动态代理的拦截器链。Plugin.wrap() 把目标对象(Executor、StatementHandler、ParameterHandler、ResultSetHandler)包装成代理对象,调用时按 @Intercepts 声明的签名匹配方法,命中就执行 Interceptor.intercept(),否则透传。多个插件会层层包裹,形成「洋葱式」代理链。
1. 四个可拦截点
| 被代理对象 | 能拦截的方法 | 典型用途 |
|---|---|---|
Executor | update、query、queryCursor、flushStatements、commit、rollback | 分页、读写分离、SQL 审计、缓存 |
StatementHandler | prepare、parameterize、batch、update、query | 改写 SQL、加 hint、监控执行时间 |
ParameterHandler | setParameters、getParameterObject | 参数脱敏、默认值填充 |
ResultSetHandler | handleResultSets、handleOutputParameters | 结果脱敏、字段加解密 |
分页插件通常拦 Executor.query(PageHelper 就是),因为这里能同时拿到 SQL、参数和分页参数,改写代价最小。
2. 代理链是怎么搭起来的
SqlSessionFactory
└─ new Executor() 之后,依次 wrap 所有 Interceptor
target = 真实 Executor
plugin1 = Plugin.wrap(target, interceptor1) ← 最内层
plugin2 = Plugin.wrap(plugin1, interceptor2) ← 外层
...
最终返回 pluginN,调用时从外向内:pluginN → ... → plugin1 → 真实对象
对应源码里的 InterceptorChain:
// 简化后的核心逻辑
for (Interceptor interceptor : interceptors) {
target = interceptor.plugin(target); // 默认实现即 Plugin.wrap(target, this)
}
return target;
Plugin.wrap 内部:先 getSignatureMap 解析注解,判断当前要代理的接口是否有匹配签名;没有匹配就原样返回目标对象,有匹配才创建 Proxy。
3. 写一个插件
@Intercepts({
@Signature(type = Executor.class, method = "query",
args = {MappedStatement.class, Object.class,
RowBounds.class, ResultHandler.class})
})
public class SqlCostInterceptor implements Interceptor {
private static final Logger log = LoggerFactory.getLogger(SqlCostInterceptor.class);
@Override
public Object intercept(Invocation invocation) throws Throwable {
long start = System.nanoTime();
try {
return invocation.proceed(); // 放行到下一个插件或真实对象
} finally {
long cost = (System.nanoTime() - start) / 1_000_000;
MappedStatement ms = (MappedStatement) invocation.getArgs()[0];
log.info("sql={} cost={}ms", ms.getId(), cost);
}
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this); // 默认实现可省略
}
@Override
public void setProperties(Properties properties) {
// 读取 <plugin><property name="..." value="..."/></plugin>
}
}
<plugins>
<plugin interceptor="com.example.SqlCostInterceptor">
<property name="slowThreshold" value="200"/>
</plugin>
</plugins>
Spring Boot 里更简单:把 Interceptor 声明成 @Component 或 @Bean,MybatisAutoConfiguration 会自动收集所有 Interceptor 注入 Configuration。
4. 两个容易踩的坑
坑一:@Signature 的 args 必须和真实方法签名完全一致。 少写一个 ResultHandler.class 就匹配不上,插件静默失效,不报错。这是插件「不生效」的第一大原因。
坑二:在插件里调用 executor.query(...) 会再次进入代理链。 如果拦截器内部又用同一个 Executor 执行查询,会死循环或重复拦截。改写 SQL 应走 invocation.getArgs() 替换 MappedStatement,而不是再发一次查询。
5. 插件顺序
<plugins> 的声明顺序 = 代理链的从内到外顺序。排在前面的插件离真实对象更近,invocation.proceed() 之后返回得更晚。所以「分页插件」通常放在最前面,保证它在最接近真实执行的位置改写 SQL;日志插件放后面,统计的是包含分页改写的总耗时。
6. 常见追问
追问一:插件能拦截 ResultSet 本身吗?
不能。ResultSet 不是 MyBatis 创建的代理目标,只能拦 ResultSetHandler,在结果映射前后做手脚。
追问二:为什么说插件有性能损耗?
每次调用多一层反射与 Proxy invoke,且 Plugin.wrap 会对所有语句做签名匹配。插件越多、调用越频繁,损耗越明显。生产上不要为了打日志加一堆插件。
追问三:MyBatis-Plus 的插件怎么工作?
同样是 Interceptor,但它的 MybatisPlusInterceptor 内部维护一个 InnerInterceptor 列表,在 intercept 里依次调用 beforeQuery/beforePrepare,本质上是一个「复合插件」,便于统一管理分页、乐观锁、防全表更新等多个能力。
追问四:插件能修改返回值类型吗?
可以,intercept 直接返回别的对象即可,但调用方会按原类型强转,类型不匹配会 ClassCastException。分页插件返回的是 List,就是因为原方法返回 List。
小结
插件的全部秘密就是三层:注解声明拦截点 → Plugin.wrap 生成动态代理 → InterceptorChain 串成责任链。写插件时记住三件事:签名要精确、proceed() 要调用、顺序要设计,剩下的就是在这个切面上做你想做的事。