• 送命题:讲一讲Mybatis插件的原理及如何实现?


    持续原创输出,点击上方蓝字关注我吧

    目录

    • 前言
    • 环境配置
    • 什么是插件?
    • 如何自定义插件?
      • 举个栗子
      • 用到哪些注解?
      • 如何注入Mybatis?
      • 测试
    • 插件原理分析
      • 如何生成代理对象?
      • 如何执行?
      • 总结
    • 分页插件的原理分析
    • 总结

    前言

    • Mybatis的分页插件相信大家都使用过,那么可知道其中的实现原理?分页插件就是利用的Mybatis中的插件机制实现的,在Executorquery执行前后进行分页处理。
    • 此篇文章就来介绍以下Mybatis的插件机制以及在底层是如何实现的。

    环境配置

    • 本篇文章讲的一切内容都是基于Mybatis3.5SpringBoot-2.3.3.RELEASE

    什么是插件?

    • 插件是Mybatis中的最重要的功能之一,能够对特定组件的特定方法进行增强。
    • MyBatis 允许你在映射语句执行过程中的某一点进行拦截调用。默认情况下,MyBatis 允许使用插件来拦截的方法调用包括:
      • Executorupdate, query, flushStatements, commit, rollback, getTransaction, close, isClosed
      • ParameterHandler: getParameterObject, setParameters
      • ResultSetHandlerhandleResultSets, handleOutputParameters
      • StatementHandler: prepare, parameterize, batch, update, query

    如何自定义插件?

    • 插件的实现其实很简单,只需要实现Mybatis提供的Interceptor这个接口即可,源码如下:
    public interface Interceptor {
      //拦截的方法
      Object intercept(Invocation invocation) throws Throwable;
      //返回拦截器的代理对象
      Object plugin(Object target);
      //设置一些属性
      void setProperties(Properties properties);
    
    }
    

    举个栗子

    • 有这样一个需求:需要在Mybatis执行的时候篡改selectByUserId的参数值。
    • 分析:修改SQL的入参,应该在哪个组件的哪个方法上拦截篡改呢?研究过源码的估计都很清楚的知道,ParameterHandler中的setParameters()方法就是对参数进行处理的。因此肯定是拦截这个方法是最合适。
    • 自定义的插件如下:
    /**
     * @Intercepts 注解标记这是一个拦截器,其中可以指定多个@Signature
     * @Signature 指定该拦截器拦截的是四大对象中的哪个方法
     *      type:拦截器的四大对象的类型
     *      method:拦截器的方法,方法名
     *      args:入参的类型,可以是多个,根据方法的参数指定,以此来区分方法的重载
     */
    @Intercepts(
            {
                    @Signature(type = ParameterHandler.class,method ="setParameters",args = {PreparedStatement.class})
            }
    )
    public class ParameterInterceptor implements Interceptor {
        @Override
        public Object intercept(Invocation invocation) throws Throwable {
            System.out.println("拦截器执行:"+invocation.getTarget());
            //目标对象
            Object target = invocation.getTarget();
            //获取目标对象中所有属性的值,因为ParameterHandler使用的是DefaultParameterHandler,因此里面的所有的属性都封装在其中
            MetaObject metaObject = SystemMetaObject.forObject(target);
            //使用xxx.xxx.xx的方式可以层层获取属性值,这里获取的是mappedStatement中的id值
            String value = (String) metaObject.getValue("mappedStatement.id");
            //如果是指定的查询方法
            if ("cn.cb.demo.dao.UserMapper.selectByUserId".equals(value)){
                //设置参数的值是admin_1,即是设置id=admin_1,因为这里只有一个参数,可以这么设置,如果有多个需要需要循环
                metaObject.setValue("parameterObject", "admin_1");
            }
            //执行目标方法
            return invocation.proceed();
        }
    
    
        @Override
        public Object plugin(Object target) {
            //如果没有特殊定制,直接使用Plugin这个工具类返回一个代理对象即可
            return Plugin.wrap(target, this);
        }
    
        @Override
        public void setProperties(Properties properties) {
        }
    }
    
    • intercept方法:最终会拦截的方法,最重要的一个方法。
    • plugin方法:返回一个代理对象,如果没有特殊要求,直接使用Mybatis的工具类Plugin返回即可。
    • setProperties:设置一些属性,不重要。

    用到哪些注解?

    • 自定义插件需要用到两个注解,分别是@Intercepts@Signature
    • @Intercepts:标注在实现类上,表示这个类是一个插件的实现类。
    • @Signature:作为@Intercepts的属性,表示需要增强Mybatis的某些组件中的某些方法(可以指定多个)。常用的属性如下:
      • Class<?> type():指定哪个组件(ExecutorParameterHandlerResultSetHandlerStatementHandler
      • String method():指定增强组件中的哪个方法,直接写方法名称。
      • Class<?>[] args():方法中的参数,必须一一对应,可以写多个;这个属性非常重用,区分重载方法。

    如何注入Mybatis?

    • 上面已经将插件定义好了,那么如何注入到Mybatis中使其生效呢?

    • 前提:由于本篇文章的环境是SpringBoot+Mybatis,因此讲一讲如何在SpringBoot中将插件注入到Mybatis中。

    • 在Mybatis的自动配置类MybatisAutoConfiguration中,注入SqlSessionFactory的时候,有如下一段代码:

    • 上图中的this.interceptors是什么,从何而来,其实就是从容器中的获取的Interceptor[],如下一段代码: 2

    • 从上图我们知道,这插件最终还是从IOC容器中获取的Interceptor[]这个Bean,因此我们只需要在配置类中注入这个Bean即可,如下代码:

    /**
     * @Configuration:这个注解标注该类是一个配置类
     */
    @Configuration
    public class MybatisConfig{
    
        /**
         * @Bean : 该注解用于向容器中注入一个Bean
         * 注入Interceptor[]这个Bean
         * @return
         */
        @Bean
        public Interceptor[] interceptors(){
            //创建ParameterInterceptor这个插件
            ParameterInterceptor parameterInterceptor = new ParameterInterceptor();
            //放入数组返回
            return new Interceptor[]{parameterInterceptor};
        }
    }
    

    测试

    • 此时自定义的插件已经注入了Mybatis中了,现在测试看看能不能成功执行呢?测试代码如下:
        @Test
        void contextLoads() {
          //传入的是1222
            UserInfo userInfo = userMapper.selectByUserId("1222");
            System.out.println(userInfo);
    
        }
    
    • 测试代码传入的是1222,由于插件改变了入参,因此查询出来的应该是admin_1这个人。

    插件原理分析

    • 插件的原理其实很简单,就是在创建组件的时候生成代理对象(Plugin),执行组件方法的时候拦截即可。下面就来详细介绍一下插件在Mybatis底层是如何工作的?
    • Mybatis的四大组件都是在Mybatis的配置类Configuration中创建的,具体的方法如下:
    
    //创建Executor
    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);
        }
        //调用pluginAll方法,生成代理对象
        executor = (Executor) interceptorChain.pluginAll(executor);
        return executor;
      }
      
      //创建ParameterHandler
      public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {
        ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement, parameterObject, boundSql);
        //调用pluginAll方法,生成代理对象
        parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler);
        return parameterHandler;
      }
    
    //创建ResultSetHandler
      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);
        //调用pluginAll方法,生成代理对象
        resultSetHandler = (ResultSetHandler) interceptorChain.pluginAll(resultSetHandler);
        return resultSetHandler;
      }
      
      //创建StatementHandler
      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);
        //调用pluginAll方法,生成代理对象
        statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
        return statementHandler;
      }
    
    • 从上面的源码可以知道,创建四大组件的方法中都会执行pluginAll()这个方法来生成一个代理对象。具体如何生成的,下面详解。

    如何生成代理对象?

    • 创建四大组件过程中都执行了pluginAll()这个方法,此方法源码如下:
    public Object pluginAll(Object target) {
        //循环遍历插件
        for (Interceptor interceptor : interceptors) {
          //调用插件的plugin()方法
          target = interceptor.plugin(target);
        }
        //返回
        return target;
      }
    
    • pluginAll()方法很简单,直接循环调用插件的plugin()方法,但是我们调用的是Plugin.wrap(target, this)这行代码,因此要看一下wrap()这个方法的源码,如下:
    public static Object wrap(Object target, Interceptor interceptor) {
        //获取注解的@signature的定义
        Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
        //目标类
        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.wrap()这个方法的逻辑很简单,判断这个插件是否是拦截对应的组件,如果拦截了,生成代理对象(Plugin)返回,没有拦截直接返回,上面例子中生成的代理对象如下图:

    如何执行?

    • 上面讲了Mybatis启动的时候如何根据插件生成代理对象的(Plugin)。现在就来看看这个代理对象是如何执行的?
    • 既然是动态代理,肯定会执行的invoke()这个方法,Plugin类中的invoke()源码如下:
    @Override
      public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        try {
          //获取@signature标注的方法
          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);
        } catch (Exception e) {
          throw ExceptionUtil.unwrapThrowable(e);
        }
      }
    
    • 逻辑很简单,这个方法被拦截了就执行插件的intercept()方法,没有被拦截,则执行原方法。
    • 还是以上面自定义的插件来看看执行的流程:
      • setParameters()这个方法在PreparedStatementHandler中被调用,如下图:
      • 执行invoke()方法,发现setParameters()这个方法被拦截了,因此直接执行的是intercept()方法。

    总结

    • Mybatis中插件的原理其实很简单,分为以下几步:
      1. 在项目启动的时候判断组件是否有被拦截,如果没有直接返回原对象。
      2. 如果有被拦截,返回动态代理的对象(Plugin)。
      3. 执行到的组件的中的方法时,如果不是代理对象,直接执行原方法
      4. 如果是代理对象,执行Plugininvoke()方法。

    分页插件的原理分析

    • 此处安利一款经常用的分页插件pagehelper,Maven依赖如下:
            <dependency>
                <groupId>com.github.pagehelper</groupId>
                <artifactId>pagehelper</artifactId>
                <version>5.1.6</version>
            </dependency>
    
    • 分页插件很显然也是根据Mybatis的插件来定制的,来看看插件PageInterceptor的源码如下:
    @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 PageInterceptor implements Interceptor {}
    
    • 既然是分页功能,肯定是在query()的时候拦截,因此肯定是在Executor这个组件中。
    • 分页插件的原理其实很简单,不再一一分析源码了,根据的自己定义的分页数据重新赋值RowBounds来达到分页的目的,当然其中涉及到数据库方言等等内容,不是本章重点,有兴趣可以看一下GitHub上的文档

    总结

    • 对于业务开发的程序员来说,插件的这个功能很少用到,但是不用就不应该了解吗?做人要有追求,哈哈。
    • 欢迎关注作者的微信公众号码猿技术专栏,作者为你们精心准备了springCloud最新精彩视频教程精选500本电子书架构师免费视频教程等等免费资源,让我们一起进阶,一起成长。
    如果觉得作者写的好,有所收获的话,点个关注,推荐一波,文章首发于公众号!!!
  • 相关阅读:
    【刷题】洛谷 P2764 最小路径覆盖问题
    【刷题】BZOJ 3546 [ONTAK2010]Life of the Party
    【刷题】BZOJ 3175 [Tjoi2013]攻击装置
    【刷题】BZOJ 4516 [Sdoi2016]生成魔咒
    【刷题】SPOJ 1811 LCS
    【刷题】洛谷 P3804 【模板】后缀自动机
    【刷题】SPOJ 8222 NSUBSTR
    (98)Wangdao.com_第三十天_拖拉事件
    ECMA Script 6_必须要知道的基础
    (97)Wangdao.com_第三十天_触摸事件
  • 原文地址:https://www.cnblogs.com/Chenjiabing/p/13683273.html
Copyright © 2020-2023  润新知