源码分析Mybatis插件(Plugin)机制与实战

本文涉及的产品
全局流量管理 GTM,标准版 1个月
云解析 DNS,旗舰版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: 源码分析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插件机制。

源码分析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技术内幕》作者。
本文来自中间件兴趣圈,了解相关信息可以关注中间件兴趣圈

目录
相关文章
|
1月前
|
SQL XML Java
8、Mybatis-Plus 分页插件、自定义分页
这篇文章介绍了Mybatis-Plus的分页功能,包括如何配置分页插件、使用Mybatis-Plus提供的Page对象进行分页查询,以及如何在XML中自定义分页SQL。文章通过具体的代码示例和测试结果,展示了分页插件的使用和自定义分页的方法。
8、Mybatis-Plus 分页插件、自定义分页
|
3天前
|
SQL Java 数据库连接
解决mybatis-plus 拦截器不生效--分页插件不生效
本文介绍了在使用 Mybatis-Plus 进行分页查询时遇到的问题及解决方法。依赖包包括 `mybatis-plus-boot-starter`、`mybatis-plus-extension` 等,并给出了正确的分页配置和代码示例。当分页功能失效时,需将 Mybatis-Plus 版本改为 3.5.5 并正确配置拦截器。
解决mybatis-plus 拦截器不生效--分页插件不生效
|
1月前
|
Java 数据库 Spring
MyBatisPlus分页插件在SpringBoot中的使用
这篇文章介绍了如何在Spring Boot项目中配置和使用MyBatis-Plus的分页插件,包括创建配置类以注册分页拦截器,编写测试类来演示如何进行分页查询,并展示了测试结果和数据库表结构。
MyBatisPlus分页插件在SpringBoot中的使用
|
22天前
|
SQL Java 数据库连接
Spring Boot联手MyBatis,打造开发利器:从入门到精通,实战教程带你飞越编程高峰!
【8月更文挑战第29天】Spring Boot与MyBatis分别是Java快速开发和持久层框架的优秀代表。本文通过整合Spring Boot与MyBatis,展示了如何在项目中添加相关依赖、配置数据源及MyBatis,并通过实战示例介绍了实体类、Mapper接口及Controller的创建过程。通过本文,你将学会如何利用这两款工具提高开发效率,实现数据的增删查改等复杂操作,为实际项目开发提供有力支持。
54 0
|
2月前
|
SQL 监控 Java
IDEA插件-Mybatis Log Free日志替换
MyBatis Log Free 是一个免费的用于在 IntelliJ IDEA 中显示 MyBatis 日志的插件。它可以帮助您更方便地查看和分析 MyBatis 的 SQL 执行情况,以及定位潜在的性能问题,提高开发效率。
204 0
IDEA插件-Mybatis Log Free日志替换
|
3月前
|
SQL 缓存 Java
Java框架之MyBatis 07-动态SQL-缓存机制-逆向工程-分页插件
Java框架之MyBatis 07-动态SQL-缓存机制-逆向工程-分页插件
|
5天前
|
缓存 前端开发 Java
【Java面试题汇总】Spring,SpringBoot,SpringMVC,Mybatis,JavaWeb篇(2023版)
Soring Boot的起步依赖、启动流程、自动装配、常用的注解、Spring MVC的执行流程、对MVC的理解、RestFull风格、为什么service层要写接口、MyBatis的缓存机制、$和#有什么区别、resultType和resultMap区别、cookie和session的区别是什么?session的工作原理
【Java面试题汇总】Spring,SpringBoot,SpringMVC,Mybatis,JavaWeb篇(2023版)
|
21天前
|
Java 数据库连接 测试技术
SpringBoot 3.3.2 + ShardingSphere 5.5 + Mybatis-plus:轻松搞定数据加解密,支持字段级!
【8月更文挑战第30天】在数据驱动的时代,数据的安全性显得尤为重要。特别是在涉及用户隐私或敏感信息的应用中,如何确保数据在存储和传输过程中的安全性成为了开发者必须面对的问题。今天,我们将围绕SpringBoot 3.3.2、ShardingSphere 5.5以及Mybatis-plus的组合,探讨如何轻松实现数据的字段级加解密,为数据安全保驾护航。
71 1
|
29天前
|
Web App开发 前端开发 关系型数据库
基于SpringBoot+Vue+Redis+Mybatis的商城购物系统 【系统实现+系统源码+答辩PPT】
这篇文章介绍了一个基于SpringBoot+Vue+Redis+Mybatis技术栈开发的商城购物系统,包括系统功能、页面展示、前后端项目结构和核心代码,以及如何获取系统源码和答辩PPT的方法。
|
1月前
|
Java 关系型数据库 MySQL
1、Mybatis-Plus 创建SpringBoot项目
这篇文章是关于如何创建一个SpringBoot项目,包括在`pom.xml`文件中引入依赖、在`application.yml`文件中配置数据库连接,以及加入日志功能的详细步骤和示例代码。