框架扩展的几种实现方式
我们都知道业务场景是多变的,框架不可能对每种业务场景都进行支持,因此框架需要预留一些扩展点,让用户利用这些扩展点来增强功能。这些增强的功能称为插件,在当前软件设计中插件随处可在(例如我们常用的chrome,vscode,notepad++等都提供了大量实用的插件,让软件变的更好用)
如果让你对原有的功能进行增强,你会想到哪种方式?
责任链模式,代理模式,装饰者模式等其实都可以的。
Servlet Filter和Spring MVC Interceptor是用的责任链模式
Dubbo Filter同时用了装饰者模式和责任链模式
Mybaits Interceptor用了责任链模式和动态代理
动态代理可以对SQL语句执行过程中的某一点进行拦截,当配置多个插件时,责任链模式可以进行多次拦截,责任链模式的UML图如下
可以看到在一条责任链中,每个Handler对象都包含对下一个Handler对象的引用,一个Handler对象处理完消息会把请求传给下一个Handler对象继续处理,以此类推,直至整条责任链结束。这时我们可以改变Handler的执行顺序,增加或者删除Handler,符合开闭原则
写一个耗时统计插件
我们先写一个打印SQL执行时间的插件,只需要实现Interceptor接口,用@Signature指定要增强的方法,重写intercept方法写增强逻辑即可
@Intercepts({@Signature(type = StatementHandler.class, method = "query", args = { Statement.class, ResultHandler.class }),
@Signature(type = StatementHandler.class, method = "update", args = { Statement.class }),
@Signature(type = StatementHandler.class, method = "batch", args = { Statement.class })})
public class SqlCostTimeInterceptor implements Interceptor {
public static final Logger logger = LoggerFactory.getLogger(SqlCostTimeInterceptor.class);
@Override
public Object intercept(Invocation invocation) throws Throwable {
StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
long start = System.currentTimeMillis();
try {
// 执行被拦截的方法
return invocation.proceed();
} finally {
BoundSql boundSql = statementHandler.getBoundSql();
String sql = boundSql.getSql();
long end = System.currentTimeMillis();
long cost = end - start;
logger.info("{}, cost is {}", sql, cost);
}
}
}
在mybatis配置文件中配置插件
<plugins>
<plugin interceptor="com.javashitang.part1.plugins.SqlCostTimeInterceptor"></plugin>
</plugins>
此时就可以打印出执行的SQL和耗费的时间,效果如下
select id, role_name as roleName, note from role where id = ?, cost is 35
可以看到还是挺简单的,我们来分析一下具体的实现
源码解析
上面说到我们手写插件只需要实现Interceptor接口即可,我们来看看Interceptor接口在哪使用了
InterceptorChain类一看就是责任链模式的实现,addIntercepto是增加插件,而pluginAll是利用插件对原有功能进行增强(我们看Interceptor#plugin方法确认一下)
在plugin方法中调用了Plugin#wrap方法,我们接着追
看到Proxy.newProxyInstance我们就可以确定这不就是动态代理么,通过动态代理对目标对象进行增强,增强的逻辑在Plugin#invoke方法(因为Plugin实现了InvocationHandler接口)
当 interfaces.length > 0 这个条件满足的时候才会进行代理,来追一下这个条件怎么才会满足?
org.apache.ibatis.plugin.Plugin#getSignatureMap
org.apache.ibatis.plugin.Plugin#getAllInterfaces
就是解析Signature注解获取要代理的类及其方法,如果当前的类需要进行代理,才会进行代理,否则不会进行代理
好了我们现在只需要看看InterceptorChain#pluginAll在哪些地方被调用了就能确定插件可以对哪些对象进行增强
可以看到Mybatis的四大重要组件Executor,StatementHandler,ParameterHandler,ResultSetHandler都可以被进行代理
当被代理的方法执行的时候会先执行Plugin#invoke方法(Plugin实现了InvocationHandler接口)
如果当前的方法需要被代理,则会调用Interceptor#intercept方法(这个方法里面我们写了增强逻辑),并把相关的参数封装为一个Invocation类
为了方便执行被代理的方法,Invocation提供了一个proceed方法。好了插件逻辑就分析完了!
参考博客
mybatis插件编写
[1]https://www.cnblogs.com/xrq730/p/6972268.html
mybatis插件实现原理
[2]https://www.cnblogs.com/xrq730/p/6984982.html
[3]https://juejin.im/post/5abe12f5f265da237411177f