MyBatis 源码学习12——MyBatis插件原理及应用

MyBatis插件

通过<plugins>标签配置,以拦截器的方式实现,可以方便地改变SQL的执行行为,例如在SQL执行时追加SQL分页语法达到简化分页查询的目的。
MyBatis 源码学习12——MyBatis插件原理及应用

MyBatis支持对Executor、ParameterHandler、ResultSetHandler、StatementHandler四种组件的方法进行拦截。

一、拦截器的注册过程:

先看下Configuration类,它维护了一个interceptorChain属性,这个属性是一个拦截器链,用于存放通过<plugins>标签注册的所有拦截器。

Configration类中还定义了一个addInterceptor()方法,用于向拦截器链中添加拦截器:

MyBatis 源码学习12——MyBatis插件原理及应用

InterceptorChain类:
List interceptors = new ArrayList();

MyBatis框架在应用启动时会对**标签进行解析,即调用XMLConfigBuilder类的pluginElement()方法**:
MyBatis 源码学习12——MyBatis插件原理及应用

1.首先获取<plugin>标签的interceptor属性,

2.然后获取用户指定的拦截器属性并转换为Properties对象,

3.然后通过Java的反射机制实例化拦截器对象,设置完拦截器对象的属性信息后,

4.将拦截器对象添加到Configuration类中维护的拦截器链中。

二、拦截器的执行过程:

用户自定义的插件只能对MyBatis中的4种组件的方法进行拦截,这4种组件及方法:
MyBatis 源码学习12——MyBatis插件原理及应用

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

为什么MyBatis插件能够对这四种组件的实例进行拦截呢?

回想下Configuration组件有3个作用:

1.描述MyBatis配置信息,项目启动时,MyBatis的所有配置信息都被转换为Configuration对象。

2.作为中介者简化MyBatis各个组件之间的交互,解决了各个组件错综复杂的调用关系,属于中介者模式的应用。

3.作为Executor、ParameterHandler、ResultSetHandler、StatementHandler组件的工厂创建这些组件的实例。

MyBatis使用工厂方法创建Executor、ParameterHandler、ResultSetHandler、StatementHandler组件的实例
其中一个原因是可以根据用户配置的参数创建不同实现类的实例;另一个是可以在工厂方法中执行拦截逻辑

在Configuration类的newParameterHandler()、newResultSetHandler()、newStatementHandler()、newExecutor()这些工厂方法中,都调用了InterceptorChain对象的pluginAll()方法:

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

pluginAll()方法返回ParameterHandler、ResultSetHandler、StatementHandler或者Executor对象的代理对象,拦截逻辑都是在代理对象中完成的。

拦截器链InterceptorChain类:
MyBatis 源码学习12——MyBatis插件原理及应用

通过一个List对象维护所有的拦截器实例,在InterceptorChain的pluginAll()方法中,会调用所有拦截器实例的plugin()方法,返回一个目标对象的代理对象。

因此MyBatis中所有用户自定义的插件都必须实现Interceptor接口
MyBatis 源码学习12——MyBatis插件原理及应用

它有3个方法:
intercept():定义拦截逻辑,该方法会在目标方法调用时执行。

plugin():用于创建Executor、ParameterHandler、ResultSetHandler或StatementHandler的代理对象,该方法的参数即为Executor、ParameterHandler、ResultSetHandler或StatementHandler组件的实例。

setProperties():用于设置插件的属性值。

需要注意的是,intercept()接收一个Invocation对象作为参数,
Invocation类
MyBatis 源码学习12——MyBatis插件原理及应用

Invocation类中封装了目标对象、目标方法及参数信息,我们可以通过Invocation对象获取目标对象(Executor、ParameterHandler、ResultSetHandle或StatementHandler)的所有信息。

另外,Invocation类中提供了一个proceed()方法,用于执行目标方法的逻辑。所以在自定义插件类中,拦截逻辑执行完毕后一般都需要调用proceed()方法执行目标方法的原有逻辑。

为了便于用户创建Executor、ParameterHandler、ResultSetHandler或StatementHandler实例的代理对象,MyBatis中提供了一个Plugin工具类
MyBatis 源码学习12——MyBatis插件原理及应用

Plugin类实现了InvocationHandler接口,即采用JDK内置的动态代理方式创建代理对象。

Plugin类中维护了Executor、ParameterHandler、ResultSetHandler或者StatementHandler类的实例,以及用户自定义的拦截器实例和拦截器中通过Intercepts注解指定的拦截方法。

Plugin类的invoke()方法会在调用目标对象的方法时执行,在invoke()方法中首先判断该方法是否被Intercepts注解指定为被拦截的方法,
如果是,则调用用户自定义拦截器的intercept()方法,并把目标方法信息封装成Invocation对象作为intercept()方法的参数。

Plugin类中还提供了一个静态的wrap()方法:第一个参数为目标对象,即Executor、ParameterHandler、ResultSetHandler、StatementHandler类的实例,第二个参数为拦截器实例,用于简化动态代理对象的创建,其实现:
MyBatis 源码学习12——MyBatis插件原理及应用

1.调用getSignatureMap()方法获取Intercepts注解指定的要拦截的组件及方法,

2.然后调用getAllInterfaces()方法获取当前Intercepts注解指定要拦截的组件的接口信息,

3.接着调用Proxy类的静态方法newProxyInstance()创建一个动态代理对象。

Intercepts注解用于修饰拦截器类,告诉拦截器要对哪些组件的方法进行拦截。下面是Intercepts注解的一个使用案例:
MyBatis 源码学习12——MyBatis插件原理及应用

如上面的代码所示,通过Intercepts注解指定拦截ResultHandler组件的query()方法,同时拦截StatementHandler组件的prepare()方法。

下面了解下Plugin类的getSignatureMap()方法解析Intercepts注解的过程:
MyBatis 源码学习12——MyBatis插件原理及应用

1.首先获取Intercepts注解,

2.然后获取Intercepts注解中配置的所有Signature注解,

3.接着对所有的Signature注解信息进行遍历,将Signature注解中指定要拦截的组件及方法添加到Map对象中,其中Key为Executor、ParameterHandler、ResultSetHandler或StatementHandler对应的Class对象,Value为拦截的所有方法对应的Method对象数组。

三、MyBatis源码中提供的一个自定义插件案例

我们以SqlSession执行查询操作为例介绍自定义插件执行拦截逻辑的过程:

MyBatis 源码学习12——MyBatis插件原理及应用
1.SqlSession操作数据库需要依赖于Executor组件,SqlSession会调用Configuration对象的newExecutor()方法获取Executor的实例。

2.Configuration类中通过一个InterceptorChain对象维护了用户自定义的拦截器链。newExecutor()工厂方法中调用InterceptorChain对象的pluginAll()方法。

3.InterceptorChain对象的pluginAll()方法中会调用自定义拦截器的plugin()方法。

4.自定义拦截器的plugin()方法是由我们来编写的,通常会调用Plugin类的wrap()静态方法创建一个代理对象。

5.SqlSession获取到的是Executor组件的代理对象,执行查询操作时会调用代理对象的query()方法。

6.按照JDK动态代理机制,调用Executor代理对象的query()方法时,

7.query()方法中会调用Plugin类的invoke()方法。

8.Plugin类的invoke()方法中会调用自定义拦截器对象的intercept()方法执行拦截逻辑。

9.自定义拦截器对象的intercept()方法调用完毕后,调用目标Executor对象的query()方法。

10.所有操作执行完毕后,会将查询结果返回给SqlSession对象。

自定义插件步骤
1.自定义插件类并实现Interceptor接口,重写接囗的三个方法,在intercept()方法中编写拦截逻辑,通过plugin()方法返回一个动态代理对象,通过setProperties()方法设置<plugin>标签中配置的属性值即可。

2.通过Intercepts注解指定对哪些组件的哪些方法进行拦截

3.在MyBatis主配置文件中对插件进行注册

MyBatis 源码学习12——MyBatis插件原理及应用