美文网首页Mybatis
MyBatis原理系列(七)-手把手带你了解如何自定义插件

MyBatis原理系列(七)-手把手带你了解如何自定义插件

作者: Renaissance_ | 来源:发表于2020-12-30 11:23 被阅读0次

    MyBatis原理系列(一)-手把手带你阅读MyBatis源码
    MyBatis原理系列(二)-手把手带你了解MyBatis的启动流程
    MyBatis原理系列(三)-手把手带你了解SqlSession,SqlSessionFactory,SqlSessionFactoryBuilder的关系
    MyBatis原理系列(四)-手把手带你了解MyBatis的Executor执行器
    MyBatis原理系列(五)-手把手带你了解Statement、StatementHandler、MappedStatement间的关系
    MyBatis原理系列(六)-手把手带你了解BoundSql的创建过程
    MyBatis原理系列(七)-手把手带你了解如何自定义插件
    MyBatis原理系列(八)-手把手带你了解一级缓存和二级缓存
    MyBatis原理系列(九)-手把手带你了解MyBatis事务管理机制

    MyBatis的一个重要的特点就是插件机制,使得MyBatis的具备较强的扩展性,我们可以根据MyBatis的插件机制实现自己的个性化业务需求。

    1. 初识插件

    我们在执行查询的时候,如果sql没有加上分页条件,数据量过大的话会造成内存溢出,因此我们可以通过MyBatis提供的插件机制来拦截sql,并进行sql改写。MyBatis的插件是通过动态代理来实现的,并且会形成一个插件链。原理类似于拦截器,拦截我们需要处理的对象,进行自定义逻辑后,返回一个代理对象,进行下一个拦截器的处理。

    我们先来看下一个简单插件的模板,首先要实现一个Interceptor接口,并实现三个方法。并加上@Intercepts注解。接下来我们以分页插件为例将对每个细节进行讲解。

    /**
     * @ClassName : PagePlugin
     * @Description : 分页插件
     * @Date: 2020/12/29
     */
    @Intercepts({})
    public class PagePlugin implements Interceptor {
        
        private Properties properties;
        
        @Override
        public Object intercept(Invocation invocation) throws Throwable {
            return invocation.proceed();
        }
    
        @Override
        public Object plugin(Object target) {
            return Plugin.wrap(target, this);
        }
    
        @Override
        public void setProperties(Properties properties) {
            this.properties = properties;
        }
    }
    

    2. 拦截对象

    在进行插件创建的时候,需要指定拦截对象。@Intercepts注解指定需要拦截的方法签名,内容是个Signature类型的数组,而Signature就是对拦截对象的描述。

    @Documented
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.TYPE)
    public @interface Intercepts {
      /**
       * Returns method signatures to intercept.
       *
       * @return method signatures
       */
      Signature[] value();
    }
    

    Signature 需要指定拦截对象中方法的信息的描述。

    @Documented
    @Retention(RetentionPolicy.RUNTIME)
    @Target({})
    public @interface Signature {
      /**
       * 对象类型
       */
      Class<?> type();
    
      /**
       * 方法名
       */
      String method();
    
      /**
       * 参数类型
       */
      Class<?>[] args();
    }
    

    在MyBatis中,我们只能对以下四种类型的对象进行拦截

    • ParameterHandler : 对sql参数进行处理
    • ResultSetHandler : 对结果集对象进行处理
    • StatementHandler : 对sql语句进行处理
    • Executor : 执行器,执行增删改查

    现在我们需要对sql进行改写,因此可以需要拦截Executor的query方法进行拦截

    @Intercepts({@Signature(type = Executor.class, 
                            method = "query", 
                            args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})})
    

    3. 拦截实现

    每个插件除了指定拦截的方法后,还需要实现Interceptor接口。Interceptor接口有以下三个方法。其中intercept是我们必须要实现的方法,在这里面我们需要实现自定义逻辑。其它两个方法给出了默认实现。

    public interface Interceptor {
    
      /**
       * 进行拦截处理
       * @param invocation
       * @return
       * @throws Throwable
       */
      Object intercept(Invocation invocation) throws Throwable;
    
      /**
       * 返回代理对象
       * @param target
       * @return
       */
      default Object plugin(Object target) {
        return Plugin.wrap(target, this);
      }
    
      /**
       * 设置配置属性
       * @param properties
       */
      default void setProperties(Properties properties) {
        // NOP
      }
    
    }
    
    

    因此我们实现intercept方法即可,因为我们要改写查询sql语句,因此需要拦截Executor的query方法,然后修改RowBounds参数中的limit,如果limit大于1000,我们强制设置为1000。

    @Slf4j
    @Intercepts({@Signature(type = Executor.class,
            method = "query",
            args = {MappedStatement.class, Object.class, RowBounds.class , ResultHandler.class})})
    public class PagePlugin implements Interceptor {
    
        private Properties properties;
    
        @Override
        public Object intercept(Invocation invocation) throws Throwable {
            Object[] args = invocation.getArgs();
            RowBounds rowBounds = (RowBounds)args[2];
            log.info("执行前, rowBounds = [{}]", JSONUtil.toJsonStr(rowBounds));
            if(rowBounds != null){
                if(rowBounds.getLimit() > 1000){
                    Field field = rowBounds.getClass().getDeclaredField("limit");
                    field.setAccessible(true);
                    field.set(rowBounds, 1000);
                }
            }else{
                rowBounds = new RowBounds(0 ,100);
                args[2] = rowBounds;
            }
            log.info("执行后, rowBounds = [{}]", JSONUtil.toJsonStr(rowBounds));
            return invocation.proceed();
        }
    
        @Override
        public Object plugin(Object target) {
            return Plugin.wrap(target, this);
        }
    
        @Override
        public void setProperties(Properties properties) {
            this.properties = properties;
        }
    }
    

    4. 加载流程

    以上我们已经实现了一个简单的插件,在执行查询的时候对query方法进行拦截,并且修改分页参数。但是我们现在还没有进行插件配置,只有配置了插件,MyBatis才能启动过程中加载插件。

    4.1 xml配置插件

    在mybatis-config.xml中添加plugins标签,并且配置我们上面实现的plugin.

       <plugins>
            <plugin interceptor="com.example.demo.mybatis.PagePlugin">
            </plugin>
        </plugins>
    
    4.2 XMLConfigBuilder加载插件

    在启动流程中加载插件那篇文章中介绍到SqlSessionFactoryBuilder的build方法,其中XMLConfigBuilder这个解析器中的parse()方法就会读取plugins标签下的插件,并加载Configuration中的InterceptorChain中。

    // SqlSessionFactoryBuilder
    public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
            SqlSessionFactory var5;
            try {
                XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
                var5 = this.build(parser.parse());
            } catch (Exception var14) {
                throw ExceptionFactory.wrapException("Error building SqlSession.", var14);
            } finally {
                ErrorContext.instance().reset();
    
                try {
                    inputStream.close();
                } catch (IOException var13) {
                }
    
            }
    
            return var5;
        }
    

    可见XMLConfigBuilder这个parse()方法就是解析xml中配置的各个标签。

    // XMLConfigBuilder
      public Configuration parse() {
        if (parsed) {
          throw new BuilderException("Each XMLConfigBuilder can only be used once.");
        }
        parsed = true;
        parseConfiguration(parser.evalNode("/configuration"));
        return configuration;
      }
    
      private void parseConfiguration(XNode root) {
        try {
          // issue #117 read properties first
          // 解析properties节点
          propertiesElement(root.evalNode("properties"));
          Properties settings = settingsAsProperties(root.evalNode("settings"));
          loadCustomVfs(settings);
          loadCustomLogImpl(settings);
          typeAliasesElement(root.evalNode("typeAliases"));
          // 记载插件
          pluginElement(root.evalNode("plugins"));
          objectFactoryElement(root.evalNode("objectFactory"));
          objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
          reflectorFactoryElement(root.evalNode("reflectorFactory"));
          settingsElement(settings);
          // read it after objectFactory and objectWrapperFactory issue #631
          environmentsElement(root.evalNode("environments"));
          databaseIdProviderElement(root.evalNode("databaseIdProvider"));
          typeHandlerElement(root.evalNode("typeHandlers"));
          mapperElement(root.evalNode("mappers"));
        } catch (Exception e) {
          throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
        }
      }
    

    XMLConfigBuilder 的pluginElement就是遍历plugins下的plugin加载到interceptorChain中。

      // XMLConfigBuilder
      private void pluginElement(XNode parent) throws Exception {
        if (parent != null) {
          // 遍历每个plugin插件
          for (XNode child : parent.getChildren()) {
            // 读取插件的实现类
            String interceptor = child.getStringAttribute("interceptor");
            // 读取插件配置信息
            Properties properties = child.getChildrenAsProperties();
            // 创建interceptor对象
            Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).getDeclaredConstructor().newInstance();
            interceptorInstance.setProperties(properties);
            // 加载到interceptorChain链中
            configuration.addInterceptor(interceptorInstance);
          }
        }
      }
    

    InterceptorChain 是一个interceptor集合,相当于是一层层包装,后一个插件就是对前一个插件的包装,并返回一个代理对象。

    public class InterceptorChain {
    
      private final List<Interceptor> interceptors = new ArrayList<>();
    
      // 生成代理对象
      public Object pluginAll(Object target) {
        for (Interceptor interceptor : interceptors) {
          target = interceptor.plugin(target);
        }
        return target;
      }
    
      // 将插件加到集合中
      public void addInterceptor(Interceptor interceptor) {
        interceptors.add(interceptor);
      }
    
      public List<Interceptor> getInterceptors() {
        return Collections.unmodifiableList(interceptors);
      }
    
    }
    
    4.3 创建插件对象

    因为我们需要对拦截对象进行拦截,并进行一层包装返回一个代理类,那是什么时候进行处理的呢?以Executor为例,在创建Executor对象的时候,会有以下代码。

      // Configuration
      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;
      }
    

    创建完Executor对象后,就会调用interceptorChain.pluginAll()方法,实际调用的是每个Interceptor的plugin()方法。plugin()就是对目标对象的一个代理,并且生成一个代理对象返回。而Plugin.wrap()就是进行包装的操作。

      // Interceptor
      /**
       * 返回代理对象
       * @param target
       * @return
       */
      default Object plugin(Object target) {
        return Plugin.wrap(target, this);
      }
    

    Plugin的wrap()主要进行了以下步骤:

    • 获取拦截器拦截的方法,以拦截对象为key,拦截方法集合为value
    • 获取目标对象的class对,比如Executor对象
    • 如果拦截器中拦截的对象包含目标对象实现的接口,则返回拦截的接口
    • 创建代理类Plugin对象,Plugin实现了InvocationHandler接口,最终对目标对象的调用都会调用Plugin的invocate方法。
      // Plugin
      public static Object wrap(Object target, Interceptor interceptor) {
        // 获取拦截器拦截的方法,以拦截对象为key,拦截方法为value
        Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
        // 获取目标对象的class对象
        Class<?> type = target.getClass();
        // 如果拦截器中拦截的对象包含目标对象实现的接口,则返回拦截的接口
        Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
        // 如果对目标对象进行了拦截
        if (interfaces.length > 0) {
          // 创建代理类Plugin对象
          return Proxy.newProxyInstance(
              type.getClassLoader(),
              interfaces,
              new Plugin(target, interceptor, signatureMap));
        }
        return target;
      }
    

    5. 例子

    我们已经了解MyBatis插件的配置,创建,实现流程,接下来就以一开始我们提出的例子来介绍实现一个插件应该做哪些。

    5.1 确定拦截对象

    因为我们要对查询sql分页参数进行改写,因此可以拦截Executor的query方法,并进行分页参数的改写

    @Intercepts({@Signature(type = Executor.class,
            method = "query",
            args = {MappedStatement.class, Object.class, RowBounds.class , ResultHandler.class})})
    
    5.2 实现拦截接口

    实现Interceptor接口,并且实现intercept实现我们的拦截逻辑

    @Slf4j
    @Intercepts({@Signature(type = Executor.class,
            method = "query",
            args = {MappedStatement.class, Object.class, RowBounds.class , ResultHandler.class})})
    public class PagePlugin implements Interceptor {
    
        private Properties properties;
    
        @Override
        public Object intercept(Invocation invocation) throws Throwable {
            Object[] args = invocation.getArgs();
            RowBounds rowBounds = (RowBounds)args[2];
            log.info("执行前, rowBounds = [{}]", JSONUtil.toJsonStr(rowBounds));
            if(rowBounds != null){
                if(rowBounds.getLimit() > 1000){
                    Field field = rowBounds.getClass().getDeclaredField("limit");
                    field.setAccessible(true);
                    field.set(rowBounds, 1000);
                }
            }else{
                rowBounds = new RowBounds(0 ,100);
                args[2] = rowBounds;
            }
            log.info("执行后, rowBounds = [{}]", JSONUtil.toJsonStr(rowBounds));
            return invocation.proceed();
        }
    
        @Override
        public Object plugin(Object target) {
            return Plugin.wrap(target, this);
        }
    
        @Override
        public void setProperties(Properties properties) {
            this.properties = properties;
        }
    }
    
    
    5.3 配置插件

    在mybatis-config.xml中配置以下插件

        <plugins>
            <plugin interceptor="com.example.demo.mybatis.PagePlugin">
            </plugin>
        </plugins>
    
    5.4 测试

    com/example/demo/dao/TTestUserMapper.java 新增selectByPage方法

        List<TTestUser> selectByPage(@Param("offset") Integer offset, @Param("pageSize") Integer pageSize);
    
    

    mapper/TTestUserMapper.xml 新增对应的sql

    
      <select id="selectByPage" resultMap="BaseResultMap">
        select
        <include refid="Base_Column_List" />
        from t_test_user
        <if test="offset != null">
          limit #{offset}, #{pageSize}
        </if>
      </select>
    

    最终测试代码,我们没有在查询的时候指定分页参数。

    public static void main(String[] args) {
            try {
                // 1. 读取配置
                InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
                // 2. 创建SqlSessionFactory工厂
                SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
                // 3. 获取sqlSession
                SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.SIMPLE);
                // 4. 获取Mapper
                TTestUserMapper userMapper = sqlSession.getMapper(TTestUserMapper.class);
                // 5. 执行接口方法
                List<TTestUser> list2 = userMapper.selectByPage(null, null);
                System.out.println("list2="+list2.size());
                // 6. 提交事物
                sqlSession.commit();
                // 7. 关闭资源
                sqlSession.close();
                inputStream.close();
            } catch (Exception e){
                log.error(e.getMessage(), e);
            }
        }
    

    最终打印的日志如下,我们可以看到rowBounds已经被我们强制修改了只能查处1000条数据。

    10:11:49.313 [main] INFO com.example.demo.mybatis.PagePlugin - 执行前, rowBounds = [{"offset":0,"limit":2147483647}]
    10:11:58.015 [main] INFO com.example.demo.mybatis.PagePlugin - 执行后, rowBounds = [{"offset":0,"limit":1000}]
    10:12:03.211 [main] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Opening JDBC Connection
    10:12:04.269 [main] DEBUG org.apache.ibatis.datasource.pooled.PooledDataSource - Created connection 749981943.
    10:12:04.270 [main] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@2cb3d0f7]
    10:12:04.283 [main] DEBUG com.example.demo.dao.TTestUserMapper.selectByPage - ==>  Preparing: select id, member_id, real_name, nickname, date_create, date_update, deleted from t_test_user 
    10:12:04.335 [main] DEBUG com.example.demo.dao.TTestUserMapper.selectByPage - ==> Parameters: 
    list2=1000
    

    总结

    本文对MyBatis的插件进行讲解,介绍了每个接口的作用,以及插件的启动,创建,实现的步骤,希望对家能够有所帮助

    相关文章

      网友评论

        本文标题:MyBatis原理系列(七)-手把手带你了解如何自定义插件

        本文链接:https://www.haomeiwen.com/subject/dshqoktx.html