MyBatis 的秘密(九)插件

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的插件的配置方法,可以看到算是比较简单。使用插件可以直接获取到核心的请求处理前的参数,属性等,也可以获取到方法调用完成后的结果,因此可以进行各种统一的处理。

而这种插件式的处理,一般都是通过责任链模式完成,比如TomcatServlet的过滤器,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 所 和上面的猜想差不多,不同点便在于通过动态代理将核心类以及其他的类包装了一遍。这样就使得所有的插件最终都变为了所需要拦截的类,比如ExecutorParameterHandler等,这样使得客户端感受不到“插件”的存在,而插件也不会侵入到核心类中。

比较巧妙的设计。


接下来分析下MyBatis的插件的责任链的设计。

首先,MyBatis创建了一个InterceptorChain类来保存所有的插件。
然后在进行包装的时候,会将上一个Interceptor 也包装进去。

Invoke()方法中,如果满足拦截条件,那么在用户调用invocation.proceed()的时候,便会调用到下一个链对象,如果不满足条件,则会直接调用下一个链对象。

这样,变形成了

chain1->chain2->chain3->chain4->ParameterHander

因此,需要注意在代码中调用invocation.proceed(),以免出现断链的情况