MyBatis插件开发原理

一、前言

MyBatis采用责任链模式,通过动态代理组织多个插件(拦截器),通过这些插件可以改变MyBatis的默认行为(诸如SQL重写之类的),由于插件会深入到MyBatis的核心,因此在编写自己的插件前最好了解下它的原理,以便写出安全高效的插件。
MyBatis在四大对象的创建过程中,都会有插件进行介入。插件可以利用动态代理机制一层层的包装目标对象,而实现目标对象在执行目标方法之前进行拦截的效果。
插件介入指的是:创建过程中都会涉及到调用interceptChain.pluginAll()方法对四大对象进行重新包装,返回一个代理对象。
MyBatis 允许在已映射语句执行过程中的某一点进行拦截调用,默认情况下,MyBatis 允许使用插件来拦截的方法调用包括:

  • Executor(update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
  • ParameterHandler(getParameterObject, setParameters)
  • ResultSetHandler(handleResultSets, handleOutputParameters)
  • StatementHandler(prepare, parameterize, batch, update, query)

插件开发是基于动态代理实现的,所有有必要对动态代理有所了解。

我们可以看一下MyBatis是怎么创建这四大接口对象的。找到源码BaseStatementHandler

this.parameterHandler = configuration.newParameterHandler(mappedStatement, parameterObject, boundSql);
this.resultSetHandler = configuration.newResultSetHandler(executor, mappedStatement, rowBounds, parameterHandler, resultHandler, boundSql);

进入configuration类,下面几处都是在创建newParameterHandler,ResultSetHandler,StatementHandler这几个对象,在调用的过程中,大家都看到了都使用了interceptorChain.pluginAll方法分别对每一个对象进行了重新包装并返回

public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {
    ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement, parameterObject, boundSql);
    parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler);
    return parameterHandler;
}

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);
    resultSetHandler = (ResultSetHandler) interceptorChain.pluginAll(resultSetHandler);
    return resultSetHandler;
}

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);
    statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
    return statementHandler;
}

点进interceptorChain.pluginAll方法里面

/**
*每一个拦截器对目标类都进行一次代理
*@target
*@return 层层代理后的对象
**/
public Object pluginAll(Object target) {
    for (Interceptor interceptor : interceptors) {
      target = interceptor.plugin(target);
    }
    return target;
}

这一段代码可以看到:获取所有的Interceptor(拦截器),我们如果需要自定义拦截器就得实现Interceptor这个接口。然后调用interceptor.plugin(target);返回target包装之后的对象
所以,我们可以使用插件为目标对象创建一个代理对象,这跟我们学习Struts2的拦截器,Spring的AOP一样,其实都是动态代理,面向切面的编程。

下面我们通过案例为StatementHandler创建代理对象

二、案例

搭建一个新的Maven工程MyBatisPlugins

MyBatis插件开发原理_第1张图片

创建一个拦截器StatementHandlerInterceptor

/**
 * 定义插件StatementHandlerInterceptor
 * 告诉MyBatis当前插件用来拦截哪个对象的哪个方法
 */
@Intercepts({@Signature(type=StatementHandler.class,method="parameterize",args=java.sql.Statement.class)})
public class StatementHandlerInterceptor implements Interceptor {

	/**
	 * 拦截目标对象的目标方法的执行
	 */
	@Override
	public Object intercept(Invocation invocation) throws Throwable {
		// 执行目标方法
		Object proceed = invocation.proceed();
		// 返回执行后的返回值
		return proceed;
	}

	/**
	 * 包装目标对象 包装:为目标对象创建一个代理对象
	 * 根据当前拦截的对象创建了一个动态代理对象。代理对象的InvocationHandler处理器为新建的Plugin对象
	 * 官方推荐实现方式:Plugin.wrap(target, this);
	 */
	@Override
	public Object plugin(Object target) {
		// 我们可以借助Plugin的wrap方法来使用当前Interceptor包装我们目标对象
		System.out.println("StatementHandlerInterceptor...plugin:mybatis将要包装的对象" + target);
		Object wrap = Plugin.wrap(target, this);
		// 返回为当前target创建的动态代理
		return wrap;
	}

	/**
	 * 将插件注册时的property属性设置进来
	 */
	@Override
	public void setProperties(Properties properties) {
		System.out.println("插件配置的信息:"+properties);
	}

}

每一个拦截器都必须实现上面的三个方法,其中:

  • Object intercept(Invocation invocation)是实现拦截逻辑的地方,内部要通过invocation.proceed()显式地推进责任链前进,也就是调用下一个拦截器拦截目标方法。
  • Object plugin(Object target) 就是用当前这个拦截器生成对目标target的代理,实际是通过Plugin.wrap(target,this) 来完成的,把目标target和拦截器this传给了包装函数。
  • setProperties(Properties properties)用于设置额外的参数,参数配置在拦截器的Properties节点里。

注解里描述的是指定拦截方法的签名   [type,method,args] (即对哪种对象的哪种方法进行拦截),它在拦截前用于决断。

将拦截器注册到全局配置文件中

MyBatis插件开发原理_第2张图片

打印测试一下,看看控制台打印效果

StatementHandlerInterceptor...plugin:mybatis将要包装的对象org.apache.ibatis.executor.CachingExecutor@4d639a35
StatementHandlerInterceptor...plugin:mybatis将要包装的对象org.apache.ibatis.scripting.defaults.DefaultParameterHandler@1d74b5ee
StatementHandlerInterceptor...plugin:mybatis将要包装的对象org.apache.ibatis.executor.resultset.DefaultResultSetHandler@27f5ad1c
StatementHandlerInterceptor...plugin:mybatis将要包装的对象org.apache.ibatis.executor.statement.RoutingStatementHandler@4229e623

2017-08-20 12:35:29,825 [main] [com.queen.mybatis.mapper.EmpMapper.findEmpById]-[DEBUG] ==>  Preparing: select id,emp_name empName,emp_email empEmail, dept_id deptId from t_emp where id = ? 
2017-08-20 12:35:29,895 [main] [com.queen.mybatis.mapper.EmpMapper.findEmpById]-[DEBUG] ==> Parameters: 1(Integer)
2017-08-20 12:35:29,949 [main] [com.queen.mybatis.mapper.EmpMapper.findEmpById]-[DEBUG] <==      Total: 1
Emp [id=1, empName=queen3aasd21, [email protected], deptId=1]

可以看到四大对象创建的时候都会调用这个拦截器因为都会被拦截下来,但是只有StatementHandler被创建了代理对象。这是为什么呢?大家可以看一下Plugin的源码

MyBatis插件开发原理_第3张图片

大家可以Debug一下源码,您会发现只有StatementHandler被创建了代理对象

MyBatis插件开发原理_第4张图片

三、总结

插件开发步骤如下:

  • 编写插件实现Interceptor接口,并使用@Intercepts注解完成插件签名
  • 在全局配置文件中注册插件
  • MyBatis插件开发原理_第5张图片

插件的原理:

  • 按照插件注解声明,按照插件配置顺序调用插件plugin方法,生成被拦截对象的动态代理
  • 多个插件依次生成目标对象的代理对象,层层包裹,先声明的先包裹;形成代理链
  • 目标方法执行时依次从外到内执行插件的intercept方法

 

 

=======欢迎大家拍砖,小手一抖,多多点赞哟!=======

版权声明:本文为博主原创文章,允许转载,但转载必须标明出处。

 

你可能感兴趣的:(MyBatis教程)