CH11-插件机制

MyBatis支持用插件对四大核心组件进行拦截,对MyBatis来说插件就是拦截器,用来增强核心组件的功能,增强功能本质上是借助于底层的动态代理实现的,换句话说,MyBatis中的四大对象都是代理对象。

  • Executor:用于执行增删改查操作的执行器类
  • StatementHandler:处理数据库SQL语句的类
  • ParameterHandler:处理SQL的参数的类
  • ResultSetHandler:处理SQL的返回结果集的类

以下是这四种处理各自能够拦截的方法:

对象描述可拦截方法方法作用
Executor上层对象,包含 SQL 执行全过程update执行 update/insert/delete
query执行 query
flushStatements在 commit 时自动调用,SimpleExecutor/ReuseExecutor/BatchExecutor 处理不同
commit提交事务
rollback回滚事务
getTransaction获取事务
close结束事务
isClosed判断事务是否关闭
StatementHandler执行 SQL 的过程,常用拦截对象prepareSQL 预编译
parameterize设置 SQL 参数
batch批处理
update增删改操作
query查询操作
ParameterHandlerSQL 参数组装过程getParameterObject获取参数
setParameters设置参数
ResultSetHandler执行 SQL 结果的组装handleResultSets处理结果集
handleOutputParameters处理存储过程出参

插件原理

  1. MyBatis的插件借助于责任链的模式进行对拦截的处理
  2. 使用动态代理对目标对象进行包装,达到拦截的目的

拦截

插件具体是如何拦截并附加额外的功能的呢?以ParameterHandler来说:

public ParameterHandlernewParameterHandler(
  	MappedStatement mappedStatement,
  	Object object,
  	BoundSql sql,
  	InterceptorChain interceptorChain){
	ParameterHandler parameterHandler = 
    mappedStatement.getLang().createParameterHandler(mappedStatement,object,sql);
	parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler);
	return parameterHandler;
}

public Object pluginAll(Objecttarget){
	for(Interceptorinterceptor:interceptors){
		target = interceptor.plugin(target);
	}
	return target;
}

interceptorChain保存了所有的拦截器(interceptors),是MyBatis初始化的时候创建的。

调用拦截器链中的拦截器依次的对目标进行拦截或增强。

interceptor.plugin(target) 中的 target 就可以理解为MyBatis中的四大组件,返回的target是被重重代理后的对象。

插件接口Interceptor

  1. intercept()方法,插件的核心方法
  2. plugin()方法,生成target的代理对象
  3. setProperties()方法,传递插件所需参数

插件实例

插件开发需要以下步骤

  1. 自定义插件需要实现上述接口
  2. 增加@Intercepts注解(声明是哪个核心组件的插件,以及对哪些方法进行扩展)
  3. 在xml文件中配置插件
**
*插件签名告诉MyBatis插件用来拦截那个对象的哪个方法
**/
@Intercepts({
	@Signature(type=StatementHandler.class,method="parameterize",args=Statement.class)
})
public class MyInterceptsimplementsInterceptor {
  /**
  *拦截目标对象的目标方法
  *
  *@paraminvocation
  *@return
  *@throwsThrowable
  */
  @Override
  public Object intercept(Invocation invocation) throws Throwable{
  	System.out.println("进入自定义的拦截器,拦截目标对象"+invocation+invocation.getMethod()+invocation.getTarget());
  	return invocation.proceed();
  }

  /**
  *包装目标对象为目标对象创建代理对象
  *
  *@Paramtarget为要拦截的对象
  *@Return代理对象
  */
  @Override
  public Object plugin(Object target){
  	System.out.println("自定义plugin方法,将要包装的目标对象"+target.toString()+target.getClass());
  	returnPlugin.wrap(target,this);
  }

  /**
  *获取配置文件的属性
  *
  *@paramproperties
  */
  @Override
  public void setProperties(Properties properties){
  	System.out.println("自定义插件的初始化参数"+properties);
  }
}

在mybatis-config.xml中配置插件

    <!--自定义插件-->
<plugins>
	<plugininterceptor="com.boxuegu.javaee.mybatissourcelearn.MyIntercepts">
		<propertyname="test"value="testvalue"/>
	</plugin>
</plugins>

调用查询方法,查询方法会返回ResultSet

public class Test{
  public static void main(String[]args){
    //1.加载配置文件
    String resource="mybatis-config.xml";
    InputStream inputStream=null;
    try{
    	inputStream=Resources.getResourceAsStream(resource);
    }catch(IOExceptione){
    	e.printStackTrace();
  	}

    //2.获取sqlSessionFactory
    SqlSessionFactory sqlSessionFactory=
      new SqlSessionFactoryBuilder().build(inputStream);

    //3.获取sqlSession
    SqlSession sqlSession=sqlSessionFactory.openSession();
    try{
      //通过xml文件直接执行sql语句
      //Employeeemployee=sqlSession.selectOne("com.boxuegu.javaee.mybatissourcelearn.dao.EmployeeMapper.getEmployeeById",1);
      //alt+shift+Lintroducelocalvariables;
      Thread thread=newThread(()->System.out.println("test"));
      //4.获取mapper接口实现
      EmployeeMapper mapper=sqlSession.getMapper(EmployeeMapper.class);
      System.out.println("mapper::::"+mapper.getClass());

      //5.执行sql语句
      Employee employee=mapper.getEmployeeById(1);
      System.out.println(employee);
    }finally{
    	sqlSession.close();
    }
  }
}

分页插件

MyBatis 自带的 RowBounds 是一次查询所有结果,然后再内存中计算并提取分页,最终返回指定页数的数据。

这种分页方式对于数据量小的还好,如果数据量大就有很大性能影响了。因为这种方式是将数据全部查询出来放到内存里,再进行分页长度的截取,是一种伪分页,是一种逻辑分页。

因此通常会使用 PageHelper 插件:

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

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

<plugins>
	<plugininterceptor="com.github.pagehelper.PageInterceptor" />
</plugins>

执行分页查询:

PageHelper.startPage(/*pageNum*/ 2, /*pageSize*/ 4);
List<User> users = userMapper.findAll();
PageInfo<User> pageInfo = new PageInfo(list);
// return pageInfo to controller

实现原理

PageHelper是利用Mybatis的插件原理,对Executor接口的query方法进行了拦截。

@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 {
    //...
}

在创建Executor时,如果有插件对Executor进行拦截,则会对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);
    }
    // 根据配置文件中的 settings 节点cacheEnabled配置项确定是否启用缓存
    if (cacheEnabled) { // 如果配置启用该缓存
        // 使用CachingExecutor装饰实际的执行器
        executor = new CachingExecutor(executor);
    }
    // 为执行器增加拦截器(插件),以启用各个拦截器的功能
    executor = (Executor) interceptorChain.pluginAll(executor);
    return executor;
}

当程序走到DefaultSqlSession中时,会调用Executor的query方法,此时就会触发PageHelper的代理,进入PageHelper的逻辑:

public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
    try {
        /**
         * 获取查询语句
         * 设置MappedStatement映射,见{@link XMLStatementBuilder#parseStatementNode()}
         */
        MappedStatement ms = configuration.getMappedStatement(statement);
        /**
         * 交由执行器进行查询,由于全局配置cacheEnabled默认是打开的,因此此处的executor通常都是CachingExecutor
         * 获取executor,见{@link Configuration#newExecutor(org.apache.ibatis.transaction.Transaction, org.apache.ibatis.session.ExecutorType)}
         */
        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();
    }
}

PageHelper拦截器PageInterceptor执行流程:

@Override
public Object intercept(Invocation invocation) throws Throwable {
    try {
        ...
        //调用方法判断是否需要进行分页,如果不需要,直接返回结果
        if (!dialect.skip(ms, parameter, rowBounds)) {
            //判断是否需要进行count查询
            if (dialect.beforeCount(ms, parameter, rowBounds)) {
                //查询总数
                Long count = count(executor, ms, parameter, rowBounds, null, boundSql);
                //处理查询总数,返回true时继续分页查询,false时直接返回
                if (!dialect.afterCount(count, parameter, rowBounds)) {
                    //当查询总数为0时,直接返回空的结果
                    return dialect.afterPage(new ArrayList(), parameter, rowBounds);
                }
            }
          	// 生成不同方言对应的分页 SQL 并执行
            resultList = ExecutorUtil.pageQuery(dialect, executor,
                    ms, parameter, rowBounds, resultHandler, boundSql, cacheKey);
        } else {
            //rowBounds有参数值,不使用分页插件处理时,仍然支持默认的内存分页
            resultList = executor.query(ms, parameter, rowBounds, resultHandler, cacheKey, boundSql);
        }
        // 将查询结果设置到Page对象中
        return dialect.afterPage(resultList, parameter, rowBounds);
    } finally {
        if(dialect != null){
            dialect.afterAll();
        }
    }
}

ExecutorUtil.pageQuery主要是基于不同的数据库方言生成分页查询 SQL,比如最简单的是在原 SQL 后追加 LIMIT ? OFFSET ?

多插件开发

  1. 创建代理对象时,按照插件配置的顺序进行包装
  2. 执行目标方法后,是按照代理的逆向进行执行

总结

1.遵循插件尽量不使用的原则,因为会修改底层设计

2.插件是生成的层层代理对象的责任链模式,使用反射机制实现

3.插件的编写要考虑全面,特别是多个插件层层代理的时候