说说 Mybatis 插件的运行原理?

#Java#面试八股

说说 Mybatis 插件的运行原理?

结论先行:MyBatis 插件本质是基于 JDK 动态代理的拦截器链。Plugin.wrap() 把目标对象(Executor、StatementHandler、ParameterHandler、ResultSetHandler)包装成代理对象,调用时按 @Intercepts 声明的签名匹配方法,命中就执行 Interceptor.intercept(),否则透传。多个插件会层层包裹,形成「洋葱式」代理链。

1. 四个可拦截点

被代理对象能拦截的方法典型用途
Executorupdate、query、queryCursor、flushStatements、commit、rollback分页、读写分离、SQL 审计、缓存
StatementHandlerprepare、parameterize、batch、update、query改写 SQL、加 hint、监控执行时间
ParameterHandlersetParameters、getParameterObject参数脱敏、默认值填充
ResultSetHandlerhandleResultSets、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() 要调用、顺序要设计,剩下的就是在这个切面上做你想做的事。

笔记加载中…