源码分析Mybatis插件(Plugin)机制与实战
源码分析Mybatis系列目录:
1、源码分析Mybatis MapperProxy初始化之Mapper对象的扫描与构建
2、源码分析Mybatis MappedStatement的创建流程
3、Mybatis执行SQL的4大基础组件详解
4、源码解析MyBatis Sharding-Jdbc SQL语句执行流程详解
有了《Mybatis执行SQL的4大基础组件详解》 与 《源码解析MyBatis Sharding-Jdbc SQL语句执行流程详解》两篇文章的铺垫,本文将直奔主题:Mybatis插件机制。
温馨提示:本文也是以提问式阅读与探究源码的技巧展示。
1、回顾
从前面的文章我们已经知道,Mybatis在执行SQL语句的扩展点为Executor、StatementHandler、ParameterHandler与ResultSetHandler,我们本节将以Executor为入口,向大家展示Mybatis插件的扩展机制。
我们先来看回顾一下Mybatis Executor的创建入口。
1.1 Configuration#newExecutor
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); } if (cacheEnabled) { executor = new CachingExecutor(executor); } executor = (Executor) interceptorChain.pluginAll(executor); // @1 return executor; }
代码@1,:使用InterceptorChain.pluginAll(executor)进行拆件化处理。
思考:使用该方法调用后,会返回一个什么对象呢?如何自定义拆件,自定义插件如何执行呢?
那接下来我们带着上述疑问,从InterceptorChain类开始进行深入学习。
2、InterceptorChain
从名字上看其大意为拦截器链。
2.1 类图
- InterceptorChain
拦截器链,其内部维护一个interceptors,表示拦截器链中所有的拦截器,并提供增加或获取拦截器链的方法,下面会重点分析pluginAll方法。 - Interceptor
拦截器接口,用户自定义的拦截器需要实现该接口。 - Invocation
拦截器执行时的上下文环境,其实就是目标方法的调用信息,包含目标对象、调用的方法信息、参数信息。其中包含一个非常重要的方法:proceed。
public Object proceed() throws InvocationTargetException, IllegalAccessException { return method.invoke(target, args); }
该方法的主要目的就是进行处理链的传播,执行完拦截器的方法后,最终需要调用目标方法的invoke方法。
记下来中先重点分析一下InterceptorChain方法的pluginAll方法,因为从开头也知道,Mybatis在创建对象时,是调用该方法,完成目标对象的包装。
2.2 核心方法一览
2.2.1 pluginAll
public Object pluginAll(Object target) { // @1 for (Interceptor interceptor : interceptors) { // @2 target = interceptor.plugin(target); // @3 } return target; }
代码@1:目标对象,需要被代理的对象。
代码@2:遍历InterceptorChain的拦截器链,分别调用Intercpetor对象的Plugin进行拦截(@3)。
那接下来有三个疑问?
问题1:InterceptorChain中的interceptors是从什么时候初始化的呢,即拦截链中的拦截器从何而来。
问题2:从前面也得知,无论是创建Executor,还是创建StatementHandler等,都是调用InterceptorChain#pluginAll方法,那是不是拦截器中的拦截器都会作用与目标对象,这应该是有问题的,该如何处理?
问题3:代理对象是如何创建的。
2.2.1 addInterceptor
public void addInterceptor(Interceptor interceptor) { interceptors.add(interceptor); }
要想知道interceptors是如何初始化的,我们只需要查看该方法的调用链即可。
一路跟踪到源头,我们会发现在初始化SqlSessionFactory时,会解析一个标签plugin,就可以得知,会在SqlSessionFacotry的一个属性中配置所有的拦截器。
具体配置示例如下:
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="dataSource" ref="shardingDataSource"/> <property name="mapperLocations" value="classpath*:META-INF/mybatis/mappers/OrderMapper.xml"/> <property name="plugins"> <array> <bean id = "teneantInteceptor" class="com.demo.inteceptor.TenaInteceptor"></bean> </array> </property> </bean>
问题1已经解决。但后面两个问题似乎没有什么突破口。由于目前所涉及的三个类,显然不足以给我们提供答案,我们先将目光移到InterceptorChain所在包中的其他类,看看其他类的职责如何。
3、Intercepts与Signature
在org.apache.ibatis.plugin中存在如下两个注解类:Intercepts与Signature,从字面意思就是用来配置拦截的方法信息。
-
Siganature注解的属性说明如下:
- Class<?> type :需要拦截目标对象的类。
- String method:需要拦截目标类的方法名。
- Class<?>[] args:需要拦截目标类的方法名的参数类型签名。
备注:至于如何得知上述字段的含义,请看下文的Plugin#getSignatureMap方法。
但另外一个类型Plugin类确引起了我的注意。接下来我们将重点分析Plugin方法。
4、Plugin详解
4.1 Plugin类图
其中InvocationHandler为JDK的动态代理机制中的事件执行器,我们可以隐约阈值代理对象的生成将基于JDK内置的动态代理机制。
Plugin的核心属性如下:
- Object target
目标对象。 - Interceptor interceptor
拦截器对象。 - Map, Set< Method>> signatureMap
拦截器中的签名映射。
4.2 构造函数
private Plugin(Object target, Interceptor interceptor, Map<Class<?>, Set<Method>> signatureMap) { this.target = target; this.interceptor = interceptor; this.signatureMap = signatureMap; }
注意:其构造函数为私有的,那如何构建Plugin呢,其构造方法为Plugin的镜头方法wrap中被调用。
4.3 核心方法详解
4.3.1 wrap
public static Object wrap(Object target, Interceptor interceptor) { Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor); // @1 Class<?> type = target.getClass(); Class<?>[] interfaces = getAllInterfaces(type, signatureMap); // @2 if (interfaces.length > 0) { return Proxy.newProxyInstance( // @3 type.getClassLoader(), interfaces, new Plugin(target, interceptor, signatureMap)); } return target; }
代码@1:获取待包装的Interceptor的方法签名映射表,稍后详细分析。
代码@2:获取需要代理的对象的Class上声明的所有接口。
代码@3:使用JDK内置的Proxy创建代理对象。Proxy创建代理对象的方法声明如下:
public static Object newProxyInstance(ClassLoader loader,Class<?>[] interfaces,InvocationHandler h),
注意其事件处理器为Plugin,故在动态运行过程中会执行Plugin的invoker方法。
在进入Plugin#invoker方法学习之前,我们先重点查看一下getSignatureMap、getAllInterfaces的实现。
4.3.2 getSignatureMap
private static Map<Class<?>, Set<Method>> getSignatureMap(Interceptor interceptor) { Intercepts interceptsAnnotation = interceptor.getClass().getAnnotation(Intercepts.class); // @1 if (interceptsAnnotation == null) { // issue #251 // @2 throw new PluginException("No @Intercepts annotation was found in interceptor " + interceptor.getClass().getName()); } Signature[] sigs = interceptsAnnotation.value(); // @3 Map<Class<?>, Set<Method>> signatureMap = new HashMap<Class<?>, Set<Method>>(); for (Signature sig : sigs) { Set<Method> methods = signatureMap.get(sig.type()); if (methods == null) { methods = new HashSet<Method>(); signatureMap.put(sig.type(), methods); } try { Method method = sig.type().getMethod(sig.method(), sig.args()); methods.add(method); } catch (NoSuchMethodException e) { throw new PluginException("Could not find method on " + sig.type() + " named " + sig.method() + ". Cause: " + e, e); } } return signatureMap; }
代码@1:首先从Interceptor的类上获取Intercepts注解。
代码@2:如果Interceptor的类上没有定义Intercepts注解,则抛出异常,说明我们在自定义插件时,必须要有Intercepts注解。
代码@3:解析Interceptor的values属性(Signature[])数组,然后存入HashMap, Set< Method>>容器内。
温馨提示:从这里可以得知:自定义的插件必须定义Intercepts注解,其注解的value值为Signature。
4.3.3 getAllInterfaces
private static Class<?>[] getAllInterfaces(Class<?> type, Map<Class<?>, Set<Method>> signatureMap) { Set<Class<?>> interfaces = new HashSet<Class<?>>(); while (type != null) { for (Class<?> c : type.getInterfaces()) { if (signatureMap.containsKey(c)) { interfaces.add(c); } } type = type.getSuperclass(); } return interfaces.toArray(new Class<?>[interfaces.size()]); }
该方法的实现比较简单,并不是获取目标对象所实现的所有接口,而是返回需要拦截的方法所包括的接口。
4.3.4 invoke
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { // @1 try { Set<Method> methods = signatureMap.get(method.getDeclaringClass()); if (methods != null && methods.contains(method)) { // @2 return interceptor.intercept(new Invocation(target, method, args)); // @3 } return method.invoke(target, args); // @4 } catch (Exception e) { throw ExceptionUtil.unwrapThrowable(e); } }
代码@1:首先对其参数列表做一个简单的说明:
- Object proxy 当前的代理对象
- Method method 当前执行的方法
- Object[] args 当前执行方法的参数
代码@2:获取当前执行方法所属的类,并获取需要被拦截的方法集合。
代码@3:如果需被拦截的方法集合包含当前执行的方法,则执行拦截器的interceptor方法。
代码@4:如果不是,则直接调用目标方法的Invoke方法。
从该方法可以看出Interceptor接口的intercept方法就是拦截器自身需要实现的逻辑,其参数为Invocation,在该方法的结束,需要调用invocation#proceed()方法,进行拦截器链的传播。
从目前的学习中,我们已经了解了Plugin.wrap方法就是生成带来带来类的唯一入口,那该方法在什么地方调用呢?从代码类库中没有找到该方法的调用链,说明该方法是供用户调用的。
再看InterceptorChain方法的pluginAll方法:
public Object pluginAll(Object target) { // @1 for (Interceptor interceptor : interceptors) { // @2 target = interceptor.plugin(target); // @3 } return target; }
该方法会遍历用户定义的插件实现类(Interceptor),并调用Interceptor的plugin方法,对target进行拆件化处理,即我们在实现自定义的Interceptor方法时,在plugin中需要根据自己的逻辑,对目标对象进行包装(代理),创建代理对象,那我们就可以在该方法中使用Plugin#wrap来创建代理类。
接下来我们再来用序列图来对上述源码分析做一个总结:
看到这里,大家是否对上面提出的3个问题都已经有了自己的答案了。
问题1:InterceptorChain中的interceptors是从什么时候初始化的呢,即拦截链中的拦截器从何而来。
答:在初始化SqlSesstionFactory的时候,会解析属性plugins属性,会加载所有的拦截器到InterceptorChain中。
问题2:从前面也得知,无论是创建Executor,还是创建StatementHandler等,都是调用InterceptorChain#pluginAll方法,那是不是拦截器中的拦截器都会作用与目标对象,这应该是有问题的,该如何处理?
答案是在各自订阅的Interceptor#plugin方法中,我们可以根据传入的目标对象,是否是该拦截器关注的,如果不关注,则直接返回目标对象,如果关注,则使用Plugin#wrap方法创建代理对象。
问题3:代理对象是如何创建的?
代理对象是使用JDK的动态代理机制创建,使用Plugin#wrap方法创建。
5、实践
实践是检验真理的唯一标准,那到底如何使用Mybatis的插件机制呢?
创建自定义的拦截器Interceptor,实现Interceptor接口。
1)实现plugin方法,在该方法中决定是否需要创建代理对象,如果创建,使用Plugin#wrap方法创建。
2)实现interceptor方法,该方法中定义拦截器的逻辑,并且在最后请调用invocation.proceed()方法传递拦截器链。
3)使用Intercepts注解,定义需要拦截目标对象的方法签名,支持多个。
将实现的Interceptor在定义SqlSessionFactory的配置中,放入plugins属性。
最后给出一个Mybatis Plugin插件机制使用案例:基于Mycat+Mybatis的多租户方案:基于Mybatis与Mycat的多租户方式,通过Mybatis的插件机制,动态改写SQL语句来实现多租户
原文发布时间为:2019-05-30
本文作者:丁威,《RocketMQ技术内幕》作者。
本文来自中间件兴趣圈,了解相关信息可以关注中间件兴趣圈。

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
Java 13 新特性及实战案例
近期 Java 界好消息频传。先是 Java 13 发布,接着 Eclipse 也发布了新版本表示支持新版本的Java 特性。本文介绍了 Java 13 的新特性并展示了相关的示例。 2019年9月17日,Java 13 正式发布。特性如下。 Java 13 新特性 此版本带来了以下几大新特性: JEP 350,Dynamic CDS Archives:扩展应用程序类-数据共享,以允许在 Java 应用程序执行结束时动态归档类。归档类将包括默认的基础层 CDS(class data-sharing)存档中不存在的所有已加载的应用程序类和库类。 JEP 351,ZGC: Uncommit Unused Memory:增强 ZGC 以将未使用的堆内存返回给操作系统。 JEP 353,Reimplement the Legacy Socket API:使用易于维护和调试的更简单、更现代的实现替换 java.net.Socket 和 java.net.ServerSocket API 使用的底层实现。 JEP 354,Switch Expressions (Preview):可在生产环境中使用...
- 下一篇
Mybatis一二级缓存实现原理与使用指南
Mybatis 与 Hibernate 一样,支持一二级缓存。一级缓存指的是 Session 级别的缓存,即在一个会话中多次执行同一条 SQL 语句并且参数相同,则后面的查询将不会发送到数据库,直接从 Session 缓存中获取。二级缓存,指的是 SessionFactory 级别的缓存,即不同的会话可以共享。 缓存,通常涉及到缓存的写、读、过期(更新缓存)等几个方面,请带着这些问题一起来探究Mybatis关于缓存的实现原理吧。 提出问题:缓存的查询顺序,是先查一级缓存还是二级缓存? 本文以SQL查询与更新两个流程来揭开Mybatis缓存实现的细节。 源码分析Mybatis系列目录:1、源码分析Mybatis MapperProxy初始化之Mapper对象的扫描与构建2、源码分析Mybatis MappedStatement的创建流程3、Mybatis执行SQL的4大基础组件详解4、源码解析MyBatis Sharding-Jdbc SQL语句执行流程详解 1、从 SQL 查询流程看一二级缓存 温馨提示,本文不会详细介绍详细的 SQL 执行流程,如果对其感兴趣,可以查阅笔者的另外一篇文...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- CentOS8,CentOS7,CentOS6编译安装Redis5.0.7
- CentOS7,CentOS8安装Elasticsearch6.8.6
- SpringBoot2整合MyBatis,连接MySql数据库做增删改查操作
- SpringBoot2整合Redis,开启缓存,提高访问速度
- CentOS6,7,8上安装Nginx,支持https2.0的开启
- 2048小游戏-低调大师作品
- CentOS7设置SWAP分区,小内存服务器的救世主
- CentOS8编译安装MySQL8.0.19
- CentOS6,CentOS7官方镜像安装Oracle11G
- Docker使用Oracle官方镜像安装(12C,18C,19C)