《 一生所爱》
从前现在过去了再不来
红红落叶长埋尘土内
开始终结总是没变改
天边的你飘泊白云外
苦海翻起爱恨
在世间难逃避命运
相亲竟不可接近
或我应该相信是缘份
情人别后永远再不来(消散的情缘)
无言独坐放眼尘世外(愿来日再续)
鲜花虽会凋谢(只愿)
但会再开(为你)
一生所爱 隐约(守候)
在白云外(期待)
苦海翻起爱恨
在世间难逃避命运
相亲竟不可接近
或我应该相信是缘份
苦海翻起爱恨
在世间难逃避命运
相亲竟不可接近
或我应该相信是缘份
前言
这是我们分析 mybatis 的第四篇文章,看标题,我们是分析 mybatis 的插件,其实,在前面的三篇文章中,我们已经在剖析源码的时候多多少少接触到 mybatis 的插件设计和运行过程了,只是没有单独的开一篇文章来讲这个,mybatis 的日志系统就是基于插件的。这个在我们之前的源码剖析里也说过。插件在整个mybatis 中只占很小的一部分,mybatis 不像 Spring ,留了很多的接口给使用者扩展,只留了一个接口给开发者扩展。究其原因还是两者的目标和工作不同。有了之前三篇文章的基础,我们今天研究 mybatis 的插件,基本就是一个复习的过程,整体上还是比较轻松的。那么,接下来我们就看看吧!
我们将分为 2 个部分来讲述,一个是插件原理,一个是如何应用插件接口并且对比国内流行的插件。
1.插件原理
我们在剖析 mybatis 的时候,就已经发现了 mybatis 的插件在他自己框架身上的应用,我们回顾一下在哪里出现的:
从上面的截图,可以看到,在mybatis 4大对象的创建过程中,都调用了 interceptorChain.pluginAll 方法,可见该方法的重要性,那么该方法的作用是上面呢?我们首先猜测一下,从该方法的名字可以看出,该方法是拦截器链调用插件方法,并传入了一个对象,最后返回了一个该对象,那么,我们看看该方法是如何实现的:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hb1vlnAa-1571037104675)(http://upload-images.jianshu.io/upload_images/4236553-26ef0f9af17d1ef8.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)]
该类可以说是非常的简单,所谓大道无形,该类是 mybatis 插件核心,首先有一个插件集合,一个 pluginAll 方法,一个 addInterceptor 方法, 一个getInterceptors 方法,可以看的出来该类就是一个过滤器链,类似tomcat 的过滤器和Spring的AOP,我们主要看两个方法,一个是 pluginAll,一个是 addInterceptor 方法,我们首先看看 addInterceptor 方法,也即使添加过滤器,什么时候添加呢?我们看看该方法的调用栈:
可以看到,从我们的main方法开始,调用了 SqlSessionFactoryBuilder.build 方法,再调用了 XMLConfigBuilder 的 parse 方法,该方法又调用了自身的 parseConfiguration 方法,在 parseConfiguration 方法中调用了 pluginElement 解析 “plugins” 属性,在该方法中调用了 configuration.addInterceptor 方法,该方法又调用了 interceptorChain.addInterceptor 方法,将插件添加进该集合。也就是说,该方法是在解析XML配置文件的时候调用的,将配置好的插件添加进集合中,以便之后的调用。
那么 pluginAll 方法是什么时候运行的呢?我们同样看看他的方法调用栈:
我们在方法调用栈图上看到的最后一层调用了 openSession 方法,也就是我们 sqlSessionFactory.openSession() 方法生成 SqlSession 的时候,该方法会调用 自身的
openSessionFromDataSource 方法,然后调用 configuration.newExecutor 方法插件 Executor,在 newExecutor 方法中,我们上面的图上也有,调用了 executor = (Executor) interceptorChain.pluginAll(executor) 方法,返回了一个 executor,很显然,这个对象肯定被处理过了。这里我们只说了 executor 对象,4大对象的其余三个对象也是这么生成的,我们就不一一讲了,有兴趣的同学可以翻看源码。
那么,我们就要看看该方法到底是如何实现的,让 mybatis 的 4 大对象都要调用该方法。
该方法循环了所有的拦截器,并调用了拦截器的 plugin 方法,每次都讲返回的 target 对象作为参数作为下一次调用。那么 plugin 方法的内容是什么呢?Interceptor 是个接口,在mybatis 源码中,只有2个实现类,我们找其中一个 ExamplePlugin 实现类看看源码实现:
该类实现了 Interceptor 接口,并重写了3个方法,其中就有我们关注的 plugin 方法,该方法内部很简单的调用了 Plugin.wrap(target, this) 方法,参数是 目标对象和自身,返回了和目标对象,我们该方法内部是如何实现的呢?
楼主只截取了一部分方法,该类实现类 JDK 动态代理中一个重要的接口 InvocationHandler 接口,而 wrap 方法是一个静态方法,通过传入的拦截器和目标对象,生成一个动态代理返回,注意,目标对象一定要实现某个接口,否则返回自身,我们看看代码实现。
- 调用自身的 getSignatureMap 方法,该方法获取了 Intercepts 注解上的 key 是 拦截的类型,value 是拦截的方法(多个)数据。并将数据包装成map返回。
- 获取目标对象的接口,并讲接口放进一个Set中并转成Class 数组返回。
- 根据上面生成的参数map,拦截器,目标对象,生成一个 puugin对象。
- 将生成 plugin 对象和接口和类加载器创建一个动态代理对象返回。
好了,我们知道了 plugin 方法的作用,也就是说,4 大对象都会调用该方法,都会将这些拦截器把自己包装起来,最后拦截自己。完成切面工作,比如日志。
那么,既然是实现类 JDK 的 InvocationHandler 接口,那么我们就要看看他的invoke 方法是怎么实现的:
该方法首先从刚刚从拦截器类 Intercepts 注解上取出的参数map中以目标方法的类作为key取出对应的方法集合,如果 invoke 方法和注解上定义的方法匹配,就执行拦截器的 intercept 方法,注意,此时,会创建一个Invocation 对象作为参数传递到 intercept 方法中,而这个对象的创建的参数包括 目标对象,代理拦截的方法,代理的参数。
我们回到 mybatis 中的拦截器例子 ExamplePlugin 类中看看 intercept 方法是如何实现的:
该方法只是调用了 invocation 的proceed 方法,那么该方法是如何定义的呢?
该方法只是用反射调用刚刚构造函数中的方法。并没有执行任何的操作。也就是说,在 Plugin 中的 invoke 方法中,调用了拦截器的 intercept 方法,并传入了 Invocation 对象,该对象的作用就是将目标对象,目标方法,目标方法参数传入,让拦截器可以取出这些参数并做加强工作。注意,需要在执行完加强操作和执行 Invocation 的 proceed 方法。也就是执行目标对象真正的方法。
到这里,我们已经弄懂了 mybatis 的拦截器原理,首先拦截器拦截的是 mybatis 的 4 大对象,我们需要在配置文件中配置拦截器,方便mybaits 添加到拦截器链中。mybatis 为我们提供了 Interceptor 接口,我们可以在该接口中实现自己的逻辑,主要需要实现 intercept 方法,在该方法中利用给定的 Invocation 对象来对我们的业务做一些增强。而调用拦截器方法的类就是 JDK 动态代理的接口 InvocationHandler 的实现类 Plugin 的invoke 方法,该方法会根据目标方法是否匹配拦截器注解的值来决定是否调用拦截器的 intercept 方法。并传入封装了目标对象,目标方法,目标方法参数的 Invocation 实例。
知道了拦截器的实现原理,那么我们就写一个例子来体验一下。
2. 拦截器的应用
首先编写 mybatis 插件需要遵守几个约定:
- 实现 Interceptor 接口并实现接口中的方法。
- 在配置文件中配置插件。
- 在实现 Interceptor 接口的类上加上 @Intercepts 注解。该注解如下:
仅有一个 Signature 注解集合,我们看看Signature 注解有哪些内容: