系列第 1 篇:手写骨架
系列第 2 篇:ParameterHandler
系列第 3 篇:MappedStatement 与三种 Executor
源码:https://github.com/OrigamiOrigamiOrigami/MyMyBatis
前言
有了Executor之后,横切能力不该继续写进BaseExecutor。官方做法是插件:用 JDK 动态代理把Executor(也可以是 StatementHandler 等)包一层,按签名拦截指定方法。
本篇实现迷你版:Interceptor、@Intercepts/@Signature、Plugin.wrap、InterceptorChain,并附带一个慢 SQL 示例。
本篇目标
| 类 | 作用 |
|---|---|
| Interceptor | intercept/plugin |
| @Intercepts / @Signature | 声明拦截哪个接口的哪个方法 |
| Plugin | InvocationHandler,决定进拦截还是放行 |
| InterceptorChain | 按注册顺序多层wrap |
| SlowSqlInterceptor | 统计query/update耗时 |
1. 注解怎么声明拦截点
@Intercepts({@Signature(type=Executor.class,method="query",args={MappedStatement.class,Object.class}),@Signature(type=Executor.class,method="update",args={MappedStatement.class,Object.class})})publicclassSlowSqlInterceptorimplementsInterceptor{...}type必须是接口。代理只会实现签名里出现过的接口。
2. Plugin.wrap 做了什么
publicstaticObjectwrap(Objecttarget,Interceptorinterceptor){Map<Class<?>,Set<Method>>signatureMap=getSignatureMap(interceptor);Class<?>[]interfaces=getAllInterfaces(target.getClass(),signatureMap);if(interfaces.length>0){returnProxy.newProxyInstance(target.getClass().getClassLoader(),interfaces,newPlugin(target,interceptor,signatureMap));}returntarget;}调用时:
Set<Method>methods=signatureMap.get(method.getDeclaringClass());if(methods!=null&&methods.contains(method)){returninterceptor.intercept(newInvocation(target,method,args));}returnmethod.invoke(target,args);Invocation.proceed()就是method.invoke(target, args),继续往下一层走。
3. 责任链:一层包一层
publicObjectpluginAll(Objecttarget){for(Interceptorinterceptor:interceptors){target=interceptor.plugin(target);}returntarget;}创建 Executor 时:
Executorexecutor=newSimpleExecutor(...);return(Executor)interceptorChain.pluginAll(executor);注册插件:
Configuration.builder().database("jdbc.properties").plugin(newSlowSqlInterceptor()).build();4. 慢 SQL 示例
publicObjectintercept(Invocationinvocation)throwsThrowable{longstart=System.currentTimeMillis();try{returninvocation.proceed();}finally{longcost=System.currentTimeMillis()-start;MappedStatementms=(MappedStatement)invocation.getArgs()[0];if(cost>=thresholdMs){System.out.println("[SlowSQL] "+cost+"ms id="+ms.getId());}else{System.out.println("[SQL] "+cost+"ms id="+ms.getId());}}}阈值可用setProperties配置。分页插件同理:在query前改BoundSql,这里先不做,避免一篇写爆。
5. 为什么一定要挂在 Executor 上
第 3 篇之前逻辑在 Session 里,横切只能改 Session。收口到 Executor 后:
- 慢 SQL、审计、租户改写都有统一挂点
- 换 SIMPLE / BATCH 不影响插件接口
- 和官方阅读源码时的路径一致
和官方的差距
- 官方还能拦
StatementHandler/ParameterHandler/ResultSetHandler,我们目前主要示范 Executor - 没有
Plugin对内部类、桥接方法的全部边角处理 - 分页改写、拦截顺序与
@Signature冲突检测未做强校验
总结
- 插件 = 带签名的 JDK 代理
- 多个插件 = 多层代理
- 业务扩展优先写 Interceptor,而不是改 Executor 源码
建议对照:plugin/Plugin.java、SlowSqlInterceptor.java、Configuration.newExecutor。
下篇预告
MyMyBatis ⑤:动态 SQL、TypeHandler 与 XML Mapper。