1.1 责任链 + 动态代理
MyBatis 插件通过 JDK 动态代理实现。多个插件依次嵌套形成责任链:
1 2 3 4
| 外部调用 → PluginB.invoke() → InterceptorB 前置 → PluginA.invoke() → InterceptorA 前置 → 原始方法 → InterceptorA 后置 → InterceptorB 后置
|
可拦截四个接口:Executor、StatementHandler、ParameterHandler、ResultSetHandler。
1.2 Plugin.wrap() 源码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| public class Plugin implements InvocationHandler { private final Object target; private final Interceptor interceptor; private final Map<Class<?>, Set<Method>> signatureMap;
@Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { Set<Method> methods = signatureMap.get(method.getDeclaringClass()); if (methods != null && methods.contains(method)) { return interceptor.intercept(new Invocation(target, method, args)); } return method.invoke(target, args); }
public static Object wrap(Object target, Interceptor interceptor) { Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor); Class<?>[] interfaces = getAllInterfaces(target.getClass(), signatureMap); if (interfaces.length > 0) { return Proxy.newProxyInstance( target.getClass().getClassLoader(), interfaces, new Plugin(target, interceptor, signatureMap)); } return target; } }
|
Executor 在 Configuration.newExecutor() 中包装,StatementHandler 在 Configuration.newStatementHandler() 中包装。
1.3 PageHelper 分页插件原理
拦截 Executor.query() 实现物理分页:
PageHelper.startPage(pageNum, pageSize) → ThreadLocal 存储分页参数
- 插件拦截器检测到 ThreadLocal 有 Page → 不直接放行
- 先执行 COUNT 查询获取总记录数
- 解析原始 SQL,按方言生成分页 SQL(MySQL:
LIMIT offset,size,Oracle: ROWNUM)
- 反射替换
BoundSql 中的 SQL 文本 → 调 invocation.proceed() 执行
- 封装
PageInfo 返回
ThreadLocal 陷阱:startPage() 后必须紧跟 Mapper 方法,否则参数可能被同线程后续查询误消费。
1.4 自定义插件:慢 SQL 告警
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| @Intercepts(@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})) public class SlowSqlPlugin implements Interceptor { private long threshold;
@Override public Object intercept(Invocation invocation) throws Throwable { long start = System.currentTimeMillis(); try { return invocation.proceed(); } finally { long elapsed = System.currentTimeMillis() - start; if (elapsed > threshold) { MappedStatement ms = (MappedStatement) invocation.getArgs()[0]; log.warn("慢SQL: [{}] 耗时{}ms", ms.getId(), elapsed); } } } public Object plugin(Object target) { return Plugin.wrap(target, this); } public void setProperties(Properties props) { this.threshold = Long.parseLong(props.getProperty("threshold", "1000")); } }
|