2023-06-06  阅读(2)
原文作者:惑边 原文地址:https://blog.csdn.net/my_momo_csdn

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 配置

  • 插件的使用如下,只需要在核心配置文件中配置插件即可
     <!--配置分页插件-->
        <plugins>
            <plugin interceptor="com.github.pagehelper.PageInterceptor">
                <!-- config params as the following -->
                <!--<property name="param1" value="value1"/>-->
            </plugin>
        </plugins>

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"));
          
          //省略....
          
          //解析<plugins>节点
          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-行为型模式(上)

五、参考


Java 面试宝典是大明哥全力打造的 Java 精品面试题,它是一份靠谱、强大、详细、经典的 Java 后端面试宝典。它不仅仅只是一道道面试题,而是一套完整的 Java 知识体系,一套你 Java 知识点的扫盲贴。

它的内容包括:

  • 大厂真题:Java 面试宝典里面的题目都是最近几年的高频的大厂面试真题。
  • 原创内容:Java 面试宝典内容全部都是大明哥原创,内容全面且通俗易懂,回答部分可以直接作为面试回答内容。
  • 持续更新:一次购买,永久有效。大明哥会持续更新 3+ 年,累计更新 1000+,宝典会不断迭代更新,保证最新、最全面。
  • 覆盖全面:本宝典累计更新 1000+,从 Java 入门到 Java 架构的高频面试题,实现 360° 全覆盖。
  • 不止面试:内容包含面试题解析、内容详解、知识扩展,它不仅仅只是一份面试题,更是一套完整的 Java 知识体系。
  • 宝典详情:https://www.yuque.com/chenssy/sike-java/xvlo920axlp7sf4k
  • 宝典总览:https://www.yuque.com/chenssy/sike-java/yogsehzntzgp4ly1
  • 宝典进展:https://www.yuque.com/chenssy/sike-java/en9ned7loo47z5aw

目前 Java 面试宝典累计更新 400+ 道,总字数 42w+。大明哥还在持续更新中,下图是大明哥在 2024-12 月份的更新情况:

想了解详情的小伙伴,扫描下面二维码加大明哥微信【daming091】咨询

同时,大明哥也整理一套目前市面最常见的热点面试题。微信搜[大明哥聊 Java]或扫描下方二维码关注大明哥的原创公众号[大明哥聊 Java] ,回复【面试题】 即可免费领取。

阅读全文