• mybatis源码分析——Plugin的使用以及原理


    一:插件的使用

    以分页插件PageHelper为例,看一下mybatis的插件如何工作

    首先添加pageHelper的maven依赖:

            <dependency>
                <groupId>com.github.pagehelper</groupId>
                <artifactId>pagehelper</artifactId>
                <version>5.1.2</version>
            </dependency>
    

      

    在mybatis-config.xml中配置插件plugins:

    <?xml version="1.0" encoding="UTF-8" ?>
    <!DOCTYPE configuration
            PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
            "http://mybatis.org/dtd/mybatis-3-config.dtd">
    <configuration>
        <!-- 引入外部资源文件
            resource:默认引入classpath路径下的资源文件
            url:引入物理路径下的资源文件(如:d:\jdbc.properties)
         -->
        <properties resource="application.properties"></properties>
        <!-- 设置参数 -->
        <settings>
            <!--  开启驼峰匹配:完成经典的数据库命名到java属性的映射
                              相当于去掉数据中的名字的下划线,和java进行匹配
            -->
            <setting name="mapUnderscoreToCamelCase" value="true"/>
        </settings>
        <!-- 配置别名 -->
        <typeAliases>
            <!-- typeAlias:用来配置别名,方便映射文件使用,type:类的全限定类名,alias:别名 -->
            <typeAlias type="com.example.mybatis.model.User" alias="User"/>
        </typeAliases>
        <plugins>
            <plugin interceptor="com.github.pagehelper.PageInterceptor"/>
            <!--<plugin interceptor="com.example.mybatis.plugin.MyFirstPlugin">
                <property name="someProperty" value="100"/>
            </plugin>-->
        </plugins>
        <!-- 配置环境:可以配置多个环境,default:配置某一个环境的唯一标识,表示默认使用哪个环境 -->
        <environments default="development">
            <!-- 配置环境,id:环境的唯一标识 -->
            <environment id="development">
                <!-- 事务管理器,type:使用jdbc的事务管理器 -->
                <transactionManager type="JDBC" />
                <!-- 数据源,type:池类型的数据源 -->
                <dataSource type="POOLED">
                    <!-- 配置连接信息 -->
                    <property name="driver" value="${jdbc.driverClass}" />
                    <property name="url" value="${jdbc.url}" />
                    <property name="username" value="${jdbc.username}" />
                    <property name="password" value="${jdbc.password}" />
                </dataSource>
            </environment>
        </environments>
        <!-- 配置映射文件:用来配置sql语句和结果集类型等 -->
        <mappers>
            <mapper resource="UserMapper.xml" />
        </mappers>
    </configuration>
    

      

    在使用的上一行语句中写上PageHelper.startPage(pageNo,pageSize) 页码,每页页数

            PageHelper.startPage(3,2);
            List<User> list =  userMapper.selectUser("hello105");
    

      

    这样就可以工作了,下面我们测试一下

    通过日志可以看到,可以实现正常的分页工作了,下面我们来研究一下它的工作原理

    二:插件工作原理

    1:插件的注册,我们在第一节分析XMLConfigBuilder解析mybatis-config.xml的时候看过解析mappers,这里重点

    看一下如何解析plugins元素

     看一下解析plugins元素下面的plugin元素,

     最后注册到configuration中的interceptorChain中

     

     到这里,解析mybatis-config.xml时注册插件的过程就完成了。

    2:对数据库操作做增强

    看一下PageInterceptor这个类,这是一个拦截器类,从注解数据可以看出它主要拦截Executor的query方法

     

    这个类里有个plugin方法,入参是被代理对象,通过静态方法wrap包装,返回代理对象

    首先读取拦截Interceptor注解上的信息,判断代理类型是否匹配注解拦截信息,如果匹配则代理,不匹配则直接返回原对象

      public static Object wrap(Object target, Interceptor interceptor) {
        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;
      }
    

      

    PageInterceptor类的具体的拦截动作是在intercept这个方法里

    看完这个类,我们看一下到底是在哪里对Executor做的增强,一定是在创建executor对象的时候,创建executor是在创建DefaultSqlSession的时候,

    那来看一下SqlSessionFactory类的方法

    创建Executor后,会通过拦截链对Executor进行增强,如果interceptor为空,或者拦截链不匹配executor是就会返回原来的executor

    注册插件的时候我们看到过这个类,addInterceptor被调用过,现在就是用到第一步注册时候的插件来拦截

    public class InterceptorChain {
    
      private final List<Interceptor> interceptors = new ArrayList<Interceptor>();
    
      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);
      }
    
    }
    

      

    这个plugin一般就是对target进行代理,在上面看PageInterceptor这个类的时候,我们已经分析过,这里PageInterceptor是可以

    匹配Executor的,所以会被拦截,增强类Plugin,内部维护了PageInterceptor这个对象,所以当Executor对象调用query方法时,

    会调用到Plugin的Invoke方法,然后会被委托给PageInterceptor对象的intercept方法

      public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        try {
          Set<Method> methods = signatureMap.get(method.getDeclaringClass());
          if (methods != null && methods.contains(method)) {
            return interceptor.intercept(new Invocation(target, method, args));
          }
          return method.invoke(target, args);
        } catch (Exception e) {
          throw ExceptionUtil.unwrapThrowable(e);
        }
      }
    

      

    这样又回到了这个主要的方法里。

    我们来看一下查询的地方,这个查询的地方,四个入参的在selectList中,DefaultSqlSession中的方法

      public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
        try {
          MappedStatement ms = configuration.getMappedStatement(statement);
          return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
        } catch (Exception e) {
          throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);
        } finally {
          ErrorContext.instance().reset();
        }
      }
    

      

    调用这个方法,最终会调到intercept方法,这个方法里面是怎么分页的逻辑,这里忽略

    3:自定义一个拦截插件

    这里我们自定义一个拦截的插件,只是在拦截的时候把信息拿出来打印一下

    /**
     * 告诉MyBatis当前插件用来拦截哪个对象的哪个方法
     */
    @Intercepts({@Signature(
                type = StatementHandler.class,
                method = "query",
                args = {Statement.class,ResultHandler.class}
            )
        })
    public class MyFirstPlugin implements Interceptor {
    
        /**
         *
         * 拦截目标对象的目标方法的执行
         */
        @Override
        public Object intercept(Invocation invocation) throws Throwable {
            System.out.println("MyFirstPlugin...intercept:"+invocation.getMethod());
            Object target = invocation.getTarget();
            System.out.println("当前拦截到的对象:"+target);
            //拿到target的元数据
            MetaObject metaObject = SystemMetaObject.forObject(target);
            Object value = metaObject.getValue("parameterHandler.parameterObject");
            System.out.println("sql语句用的参数是:"+value);
            //执行目标方法
            Object proceed = invocation.proceed();
            //返回执行后的返回值
            return proceed;
        }
    
        /**
         *
         *包装目标对象的:为目标对象创建一个代理对象
         */
        @Override
        public Object plugin(Object target) {
            //我们可以借助Plugin的wrap方法来使用当前Interceptor包装我们目标对象
            System.out.println("MyFirstPlugin...plugin:mybatis将要包装的对象"+target);
            Object wrap = Plugin.wrap(target, this);
            //返回为当前target创建的动态代理
            return wrap;
        }
    
        /**
         *
         *将插件注册时 的property属性设置进来
         */
        @Override
        public void setProperties(Properties properties) {
            System.out.println("插件配置的信息:"+properties);
        }
    
    }
    

      

    定义好插件后,要在mybatis-config.xml中配置一下,这样才能在解析xml的时候实现注册缓存到configuration中

        <plugins>
            <plugin interceptor="com.github.pagehelper.PageInterceptor"/>
             <plugin interceptor="com.example.mybatis.plugin.MyFirstPlugin">
                <property name="someProperty" value="100"/>
            </plugin>
        </plugins>
    

      

    看一下运行结果:

     具体是在哪里调用的呢,那就要找到创建statement的地方

    SimpleExecutor类中有doQuery这个方法,方法里面有创建statementHandler对象的方法

      public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
        Statement stmt = null;
        try {
          Configuration configuration = ms.getConfiguration();
          StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
          stmt = prepareStatement(handler, ms.getStatementLog());
          return handler.<E>query(stmt, resultHandler);
        } finally {
          closeStatement(stmt);
        }
      }
    

      

    newStatementHandler方法,会使用拦截链过滤这个statementHandler,看是否和拦截链中的interceptor匹配,如果匹配就会生成代理。

    如果匹配,那么返回的statementHandler对象就是代理对象,statementHandler调用query时,调用的是Plugin的invoke方法,

    然后委托给MyFirstPlugin这个拦截器的intercept方法执行。

    总结:

    插件的使用可以在不修改原有逻辑的基础上,对功能进行增强,这也是动态代理的特性,在mybatis中可以支持插件拦截的地方有四个,上面已经分析,executor、statementHandler、parameterHandler、resultHanlder

    ,原理就是在mybatis-config配置插件信息,在解析mybatis-config.xml的时候会注册拦截信息到configuration的拦截链,然后在创建上面四个对象的时候实现增强,在具体调用拦截方法的时候,会

    调用到Plugin的invoke方法,在invoke中委托给插件处理。

  • 相关阅读:
    C#位操作符
    NGEN 本机映像生成器 【转载】
    Azure Services Platform
    补补算术基础:编程中的进制问题
    泛型约束
    去除Live Messenger 中的广告
    对代码性能进行调试和量测
    几个常用的文档转换工具(Office System)
    LINQ to DataSet
    使用 Entity Framework 實現彈性的資料模型 【转载】
  • 原文地址:https://www.cnblogs.com/warrior4236/p/13104984.html
Copyright © 2020-2023  润新知