← 返回题目列表

MyBatis 插件机制的原理是什么?能拦截哪些对象?

高频 困难 第 13 / 24 题 更新于 2026/07/26
MyBatisPluginInterceptor分页插件

简化版

MyBatis 插件通过 Interceptor 和 JDK 动态代理,包装 Executor、StatementHandler、ParameterHandler、ResultSetHandler 四类核心对象的指定方法。@Intercepts/@Signature 声明拦截类型、方法和参数,intercept 执行增强;它不是任意方法 AOP,改写 SQL 时还要同步考虑参数映射、缓存键和插件顺序。

详细版

Configuration 创建四类对象时,会依次经过 InterceptorChain 的 pluginAll,匹配签名的插件返回代理。方法调用进入 Invocation 后,插件可查看 target、method、args,调用 invocation.proceed() 继续执行,或在前后增加分页、审计、脱敏等逻辑。

拦截层次决定可见信息:Executor 层能看到 MappedStatement、参数、RowBounds 和缓存流程;StatementHandler 层更接近最终 BoundSql/JDBC Statement;ParameterHandler 负责入参;ResultSetHandler 负责结果集。选择过晚的 SQL 改写点可能让 CacheKey 与真实 SQL 不一致。

完整版教学

一、一个最小拦截器

@Intercepts({
  @Signature(
    type = StatementHandler.class,
    method = "prepare",
    args = {Connection.class, Integer.class}
  )
})
class SqlTimingInterceptor implements Interceptor {
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        long start = System.nanoTime();
        try {
            return invocation.proceed();
        } finally {
            record(System.nanoTime() - start);
        }
    }
}

Signature 必须与目标接口方法准确对应,否则插件不会按预期拦截或启动时报配置错误。

可拦截对象典型方法更适合做什么不适合做什么
Executorqueryupdate高层查询更新、缓存相关增强只关心 JDBC Statement 细节
StatementHandlerpreparequeryupdate分页 SQL、SQL 计时、方言处理忽略 CacheKey 直接改 SQL
ParameterHandlersetParameters参数审计、特殊类型处理大范围改写 SQL 结构
ResultSetHandlerhandleResultSets结果脱敏、映射后增强影响查询执行计划

记忆钩子:插件不是万能 AOP,它只能站在 MyBatis 允许的四个路口;站得越靠前,越影响缓存和执行计划,站得越靠后,越接近 JDBC 或结果对象。

二、代理是如何套上的

每当 MyBatis 创建 Executor 等对象,InterceptorChain 依注册顺序调用每个插件的 plugin。默认实现通常使用 Plugin.wrap(target, this),只为匹配接口生成 JDK 代理。

多个插件会形成代理链,注册顺序影响调用嵌套和前后置执行顺序。插件之间都修改 BoundSql 时尤其需要集成测试,不能依赖偶然顺序。

原始 StatementHandler
  -> PluginA.wrap
    -> PluginB.wrap
      -> PluginC.wrap

调用时进入顺序可能是 C -> B -> A -> 原始对象
返回时再按相反方向退出

如果一个分页插件改了 SQL,另一个审计插件也读取 SQL,注册顺序会决定审计看到的是原 SQL 还是分页后的 SQL。一个项目里有 3 个插件时,至少要用集成测试覆盖组合顺序,而不是只测每个插件单独工作。

三、四类拦截点如何选择

  • Executor:查询、更新、提交、缓存等较高层行为。
  • StatementHandler:SQL prepare、parameterize、query/update 等 JDBC 前后行为。
  • ParameterHandler:把参数设置到 PreparedStatement。
  • ResultSetHandler:结果集与存储过程输出参数处理。

分页、租户条件等需要改 SQL 的插件,必须同时理解 Count 查询、参数映射、方言和缓存;结果脱敏则更接近 ResultSetHandler 或业务层。

四、SQL 改写的风险

仅替换 SQL 字符串却不更新 ParameterMapping,会造成占位符数量与参数不一致。新增参数还要放入 BoundSql 可访问的附加参数或原参数对象中,并使用正确 TypeHandler。

Executor 可能已经根据 statement、分页、SQL 和参数生成 CacheKey;若之后才在 StatementHandler 修改 SQL,两个不同真实查询可能错误共享缓存键。成熟分页插件会选择合适层次并协调缓存与参数。

带数字例子:原 SQL 是 SELECT * FROM orders WHERE status = ?,只有 1 个参数。租户插件若改成 ... WHERE status = ? AND tenant_id = ?,SQL 里变成 2 个占位符,但 ParameterMapping 仍只有 1 个,就会在绑定阶段报错或参数错位。即使补了参数,如果缓存键仍按原 SQL 生成,租户 1 和租户 2 可能错误共享查询缓存,这是严重数据隔离问题。

五、不要用插件承载业务逻辑

插件影响所有匹配语句,排错范围大,适合通用基础设施能力。复杂权限、订单规则等业务逻辑放进 SQL 插件,会隐藏执行条件并增加误更新风险。

多租户插件也必须处理 JOIN、子查询、批处理、绕过规则和管理员场景,不能靠简单字符串拼接 WHERE。

六、性能与可观测性

插件处于数据库热路径,应避免每次反射解析大对象、打印完整结果集或同步调用远程服务。SQL 日志要参数脱敏并限制长度,计时还要区分等待连接、JDBC 执行和结果映射阶段。

七、常见误区与追问

  • 误区:MyBatis 插件能拦截任意 Mapper 方法。 它只能拦截 Executor、StatementHandler、ParameterHandler、ResultSetHandler 这四类对象的指定方法。
  • 误区:改 SQL 只要替换 BoundSql 的字符串。 新增或删除占位符时必须同步 ParameterMapping、附加参数和 TypeHandler,否则参数绑定会错。
  • 追问:分页插件常拦截哪一层? 常见做法是在 Executor 或 StatementHandler 层处理,既要能拿到 SQL,又要考虑 RowBounds、Count 查询和缓存键。
  • 追问:插件顺序为什么重要? 多个代理嵌套执行,谁先改 SQL、谁后记录日志会影响最终行为和可观测结果。
  • 误区:业务权限规则很适合塞进 MyBatis 插件。 插件影响范围广且隐蔽,复杂业务规则放进去会增加误更新和排查成本。
  • 追问:为什么 SQL 改写要考虑 CacheKey? 如果缓存键按旧 SQL 或旧参数生成,不同真实查询可能命中同一缓存,造成错误结果。

八、加强记忆

MyBatis 插件只代理四大对象的指定方法:Executor 高层调度,StatementHandler 靠近 SQL,ParameterHandler 绑参数,ResultSetHandler 映射结果。改 SQL 必须同步参数和 CacheKey,插件顺序也要测试,通用增强才适合放插件。