mybatis插件通过jdk动态代理拦截executor、statementhandler等四个接口的public非final方法,需实现interceptor接口并用@intercepts精确声明@signature;一级缓存绑定sqlsession生命周期;二级缓存需满足全局启用、mapper声明、pojo可序列化、sql未禁用缓存四条件;插件执行在缓存外层,顺序为plugin→cachingexecutor→baseexecutor。

MyBatis插件怎么拦截Executor、StatementHandler这些对象
MyBatis插件本质是基于JDK动态代理,只对Executor、ParameterHandler、ResultSetHandler、StatementHandler这四个接口生效。不是所有方法都能被拦截,必须是接口中定义的、且未被final修饰的public方法。
关键点在于:插件类需实现Interceptor接口,并在@Intercepts注解里明确指定@Signature——包括type(目标接口)、method(方法名)、args(参数类型数组)。漏写args或类型不匹配会导致代理失效,根本不会进入intercept()方法。
常见错误现象:
- 插件类加了@Intercepts但没生效 → 检查args是否与目标方法签名完全一致(比如query(Statement, ResultHandler)不能写成query(Object, Object))
- 拦截Executor.update()时发现入参是MappedStatement而非String → 因为实际调用的是Executor.doUpdate(MappedStatement, Object),真正执行SQL的是底层StatementHandler
@Intercepts({
@Signature(type = Executor.class, method = "update",
args = {MappedStatement.class, Object.class})
})
public class MyPlugin implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
System.out.println("before update");
Object result = invocation.proceed(); // 必须调用,否则链中断
System.out.println("after update");
return result;
}
}
一级缓存为什么在同一个SqlSession里才生效
一级缓存是BaseExecutor内部维护的PerpetualCache,生命周期与SqlSession绑定。只要SqlSession没关闭或没调用clearCache(),后续相同MappedStatement + 相同参数的查询就会命中缓存。
Java JDK 25 来自 OpenJDK 官方归档,版本为 JDK 25,本条下载地址已指向官方 Windows x64 zip 安装包直链,适合调试旧项目或兼容旧版 Java 运行环境。
容易踩的坑:
- 多线程共用一个SqlSession → 缓存状态混乱,可能读到脏数据
- 执行了insert/update/delete操作后,缓存自动清空 → 这是MyBatis默认行为,由BaseExecutor.clearLocalCacheOnUpdate()触发,无需手动干预
- 使用RowBounds分页时缓存失效 → 因为RowBounds参与key计算,两次offset/limit不同则key不同
验证方式:开启log4j.logger.org.apache.ibatis.cache=DEBUG,看到Cache Hit Ratio统计即可确认是否命中。
二级缓存开启后为什么还是没生效
二级缓存是Mapper级别(即namespace),默认关闭,必须同时满足四个条件:
- 全局配置cacheEnabled=true(默认true,但显式设为false会禁用)
- Mapper XML中声明<cache></cache>或使用@CacheNamespace注解
- 对应的POJO实现Serializable接口(否则反序列化失败,缓存写入直接跳过)
- 执行的SQL语句所在的MappedStatement没有设置useCache="false"
典型问题:
- 开启了二级缓存但日志里始终没有Cache Hit Ratio → 检查POJO是否真的可序列化(包括其所有字段类型,如含ThreadLocal或InputStream会报NotSerializableException)
- 同一Mapper下部分查询走缓存、部分不走 → 查看对应<select></select>标签是否写了useCache="false",或是否用了flushCache="true"
- 多个SqlSession查同一数据,仍发SQL → 确认是否提交了事务(二级缓存仅在commit后写入,未提交前其他session不可见)
缓存和插件谁先执行、会不会互相干扰
执行顺序是固定的:插件链在最外层,缓存逻辑在CachingExecutor内部。当启用二级缓存时,MyBatis会把原始Executor包装成CachingExecutor,而你的插件又会去代理这个CachingExecutor。所以调用链是:Plugin → CachingExecutor → BaseExecutor。
这意味着:
- 如果你在插件里修改了Invocation.args(比如篡改SQL参数),会影响后续缓存key的计算 → 可能导致缓存击穿或误命中
- CachingExecutor的query()方法本身也会被插件拦截,此时invocation.getTarget()返回的是CachingExecutor实例,不是BaseExecutor
- 不建议在插件中直接操作Executor.localCache,因为二级缓存场景下它只是临时存储,最终以delegate(即CachingExecutor持有的Cache)为准
最易忽略的一点:插件的plugin()方法返回代理对象,如果忘了调用Plugin.wrap(target, this),整个插件就形同虚设——Spring Boot里常因没在@Configuration类中正确注册Interceptor bean而导致此问题。
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南










