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中委托给插件处理。

原文地址:https://www.cnblogs.com/warrior4236/p/13104984.html