在《MyBatis是纸老虎吗?(三)》这篇文章中我们一起梳理了MyBatis配置文件的解析流程,并详细介绍了其中的一些常见节点的解析步骤。通过梳理,我们弄清楚了MyBatis配置文件中的一些常用配置项与Java Bean之间的对应关系,这进一步加深了我们对MyBatis配置文件认识;通过梳理,我们对MyBatis的使用步骤有了更全面的了解,这进一步提高了我们使用MyBatis的能力。今天我想继续梳理MyBatis这个框架,因为我们了解的,仅仅是冰山的一角。MyBatis中还有很多其他实用的知识点和好的设计思想值得我们深究。那今天就一起研究一下MyBatis配置文件中的plugins元素吧。
1 plugins元素的定义及解析
上篇文章——《MyBatis是纸老虎吗?(三)》——有提到过这个元素。这个元素的作用就是允许开发者指定一个插件,这个插件可以在映射语句执行过程中的某一点进行拦截,然后做一些特殊的处理,比如数据分页、操作日志增强、sql性能监控等。那如何定义一个插件呢?很简单,只需实现org.apache.ibatis.plugin.Interceptor接口即可。下面是一个自定义插件的示例:
@Intercepts({
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})
})
public class ExamplePlugin implements Interceptor {
private Properties properties = new Properties();
public Object intercept(Invocation invocation) throws Throwable {
// implement pre processing if need
Object returnObject = invocation.proceed();
// implement post processing if need
return returnObject;
}
public void setProperties(Properties properties) {
this.properties = properties;
}
}
这个拦截器中的intercept()方法并未做任何处理,只是调用了Invocation对象上的proceed()方法,并将该方法的执行结果返回给上级调用者。梳理到这里,我想看一下Interceptor接口的源码,如下所示:
public interface Interceptor {
Object intercept(Invocation invocation) throws Throwable;
// default xxxx,如果没有没记错的话,这是 jdk1.8的新特性
default Object plugin(Object target) {
return Plugin.wrap(target, this);
}
default void setProperties(Properties properties) {
// NOP
}
}
由此源码,我们可以知道Interceptor,拦截器,是一个接口,其中仅有一个名为intercept的方法,因此实现该接口的类一般都要对这个方法进行实现。上面展示的自定义拦截器就对这个方法进行了实现。注意:这个方法会接收一个Invocation类型的参数,该类的源码如下所示:
public class Invocation {
private final Object target;
private final Method method;
private final Object[] args;
public Invocation(Object target, Method method, Object[] args) {
this.target = target;
this.method = method;
this.args = args;
}
public Object getTarget() {
return target;
}
public Method getMethod() {
return method;
}
public Object[] getArgs() {
return args;
}
public Object proceed() throws InvocationTargetException, IllegalAccessException {
return method.invoke(target, args);
}
}
梳理到这里,我非常想知道是这个自定义拦截器要怎样用。想必诸位都已先我一步知道了这个问题的答案:直接在MyBatis的配置文件config.xml中新增plugins配置项。具体代码如下所示:
<plugins>
<plugin interceptor="包名.插件类名"></plugin>
</plugins>
进行到这里,所有的前期准备工作就完成了。下面就一起看一下这个元素的解析过程吧!通过上篇文章我们知道XMLConfigBuilder类的parse()方法开启了执行流程,其中执行解析工作的核心是与其同属一类的parseConfiguration()方法。该方法会对MyBatis配置文件中的元素按照既定顺序逐个解析。这些元素的解析顺序为:properties、settings、typeAliases、plugins、objectFactory、objectWrapperFactory、reflectorFactory、environments、databaseIdProvider、typeHandlers、mappers。上节我们一起梳理了properties、settings、typeAliases、environments四个元素的解析过程,这节就详细梳理一下plugins元素的解析过程。解析plugins元素的方法的名为pluginsElement(),其源码如下所示:
private void pluginsElement(XNode context) throws Exception {
if (context != null) {
for (XNode child : context.getChildren()) {
String interceptor = child.getStringAttribute("interceptor");
Properties properties = child.getChildrenAsProperties();
Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).getDeclaredConstructor()
.newInstance();
interceptorInstance.setProperties(properties);
configuration.addInterceptor(interceptorInstance);
}
}
}
该方法被调用时的状态如下图所示(注意截图中context参数的内容,就是MyBatis配置文件中的plugins标签下的内容):
借助这段运行时状态图,让我们一起分析一下这段代码的执行逻辑:1.拿到context参数所代表的plugins元素下的所有plugin节点,并遍历这些节点,然后执行下述步骤;2.拿到plugin节点上interceptor属性的值,这个值就是我们自定义的拦截器的包全名+类名;3.解析plugin元素下的子元素,并将其包装为Properties对象;4.解析第二步拿到的数据,然后加载相应的类,并实例化一个对象出来(注意:这里用到了反射);5.将第三步解析出来的Properties对象设置到第四步创建的Interceptor对象上;6.将第四步创建的Interceptor对象设置到Configuration对象的interceptorChain属性上(这个操作是通过调用Configuration类上的addInterceptor()方法完成的。这里还有一点需要注意:interceptorChain的类型为InterceptorChain,这让我想到了责任链及Spring的AOP和事务,有兴趣的可以翻看一下我之前梳理的与这两个主题有关的系列文章《Spring AOP系列》、《Spring事务系列》)。
关于上述解析步骤,个人觉得有以下几点需要注意:
- 上述第二步和第五步中都提到了Properties,为什么我们可以在plugin元素中使用property标签呢?为什么我们可以将这些值设置到Interceptor类型的对象上呢?这两个问题很好回答。关于第一个问题:因为MyBatis支持,如若不然,plugins元素的解析逻辑中不会出现Properties properties = child.getChildrenAsProperties()这样一行代码。那MyBatis是怎么支持的呢?这个就要看MyBatis配置文件的dtd约束文件了,先看下面这段从mybatis-3-config.dtd文件中摘抄出来的代码:<!ELEMENT plugin (property*)>。这段代码的大致意思就是说在plugin元素下可以有零个或多个property标签(具体参照下图“mybatis dtd文件关于plugin的定义”)。关于第二个问题:根据前面列出的源码,不难发现Interceptor源码中有一个default修饰的setProperties()方法,该方法返回值为void类型,默认不做任何处理。前面自定义的拦截器实现了这个方法。正因为Interceptor中有这样一个方法,所以解析代码中才有这样一句:interceptorInstance.setProperties(properties)。(也就是上面描述中的第五步)
- 上述第二步调用XMLConfigBuilder类的父类BaseBuilder类resolveClass()方法去解析我们在plugin元素中指定的interceptor属性值(包全名+拦截器名),该方法会继续调用BaseBuilder类中的resolveAlias()方法,这个方法会继续调用TypeAliasRegistry对象的resolveAlias()方法,这个方法的源码在上篇文章中已经展示过,这里就不再啰嗦,有兴趣可以翻看源码或者翻阅上篇文章。这段代码会直接将传递进来的string参数转为小写,然后从typeAliases中查找这个string参数代表的key是否存在,如果存在,则直接返回其对应的Class<?>类型的值,如果不存在,则直接使用Resources加载这个类。plugins的解析最终走的就是这一步
mybatis dtd文件关于plugin的定义
2 关于InterceptorChain的介绍
上小节的第六步中提到解析出来的Interceptor的对象会被设置到Configuration对象中的interceptorChain属性上。这个属性的实际类型为InterceptorChain。该类的源码为:
public class InterceptorChain {
private final List<Interceptor> interceptors = new ArrayList<>();
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);
}
}
这个类中定义了一个List类型的变量interceptors,其持有的类型为Interceptor,所以第六步调用Configuration类上的addInterceptor()方法,最终实际上调用的就是这个类上的addInterceptor()方法向interceptors变量中添加数据。由此,我们可以得出这样的结论InterceptorChain类就是Interceptor的容器,其主要作用就是聚集所有实现了Interceptor接口的类,并充当调度的入口。那这个调度入口究竟在哪里呢?这个入口在《MyBatis是纸老虎吗?(二)》这篇文章中有提到。有兴趣的可以翻阅一下。如果实在不想翻,可以看下面这幅图:
通过这幅图片,我们可以看到在Configuration类的newExecutor()方法中会调用InterceptorChain类中的pluginAll()方法,同时会向该方法中传递一个Executor型的参数。这样我们就弄清楚InterceptorChain类使用的地方了。那这个pluginAll()方法究竟做了什么?继续跟踪源码,其运行时状态如下图所示:
注意:图片中target的实际类型为CachingExecutor,而interceptors中只有一个ExamplePlugin对象,所以循环体中interceptor.plugin(target)一句代码最终调用的是Interceptor接口中的default方法plugin(),这个方法的源码如下所示:
default Object plugin(Object target) {
return Plugin.wrap(target, this);
}
这个方法会调用Plugin类中名为wrap()的方法,然后将Executor(CachingExecutor)和Interceptor(ExamplePlugin)两个对象进行组合,Plugin#wrap()方法的源码如下所示:
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;
}
这个方法会首先调用同类中的getSignatureMap()方法,该方法的主要作用是解析Interceptor实现类上的@Intercepts注解,然后解析该注解中的Signature属性,该注解及其属性的定义如下所示:
@Intercepts({
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})
})
这里就不再就具体执行过程进行过多说明,详细逻辑请参看源码:
private static Map<Class<?>, Set<Method>> getSignatureMap(Interceptor interceptor) {
// 解析Interceptors上的注解@Interceptors,这个注解的值是一个由Signature注解组成的数组
Intercepts interceptsAnnotation = interceptor.getClass().getAnnotation(Intercepts.class);
// issue #251
if (interceptsAnnotation == null) {
throw new PluginException(
"No @Intercepts annotation was found in interceptor " + interceptor.getClass().getName());
}
// 拿到Interceptors注解上的数据
Signature[] sigs = interceptsAnnotation.value();
Map<Class<?>, Set<Method>> signatureMap = new HashMap<>();
// 遍历 Signature[] 数组,并对其进行处理。注意:Signature中有这样几个属性:type(Executor类型)、method(将要被拦截的方法名,这些方法都是type指定的类中的方法)、args(这个参数用于表示method参数指定的方法中包含的参数)
// 比如上面示例中的配置:MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class,它们是query方法的参数,这个方法位于Executor接口中
for (Signature sig : sigs) {
// 这里的MapUtil是ibatis自定义的一个工具类,这个方法的源码如下所示:
// public static <K, V> V computeIfAbsent(Map<K, V> map, K key, Function<K, V> mappingFunction) {
// V value = map.get(key);
// if (value != null) {
// return value;
// }
// return map.computeIfAbsent(key, mappingFunction);
// }
// 最终这个方法会调用HashMap中的computeIfAbsent()方法,这个方法对 hashMap 中指定 key 的值进行重新计算,如果不存在这个 key,则添加到 hashMap 中
// 最终就是将sig.type()和k->new HashSet<>()之间建立关系,并将这组关系存放到signatureMap中
Set<Method> methods = MapUtil.computeIfAbsent(signatureMap, sig.type(), k -> new HashSet<>());
try {
// 拿到Signature对象上的method值和args数据,然后拿到Signature中type表示的字节码,并从中查找method和args能够确定的方法,其结果是一个Method对象
Method method = sig.type().getMethod(sig.method(), sig.args());
// 将上一步创建的Method对象放到Set<Method>对象中
methods.add(method);
} catch (NoSuchMethodException e) {
throw new PluginException("Could not find method on " + sig.type() + " named " + sig.method() + ". Cause: " + e,
e);
}
}
// 返回signatureMap 对象(其中存储了在拦截器上的@Signatrue注解指定的数据,对应关系为:type->Method对象(method(args)))
return signatureMap;
}
通过这个方法,我们会拿到一个Map<Class<?>, Set<Method>>集合,这个集合中存储的是一组映射:其中key就是@Signatrue注解中的type,value则是通过@Signature注解中的type、method和args确定的Method对象,也就是type所表示的类中的某个方法。这里我们一起看一下Intercepts及Signature的源码:
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Intercepts {
/**
* Returns method signatures to intercept.
*
* @return method signatures
*/
Signature[] value();
}
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({})
public @interface Signature {
/**
* Returns the java type.
*
* @return the java type
*/
Class<?> type();
/**
* Returns the method name.
*
* @return the method name
*/
String method();
/**
* Returns java types for method argument.
*
* @return java types for method argument
*/
Class<?>[] args();
}
接着再来看Plugin#wrap()方法中的Class<?> type = target.getClass(),这里就是获取target对象的字节码数据,这里的target就是前面调用时传递进来的CachingExecutor对象。紧接着再看Class<?>[] interfaces = getAllInterfaces(type, signatureMap)这句,其主要目的就是获取type上存在于signatureMap中的父接口,这里的getAllInterfaces()方法的源码如下所示:
private static Class<?>[] getAllInterfaces(Class<?> type, Map<Class<?>, Set<Method>> signatureMap) {
// 注意案例执行时,这里的type是CachingExecutor
// 这里首先创建一个Set<Class<?>>型变量
Set<Class<?>> interfaces = new HashSet<>();
// 如果type值不为空,则进入 while 循环
// 调用type上的getInterfaces()方法获得type上的父接口,接着判断signatureMap是否包含这个字节码,如果包含就将其添加到interfaces集合中;如果不存在则继续遍历type上的父类
while (type != null) {
for (Class<?> c : type.getInterfaces()) {
if (signatureMap.containsKey(c)) {
interfaces.add(c);
}
}
type = type.getSuperclass();
}
return interfaces.toArray(new Class<?>[0]);
}
最后判断上一步获取的interfaces数组是否为空,如果不为空则直接调用Proxy的newProxyInstance()方法创建代理对象,这里是标准的jdk动态代理。调用该方法时,会传递三个参数:类加载器、字节码数组(interfaces)、InvocationHandler对象(实际就是Plugin对象,该对象有三个属性:target【案例中的CachingExecutor】、interceptor【由Executor.class组成的数组】、signatureMap【其中存储了在拦截器上的@Signatrue注解指定的数据,对应关系为:type->Method对象(method(args))】)。关于jdk动态代理可以参考《RMI 总结之代理》这篇文章。
至此系统就为我们创建了一个代理类,接着代码会向上返回,直至返回到Configuration类的newExecutor()方法中((Executor) interceptorChain.pluginAll(executor))。由此继续向上返回到DefaultSqlSessionFactory类的openSessionFromDataSource()方法中(Executor executor = configuration.newExecutor(tx, execType)),最后该方法会创建一个DefaultSqlSession对象并返回给上级调用者,即SpringTransactionApplication类的mybatis()中。
好,让我们回过头来看本小节开头的提出的那个问题:pluginAll()方法究竟做了什么?这个方法会在创建DefaultSessionFactory时调用,其主要作用是遍历MyBatis配置文件中指定的Interceptor集合,然后依次调用集合中的拦截器上的plugin()方法,对传递进来的Executor对象进行包装,即创建对应的代理对象。注意:这里的InterceptorChain类是责任链(Interceptor集合)的调度入口。
3 总结
记得在《MyBatis是纸老虎吗?(二)》结尾有提到过这样一个问题:这里的interceptorChain是为sql语句新增mysql查询条数的责任链吗?现在想想,这个问法有些问题:InterceptorChain是一个容器(暂时这么理解吧),用于存放实现了Interceptor接口的拦截器(通过InterceptorChain中的addInterceptor()方法完成拦截器的添加,数据会添加到InterceptorChain中的interceptors中)。其次该接口中的pluginAll()方法的主要作用是遍历Interceptor拦截器组成的集合(即:InterceptorChain中的interceptors属性),然后用集合中的元素对传递进该方法中的Object对象(实际上就是Executor对象,注意这里这么说是因为跟踪的代码在Configuration类的newExecutor()方法中)进行包装(通过调用Interceptor接口中的plugin()方法完成包装,这里的包装说白了就是创建动态代理对象,动态代理的创建过程可以看前一小节——关于InterceptorChain的介绍。说白了就是解析Interceptor实现类上的@Interceptors注解,然后用标准的jdk动态代理创建方式创建一个动态代理)。其次问题中的为sql增加查询条数的说法不是在这一步完成的,这一步的主要作用是创建动态代理,比如创建Executor对象的动态代理对象。再次这里说是责任链个人觉得有点不妥,应该说是Interceptor链,即拦截器链。
下面就本篇的主要内容进行一个小结,本节主要目的是梳理一下如何在MyBatis中自定义并使用一个拦截器【期间梳理了拦截器的解析过程、又梳理了拦截器链的执行过程(即InterceptorChain中的pluginAll()方法的执行流程,通过这个梳理明白了动态代理的创建过程)】。下面就拦截器的定义步骤进行详细说明:
- 继承Interceptor接口,并实现其中的方法
- 在该实现类上添加@Intercepts注解,在该注解中指定@Signature数据,这里需要注意一下:@Intercepts用于标识该类是一个拦截器;@Signature(拦截点)则用于指明自定义拦截器需要拦截哪一个类型,哪一个方法,可以拦截的类型有:Executor——拦截执行器的方法、ParameterHandler——拦截参数的处理、ResultHandler——拦截结果集的处理、StatementHandler——拦截Sql语法构建的处理。@Signature中的属性有:type——上述四种类型中的一种;method——对应接口中的哪类方法(因为可能存在重载方法);args——对应哪一个方法的入参。
- 在MyBatis配置文件的plugins节点中指定相应的拦截器类