15-Mybatis源码和设计模式-6(插件模块和代理模式、责任链模式)

文章目录

  • Mybatis插件
    • 一、自定义插件
      • 1.1 示例
      • 1.2 要点
    • 二、插件的配置和解析
      • 2.1 配置
      • 2.2 解析
        • 2.2.1 XMLConfigBuilder#parseConfiguration
        • 2.2.2 XMLConfigBuilder#pluginElement
    • 三、代理
      • 3.1 代理增强
        • 3.1.1 Configuration
        • 3.1.2 InterceptorChain
      • 3.2 Plugin类
        • 3.2.1 细节
        • 3.2.2 Plugin类
      • 3.3 小结
    • 四、设计模式
    • 五、参考

Mybatis插件

一、自定义插件

1.1 示例

  • 如下是Mybatis分页插件里面实现的一个插件,我们从这里开始看实现自定义插件的方法
/**
 * QueryInterceptor 规范
 * 详细说明见文档:https://github.com/pagehelper/Mybatis-PageHelper/blob/master/wikis/zh/Interceptor.md
 * @author liuzh/abel533/isea533
 * @version 1.0.0
 */
@Intercepts(
    {
        @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
        @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}),
    }
)
public class QueryInterceptor implements Interceptor {

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        Object[] args = invocation.getArgs();
        MappedStatement ms = (MappedStatement) args[0];
        Object parameter = args[1];
        RowBounds rowBounds = (RowBounds) args[2];
        ResultHandler resultHandler = (ResultHandler) args[3];
        Executor executor = (Executor) invocation.getTarget();
        CacheKey cacheKey;
        BoundSql boundSql;
        //由于逻辑关系,只会进入一次
        if(args.length == 4){
            //4 个参数时
            boundSql = ms.getBoundSql(parameter);
            cacheKey = executor.createCacheKey(ms, parameter, rowBounds, boundSql);
        } else {
            //6 个参数时
            cacheKey = (CacheKey) args[4];
            boundSql = (BoundSql) args[5];
        }
        //TODO 自己要进行的各种处理
        //注:下面的方法可以根据自己的逻辑调用多次,在分页插件中,count 和 page 各调用了一次
        return executor.query(ms, parameter, rowBounds, resultHandler, cacheKey, boundSql);
    }

    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {
    }

}

1.2 要点

  • 要点1:通过注解给出拦截器需要拦截类型和需要拦截的方法,默认四种接口类型包括Executor、StatementHandler、ParameterHandler和ResultSetHandler自定义拦截器必须使用Mybatis提供的注解来指明我们要拦截的是四类中的哪一个类接口。
Signature指明拦截器需要拦截哪一个接口的哪一个方法,内部type对应四类接口中的某一个(比如是 Executor),
method对应接口中的方法 , args对应方法参数(重载)
  • 要点2:实现Interceptor接口中的方法。
Interceptor接口的三个方法中, intercept实现插件的逻辑, plugin返回一个插件代理对象(插件的实现是基于动态代理的),setProperties是属性赋值方法。
对于plugin方法因为实现起来比较复杂,实际上Mybatis已经为我们提供了一种实现,只需要简单调用Plugin.wrap(target, this)方法即可,如此就实现了一个插件。
  • 要点3:拦截器顺序。
不同类型拦截器顺序:Executor -> ParameterHandler -> StatementHandler -> ResultSetHandler
同一个类型拦截器顺序: 根据Mybatis核心配置文件配置的位置,从上往下 

二、插件的配置和解析

2.1 配置

  • 插件的使用如下,只需要在核心配置文件中配置插件即可
 
    
        
            
            
        
    

2.2 解析

2.2.1 XMLConfigBuilder#parseConfiguration

  • Mybatis的插件在核心配置文件中配置,而核心配置文件的解析是主流程的第一阶段,可以参考16-Mybatis 核心流程01-初始化阶段。我们直接从主配置文件的解析方法:XMLConfigBuilder#parseConfiguration开始看,下面省去了其他的代码,从中可以看到解析插件的入口方法是XMLConfigBuilder#pluginElement
/**
   * 解析核心配置文件的关键方法,
   * 读取节点的信息,并通过对应的方法去解析配置,解析到的配置全部会放在configuration里面
   * */
private void parseConfiguration(XNode root) {
    try {
      Properties settings = settingsAsPropertiess(root.evalNode("settings"));
      
      //省略....
      
      //解析节点
      pluginElement(root.evalNode("plugins"));
           
      //省略....
           
    } catch (Exception e) {
      throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
    }
  }

2.2.2 XMLConfigBuilder#pluginElement

  • XMLConfigBuilder#pluginElement中完成插件的对象的实例化,赋值,并保存到Configuration对象里面的插件链中
  /**
   * 解析插件
   * */
  private void pluginElement(XNode parent) throws Exception {
    if (parent != null) {
        //1.遍历处理每个节点
      for (XNode child : parent.getChildren()) {
          //2.获取interceptor插件名称,实际上是全限定类名
        String interceptor = child.getStringAttribute("interceptor");
        //3.获取配置的属性
        Properties properties = child.getChildrenAsProperties();
        //4.反射获取到这个类的实例
        Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).newInstance();
        //5.给实例赋配置的属性值
        interceptorInstance.setProperties(properties);
        //6.保存到Configuration中的插件链对象
        //protected final InterceptorChain interceptorChain = new InterceptorChain();
        configuration.addInterceptor(interceptorInstance);
      }
    }
  }
  
  //下面是Configuration中的属性,保存全部插件
  protected final InterceptorChain interceptorChain = new InterceptorChain();
  • 由2.2这里面的2段代码我们看到,在配置解析阶段就会把插件都放到Configuration对象的interceptorChain属性中去,由此解析完成,后面就是代理阶段来生成相关的类实现功能增强。

三、代理

3.1 代理增强

  • 前面提到过,插件是基于动态代理来实现的,假设我有一个插件需要对StatementHandler进行一定的处理,那么就会生成一个代理对象,这个代理对象在原有StatementHandler的功能基础上,添加了我这个插件想要做的工作。这个生成代理的过程实际上是在四种类型的对象初始化的时候做的,我们看下面源码(Configuration.java)

3.1.1 Configuration

  • Configuration在配置解析时获取到了全部插件对象实例,在创建四大对象的时候,这些插件实例会排上用场。
/**
     * 在创建ParameterHandler对象的时候,会调用interceptorChain.pluginAll来获取一个动态代理后的对象,实际返回的
     * 是一个增强后的动态代理对象,不是一个最原始的ParameterHandler对象
     * 这个代理对象描述如下:
     * 1.代理对象是一个Plugin类的对象。从interceptorChain.pluginAll->interceptor.plugin->Plugin.wrap调用链跟踪下去可以看到
     * 最后返回的是Proxy.newProxyInstance(type.getClassLoader(),interfaces, new Plugin(target, interceptor, signatureMap));
     * 由此可以看出。
     * 2.如果包含多个插件,这个代理对象是经过多层代理增强的。在pluginAll方法里面会依次遍历所有的插件,针对每个插件会生成一个增强的代理对象,
     * 然后把这个代理对象交给下一个插件做增强,也就是有多层包装(有点像装饰器那种多层装饰的感觉)
     * 3.假如没有插件,那么方法返回的就是原始对象,假如多个插件只有一部分对ParameterHandler需要增强,那么这个逻辑也会在plugin中进行处理,
     * 并不是说几个插件就代理几次,会按照需要,这些信息在插件的Intercepts注解上会定义好
     * 4.后面的其余三个对象原理也是一样。(ResultSetHandler,StatementHandler和Executor)
     *
     * */
    public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {
        ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement, parameterObject, boundSql);
        parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler);
        return parameterHandler;
    }

    public ResultSetHandler newResultSetHandler(Executor executor, MappedStatement mappedStatement, RowBounds rowBounds, ParameterHandler parameterHandler,
                                                ResultHandler resultHandler, BoundSql boundSql) {
        ResultSetHandler resultSetHandler = new DefaultResultSetHandler(executor, mappedStatement, parameterHandler, resultHandler, boundSql, rowBounds);
        resultSetHandler = (ResultSetHandler) interceptorChain.pluginAll(resultSetHandler);
        return resultSetHandler;
    }

    public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
        StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql);
        statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
        return statementHandler;
    }

    public Executor newExecutor(Transaction transaction) {
        return newExecutor(transaction, defaultExecutorType);
    }

    public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
        executorType = executorType == null ? defaultExecutorType : executorType;
        executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
        Executor executor;
        if (ExecutorType.BATCH == executorType) {
            executor = new BatchExecutor(this, transaction);
        } else if (ExecutorType.REUSE == executorType) {
            executor = new ReuseExecutor(this, transaction);
        } else {
            executor = new SimpleExecutor(this, transaction);
        }
        if (cacheEnabled) {
            executor = new CachingExecutor(executor);
        }
        executor = (Executor) interceptorChain.pluginAll(executor);
        return executor;
    }

3.1.2 InterceptorChain

  • 在前面的3.1.1中获取代理对象调用的是InterceptorChain#pluginAll,我们来看看这个里面的细节。
/**
 * 保存所有的Mybatis插件,Configuration对象中持有InterceptorChain实例,保存全部的插件。
 * 该类主要提供2个作用:
 * 1.提供添加插件的功能。添加功能在解析配置的时候会用到,将解析后的插件保存到一个集合里面
 * 2.提供获取增强后的对象的功能。这个功能在实例化四大对象的时候会调用。pluginAll方法会对传入的对象进行代理增强,
 * 每一次plugin都是一个代理的过程,前一个插件获取的代理后的结果是后一个插件进行增强的入参,最终返回的对象包含全
 * 部插件的增强。
 * @author Clinton Begin
 */
public class InterceptorChain {

  //1.内部用于保存插件的list
  private final List<Interceptor> interceptors = new ArrayList<Interceptor>();

  //2.依次调用每个插件,让每个插件对target对象进行包装代理
  //比如传进来的是一个parameterHandler,现在有三个插件interceptor1,interceptors2,interceptors3
  //那么这里第一步会先调用interceptor1.plugin(parameterHandler),这个过程就会把parameterHandler进行一次代理,然后将代理对象返回,
  //(如果不需要代理就会返回原来的对象,这个过程在plugin方法中会进行处理)
  //然后在将第一步得到的对象应用于interceptors2,interceptors3...依次处理
  //处理完毕之后获得的就是一个原始target对象的代理对象,Mybatis提供了一个简单的获取代理对象的方法Plugin.wrap(target, this);
  //因此很多插件的plugin方法都是调用的这个方法
  public Object pluginAll(Object target) {
    for (Interceptor interceptor : interceptors) {
      target = interceptor.plugin(target);
    }
    return target;
  }

  //添加插件的方法
  public void addInterceptor(Interceptor interceptor) {
    interceptors.add(interceptor);
  }
  
 //省略interceptors的get方法
}
  • 由此我们看到,在初始化的时候会调用interceptorChain.pluginAll(object) 来获取增强后的object(Object属于四类中的某一类)。

3.2 Plugin类

3.2.1 细节

  • 上面我们看到了初始化过程的代理增强对象的获取,不过我们没有看增强对象的生成细节,怎么增强的?我们知道插件的逻辑是在intercept中实现的,在1.2的要点2我们提到,plugin方法是用于返回增强的代理对象,那么我们先看看已有的插件这里是如何实现的。在1.1的分页插件中plugin方法是调用Plugin.wrap(target, this),在Mybatis源码工程中的插件测试代码ExamplePlugin中也是 return Plugin.wrap(target, this);如下所示:
@Intercepts({})
public class ExamplePlugin implements Interceptor { 

  @Override
  public Object plugin(Object target) {
    return Plugin.wrap(target, this);
  }
  //省略其他...
}
  • 那么我们就想知道这个方法里面做了什么,下面分析。

3.2.2 Plugin类

  • Plugin类使用了动态代理模式,它实现了InvocationHandler接口,内部持有Interceptor插件对象和目标对象,实际程序运行时期的对象就是一个Plugin实例,它会判断什么时候该执行插件的intercept方法,带注释源码如下:
/**
 * 插件的代理类,主要负责:
 * 1.生成插件拦截器对象Interceptor的代理对象(wrap方法)。简化插件开发者自行实现插件代理对象的逻辑,(自行实现的话,需要根
 * 据插件的Intercepts注解里面的信息,根据需要代理的类,方法来插件一个代理对象,这个过程比较繁琐,因此Mybatis简化了这个逻辑,
 * 在wrap方法中帮助我们实现了)
 * 2.插件的调用。插件的调用就是调用interceptor的intercept方法,在invoke方法中实现。invoke调用方法时会判断,方法被拦截的情
 * 况则会调用插件的interceptor方法,方法不需要被拦截那就直接调用目标对象的方法。在Plugin实例创建的时候,就已经把需要拦截的全
 * 部方法保存到signatureMap里面了,因此很容易判断出来。
 * @author Clinton Begin
 */
public class Plugin implements InvocationHandler {

    private Object target;
    private Interceptor interceptor;
    private Map<Class<?>, Set<Method>> signatureMap;

    private Plugin(Object target, Interceptor interceptor, Map<Class<?>, Set<Method>> signatureMap) {
        this.target = target;
        this.interceptor = interceptor;
        this.signatureMap = signatureMap;
    }

    /**
     * 返回一个代理增强后的对象,这个对象有以下特点:
     * 1.这个对象内部持有的InvocationHandler是一个Plugin,表面来看是对这个Plugin进行的代理增强,但是实际上Plugin只是
     * 一个壳子,Plugin内部封装了3个信息,一个target代表真正被代理的目标对象,interceptor代表了插件对象,signatureMap
     * 代表了插件要拦截的方法集合(key是四大对象的类对象,value是需要拦截的方法集合)。
     * 2.返回的代理对象真正代理的是target对象,从classLoad和interfaces参数可以知道。
     * 3.这个代理对象内部即持有目标对象target,又持有插件对象interceptor(因为Plugin其实就是对这二者的封装)
     * */
    public static Object wrap(Object target, Interceptor interceptor) {
        //1.signatureMap包含插件需要拦截的全部方法,Class对象为key,方法集合为value
        Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
        Class<?> type = target.getClass();
        //2.获取目标类的需要被代理的接口。比如type是StatementHandler实现类,它一共实现了100个接口,但是我需要拦截的只有1个接口,
        // 那么只返回1个。为什么呢?因为动态代理生成的代理对象是需要实现这些接口的(目标类实现了什么接口,代理类也要实现对应的接口,
        // 才能实现动态代理),既然只需要增强1个接口的方法那就只实现这1个接口就好了,没有必要全部实现
        Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
        if (interfaces.length > 0) {
            return Proxy.newProxyInstance(
                    type.getClassLoader(),
                    interfaces,
                    new Plugin(target, interceptor, signatureMap));
        }
        return target;
    }

    /**
     * 代理对象被调用时走的方法。
     * 因为代理对象即持有目标对象(比如StatementHandler或者Executor),又持有插件和插件要拦截的方法列表,因此
     * 调用方法的时候要判断是不是要拦截这个方法,如果在拦截列表就要。
     * */
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        try {
            //1.signatureMap是插件的方法集合,先判断需要调用的方法是不是插件的方法。比如method是StatementHandler的一个方法A,那么
            //这里就会是get(StatementHandler.class),如果拦截器声明了拦截了StatementHandler的B,C方法,那么就会获取到这个包含B,C
            //方法的集合,再来判断,发现不包含A,那么就执行最后面的逻辑直接目标对象调用,如果method是B方法,那么就会走if的逻辑,在
            //intercept方法里面调用我们拦截器的逻辑,至于拦截器里面还要不要调用目标方法,这个是在拦截器里面控制的,因此2和3的逻辑是
            //互斥的,并不需要调用2之后还调用3
            Set<Method> methods = signatureMap.get(method.getDeclaringClass());
            //2.这说明是插件的方法,那么就调用插件的
            if (methods != null && methods.contains(method)) {
                return interceptor.intercept(new Invocation(target, method, args));
            }
            //3.如果不是插件的方法,说明是目标对象自己的方法,比如是StatementHandler对象自己的一个方法,直接调用即可
            return method.invoke(target, args);
        } catch (Exception e) {
            throw ExceptionUtil.unwrapThrowable(e);
        }
    }

    /**
     * 获取Interceptor插件对象的方法列表。以Class对象为key,方法集合的set为value保存到Map中。
     * 这个set里面保存的方法是需要插件拦截的方法
     * */
    private static Map<Class<?>, Set<Method>> getSignatureMap(Interceptor interceptor) {
        Intercepts interceptsAnnotation = interceptor.getClass().getAnnotation(Intercepts.class);
        // issue #251
        //1.插件需要注解,没有注解抛异常
        if (interceptsAnnotation == null) {
            throw new PluginException("No @Intercepts annotation was found in interceptor " + interceptor.getClass().getName());
        }
        //2.获取Signature数组
        Signature[] sigs = interceptsAnnotation.value();
        Map<Class<?>, Set<Method>> signatureMap = new HashMap<Class<?>, Set<Method>>();
        //3.遍历处理每一个Signature
        for (Signature sig : sigs) {
            //4.第一次get肯定没有,就初始化里面的方法集合
            Set<Method> methods = signatureMap.get(sig.type());
            if (methods == null) {
                methods = new HashSet<Method>();
                signatureMap.put(sig.type(), methods);
            }
            //5.初始化完了到这一步,会获取对应的类型的全部方法和方法参数,封装成一个Method对象保存到method集合
            try {
                Method method = sig.type().getMethod(sig.method(), sig.args());
                methods.add(method);
            } catch (NoSuchMethodException e) {
                throw new PluginException("Could not find method on " + sig.type() + " named " + sig.method() + ". Cause: " + e, e);
            }
        }
        //6.最后返回的signatureMap就是以Class对象为key,方法列表为value的一个Map,这里的方法都是插件上的注解声明需要拦截的方法
        return signatureMap;
    }

    /**
     *返回type类型的所有方法中,那些需要被拦截的方法
     */
    private static Class<?>[] getAllInterfaces(Class<?> type, Map<Class<?>, Set<Method>> signatureMap) {
        Set<Class<?>> interfaces = new HashSet<Class<?>>();
        while (type != null) {
            //1.遍历type类型的全部方法,包括父类的方法
            for (Class<?> c : type.getInterfaces()) {
                //2.只有需要拦截的方法,才返回
                if (signatureMap.containsKey(c)) {
                    interfaces.add(c);
                }
            }
            //3.处理完子类,递归处理父类
            type = type.getSuperclass();
        }
        //4.返回
        return interfaces.toArray(new Class<?>[interfaces.size()]);
    }
}

3.3 小结

  • 我们梳理一下整个插件工作的机制
A.首先是配置解析。解析完毕之后所有的插件都会被实例化,保存到Configuration对象的interceptorChain属性里面(XMLConfigBuilder#pluginElement)
B.第二是初始化。四大对象的初始化都在Configuration中,初始化的时候会通过interceptorChain将四大对象进行增强(Configuration四大对象构造方法)
 B1 这个增强获得的代理对象是Plugin类的实例,该实例内部持有四大对象和插件对象实例。(Plugin#wrap)
 B2 增强对象初始化的时候就会把需要拦截的方法保存好,便于后面真正调用方法的时候判断(Plugin#wrap第一行代码)
 B3 增强对象调用方法的时候会判断这个方法是否需要拦截,是就走插件对象的intercept方法,不需要就直接调用四大对象自己的方法(Plugin#invoke)
 B4 增强对象很可能是一个多层的代理包装,每一个插件会代理一次(InterceptorChain#pluginAll)
 B5 对B1的过程,返回代理对象是在Interceptor#plugin中需要用户自行完成的事情,需要根据插件注解等信息实现返回代理对象的逻辑,贴心的Mybatis已经在(Plugin#wrap   )中实现好了,直接使用即可。(从入参Plugin.wrap(target, this)也可以看出,代理对象是持有目标对象和插件对象实例的,印证了B1的说法)
C.完成了B的初始化之后,程序运行阶段,四大对象就不再是原始的四大对象,而是经过了增强实现了对应插件功能的代理对象了。

四、设计模式

  • 文章标题写了代理模式和责任链模式。代理模式其实在框架中几乎是无处不在,Mybatis中也用的非常多,责任链模式体现在InterceptorChain#pluginAll方法中,对四大对象目标类让插件进行逐个增强,不过感觉不是很明显,具体关于责任链模式可以参考: 03-行为型模式(上)

五、参考

  • [1] 真正理解mybatis拦截器以及Interceptor和Plugin作用

你可能感兴趣的:(Mybatis)