5.3 插件机制:四大拦截点 本节摘要:插件是插在执行链上的"编外器械"——框架在创建四大核心对象时用动态代理包一层,你声明要拦哪个对象的哪个方法,调用就会先经过你的代码。本节先认清四大对象各自能拦什么,再拆开 Plugin.wrap 的责任链看原理,最后手写一个慢 SQL 计时插件并注册生效。 5.2 的 TypeHandler 管"点"上的翻译——每个字段一对类型。这一节的插件管"链"上的插队——横跨所有 SQL 的共性需求,分页、慢 SQL 审计、数据脱敏,都不必改业务代码。 四大对象,各管一段 SQL 从提交到返回,要经过四个核心对象的手。
本节摘要:插件是插在执行链上的"编外器械"——框架在创建四大核心对象时用动态代理包一层,你声明要拦哪个对象的哪个方法,调用就会先经过你的代码。本节先认清四大对象各自能拦什么,再拆开 Plugin.wrap 的责任链看原理,最后手写一个慢 SQL 计时插件并注册生效。
5.2 的 TypeHandler 管"点"上的翻译——每个字段一对类型。这一节的插件管"链"上的插队——横跨所有 SQL 的共性需求,分页、慢 SQL 审计、数据脱敏,都不必改业务代码。
SQL 从提交到返回,要经过四个核心对象的手。每个对象都留了可拦的方法口:
| 对象 | 职责 | 常拦的方法 |
|---|---|---|
| Executor | 调度器:管增删改查的执行、事务提交回滚 | query、update、commit、rollback |
| StatementHandler | 语句处理器:管 PreparedStatement 创建与执行 | prepare、parameterize、query |
| ParameterHandler | 参数处理器:管参数设值 | setParameter |
| ResultSetHandler | 结果处理器:管结果集到对象的映射 | handleResultSets |
选哪个口下刀,取决于你想改什么:改 SQL 文本(加 LIMIT、加权限过滤)拦 StatementHandler.prepare;只记录执行耗时拦 Executor.query/update;做参数加解密拦 ParameterHandler;做结果脱敏拦 ResultSetHandler。
框架在创建四大对象时检查插件清单,有的话就用 Plugin.wrap 把对象包成 JDK 动态代理。多个插件按配置顺序层层包裹,调用时像洋葱一样一层层进出:

看图里最重要的一件事:每个插件的 intercept 方法拿到 Invocation 后,必须调 invocation.proceed() 让调用继续往下走。proceed 之后的结果还可以再加工——这就是"返回原路再加工"的落点。
背景:想在不改任何业务代码的前提下,把执行超过 500 毫秒的 SQL 连同耗时打出来告警。拦 Executor 的 query 和 update 两个口就够覆盖全量读写。
@Intercepts({ @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}), @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}) }) public class SlowSqlTimerPlugin implements Interceptor { private long thresholdMs = 500; // 慢 SQL 阈值,可由 properties 注入 @Override public Object intercept(Invocation invocation) throws Throwable { MappedStatement ms = (MappedStatement) invocation.getArgs()[0]; long start = System.currentTimeMillis(); try { return invocation.proceed(); // 放行:继续走真实执行 } finally { long cost = System.currentTimeMillis() - start; if (cost >= thresholdMs) { System.out.printf("[慢SQL] %dms -> %s (%s)%n", cost, ms.getId(), invocation.getMethod().getName()); // 生产环境这里接告警通道,而不是控制台 } } } @Override public Object plugin(Object target) { // 只对四大接口对象包代理,其他对象原样放行 return (target instanceof Executor) ? Plugin.wrap(target, this) : target; } @Override public void setProperties(Properties props) { String v = props.getProperty("thresholdMs"); if (v != null) thresholdMs = Long.parseLong(v); } }
三个方法各司其职:@Intercepts/@Signature 声明要拦的口(接口 + 方法名 + 参数类型,三者必须完全对得上,参数列表写错一个就拦不到);intercept 是核心——记录开始时间,proceed 放行,finally 里算耗时,保证异常路径也能记账;plugin 决定包不包。
注册——配置文件里一行声明,属性顺带注入:
<plugins> <plugin interceptor="com.example.plugin.SlowSqlTimerPlugin"> <property name="thresholdMs" value="500"/> </plugin> </plugins>
验证:造一条必然慢的查询(比如 SQL 里塞 SLEEP(1)),执行一次:
[慢SQL] 1013ms -> com.example.mapper.UserMapper.findByComplexCond (query)
mapper id 和耗时都有了,定位到方法级。
⚠️ 常见坑:@Signature 的 args 必须与方法签名的参数类型逐位对应。Executor.query 有一个带 ResultHandler 的五参重载、一个带 RowBounds 的四参重载,只写四参就拦不到传了 ResultHandler 的那次调用。拿不准时把两个重载都声明上。
💡 关键直觉:插件是全局横切工具,代价是每一次 SQL 调用都多穿一层代理。能拦一个方法就别拦整个接口,能拦 Executor 就别深入 StatementHandler——拦截点越深,改坏执行链的风险越大。分页、审计这类框架级需求才用插件,单次业务差异请走 SQL 参数。
编外器械装完了,下一节上最后一件监护设备:MBG——三十张表的标准手术方案,让代码生成器批量预制。