Plugins
一款好的框架,都是支持定制化的,MyBatis
也不例外,MyBatis
支持用户自定义插件,在MyBatis
的几个核心类的方法调用前,后进行统一的处理。使用方式如下:
首先,定义插件行为,实现Interceptor
接口,同时,通过Intercepts
注解定义插件拦截的位置
,接口方法如下
public interface Interceptor {
//拦截成功后需要进行的操作
Object intercept(Invocation invocation) throws Throwable;
//产生对象的方式,一般应该直接使用默认方式
default Object plugin(Object target) {
return Plugin.wrap(target, this);
}
//设置的属性
default void setProperties(Properties properties) {
// NOP
}
}
例如:
//定义拦截的类,方法,以及参数
@Intercepts({@Signature(
type= Executor.class,
method = "update",
args = {MappedStatement.class,Object.class})})
public class ExamplePlugin implements Interceptor {
private Properties properties = new Properties();
public Object intercept(Invocation invocation) throws Throwable {
//将请求转接到下一个插件
Object returnObject = invocation.proceed();
//返回结果
return returnObject;
}
//设置属性
public void setProperties(Properties properties) {
this.properties = properties;
}
}
需要注意的是invocation.proceed()
这个方法,可以类比Servlet
的过滤器,如果不调用这个方法,则请求将会直接中断在这里,那么其他插件以及MyBatis
中被拦截的类将无法接收到请求,因此需要慎重考虑是否真的不需要调用invocation.proceed()
<plugins>
<plugin interceptor="org.mybatis.example.ExamplePlugin">
<property name="someProperty" value="100"/>
</plugin>
</plugins>
接下来上述方式配置插件。
上面便是MyBatis
的插件的配置方法,可以看到算是比较简单。使用插件可以直接获取到核心的请求处理前的参数,属性等,也可以获取到方法调用完成后的结果,因此可以进行各种统一的处理。
而这种插件式的处理,一般都是通过责任链模式完成,比如Tomcat
的Servlet
的过滤器,MyBatis
也不例外。
首先,需要考虑的是,所配置的这类,你原本是无法直接import
的,因为是通过用户定义然后通过XML
定义的,因此这里一定离不开反射。
其次,MyBatis
的插件的过滤条件不是写在XML
中的,而是放在了类的注解上,因此需要在获取到这个类之后,再获取去注解的信息。
如果是我设计,我可能会先读取配置,加载对应的类,然后通过反射创建对象。这样便拿到了这个对象,然后分析这个类的注解信息,记录过滤条件,生成match()
方法,最后将这两个对象包装起来形成一个责任链的链对象,类似如下
class InterceptorWrapper implements InterceptorChain{
private Matcher matcher;
private Interceptor target;
private InterceptorChain next;
//判断是否符合需要拦截的条件
public boolean isMatch(Method method){
return matcher.match(method);
}
//调用方法
public Object intercept(Invocation invocation) throws Throwable{
if(isMatch(invocation.getMethod())){
target.intercept(invocation);
}else{
next.intercept(invocation);
}
}
}
接下来再将其注册到责任链的注册类中,形成环中的一个链。即可完成。
但是这样做有一个问题,那就是MyBatis
的核心类,Executor
,ParameterHandler
等需要通过怎样的方式才能成为责任链中的最后一环呢?同时Executor
,ParameterHandler
并没有实现InterceptorChain
接口,并且它还实现了其他的接口,如果想要强行修改Executor
,那侵入性也太大了。
因此MyBatis
并没有这样做,而是结合的动态代理,使用动态代理的方式来调用Executor
,ParameterHandler
的核心类,我们可以直接分析源码:
//创建ParameterHandler
public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {
//首先,new ParameterHandler
ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement, parameterObject, boundSql);
//通过`interceptorChain`包装对象
parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler);
return parameterHandler;
}
InterceptorChain#pluginAll()
public Object pluginAll(Object target) {
//遍历所有的interceptors,调用其plugin方法,并返回其返回结果
for (Interceptor interceptor : interceptors) {
target = interceptor.plugin(target);
}
return target;
}
Interceptor#plugin()
方法默认调用的是Plugin#wrap()
方法
Plugin#wrap()
public static Object wrap(Object target, Interceptor interceptor) {
//获取插件注解的需要拦截的Class以及Method
Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
//获取目标对象的Class,一般第一个是核心类,比如`ParameterHandler`
Class<?> type = target.getClass();
//获取被注解标注需要拦截的类与这个类的接口的交集
Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
//如果目标类中包含了这个类需要拦截的接口实现,则生成动态代理对象
if (interfaces.length > 0) {
return Proxy.newProxyInstance(
type.getClassLoader(),
interfaces,
new Plugin(target, interceptor, signatureMap));
}
return target;
}
Plugin#invoke()
@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)) {
//则调用插件的intercept()方法
return interceptor.intercept(new Invocation(target, method, args));
}
//否则,直接调用方法
return method.invoke(target, args);
}
可以看到,总体来说,MyBatis
所 和上面的猜想差不多,不同点便在于通过动态代理将核心类以及其他的类包装了一遍。这样就使得所有的插件最终都变为了所需要拦截的类,比如Executor
,ParameterHandler
等,这样使得客户端感受不到“插件”的存在,而插件也不会侵入到核心类中。
比较巧妙的设计。
接下来分析下MyBatis
的插件的责任链的设计。
首先,MyBatis
创建了一个InterceptorChain
类来保存所有的插件。
然后在进行包装的时候,会将上一个Interceptor
也包装进去。
在Invoke()
方法中,如果满足拦截条件,那么在用户调用invocation.proceed()
的时候,便会调用到下一个链对象,如果不满足条件,则会直接调用下一个链对象。
这样,变形成了
chain1->chain2->chain3->chain4->ParameterHander
因此,需要注意在代码中调用invocation.proceed()
,以免出现断链的情况