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

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

有了 Mybatis执行SQL的4大基础组件详解源码解析MyBatis Sharding-Jdbc SQL语句执行流程详解两篇文章的铺垫,本文将直奔主题:Mybatis插件机制。


温馨提示:本文也是以提问式阅读与探究源码的技巧展示。


image.png

从前面的文章我们已经知道,Mybatis在执行SQL语句的扩展点为Executor、StatementHandler、ParameterHandler与ResultSetHandler,我们本节将以Executor为入口,向大家展示Mybatis插件的扩展机制。


我们先来看回顾一下Mybatis Executor的创建入口。


Configuration#newExecutor


1public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
 2  executorType = executorType == null ? defaultExecutorType : executorType;
 3  executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
 4  Executor executor;
 5  if (ExecutorType.BATCH == executorType) {
 6    executor = new BatchExecutor(this, transaction);
 7  } else if (ExecutorType.REUSE == executorType) {
 8    executor = new ReuseExecutor(this, transaction);
 9  } else {
10    executor = new SimpleExecutor(this, transaction);
11  }
12  if (cacheEnabled) {
13    executor = new CachingExecutor(executor);
14  }
15  executor = (Executor) interceptorChain.pluginAll(executor);   // @1
16  return executor;
17}

代码@1,:使用InterceptorChain.pluginAll(executor)进行拆件化处理。


思考:使用该方法调用后,会返回一个什么对象呢?如何自定义拆件,自定义插件如何执行呢?


那接下来我们带着上述疑问,从InterceptorChain类开始进行深入学习。

image.png

从名字上看其大意为拦截器链。


2.1 类图


646871cab38fef8017d8b391840f614d.jpg

  • InterceptorChain
    拦截器链,其内部维护一个interceptors,表示拦截器链中所有的拦截器,并提供增加或获取拦截器链的方法,下面会重点分析pluginAll方法。
  • Interceptor
    拦截器接口,用户自定义的拦截器需要实现该接口。
  • Invocation
    拦截器执行时的上下文环境,其实就是目标方法的调用信息,包含目标对象、调用的方法信息、参数信息。其中包含一个非常重要的方法:proceed。
1public Object proceed() throws InvocationTargetException, IllegalAccessException {
2  return method.invoke(target, args);
3}

该方法的主要目的就是进行处理链的传播,执行完拦截器的方法后,最终需要调用目标方法的invoke方法。


记下来中先重点分析一下InterceptorChain方法的pluginAll方法,因为从开头也知道,Mybatis在创建对象时,是调用该方法,完成目标对象的包装


2.2 核心方法一览


2.2.1 pluginAll


1public Object pluginAll(Object target) {   // @1
2  for (Interceptor interceptor : interceptors) {   // @2
3    target = interceptor.plugin(target);         
4  // @3
5  }
6  return target;
7}

代码@1:目标对象,需要被代理的对象。


代码@2:遍历InterceptorChain的拦截器链,分别调用Intercpetor对象的Plugin进行拦截(@3)。


那接下来有三个疑问?


问题1:InterceptorChain中的interceptors是从什么时候初始化的呢,即拦截链中的拦截器从何而来。


问题2:从前面也得知,无论是创建Executor,还是创建StatementHandler等,都是调用InterceptorChain#pluginAll方法,那是不是拦截器中的拦截器都会作用与目标对象,这应该是有问题的,该如何处理?


问题3:代理对象是如何创建的。


2.2.1 addInterceptor


1public void addInterceptor(Interceptor interceptor) {
2  interceptors.add(interceptor);
3}

要想知道interceptors是如何初始化的,我们只需要查看该方法的调用链即可。


一路跟踪到源头,我们会发现在初始化SqlSessionFactory时,会解析一个标签plugin,就可以得知,会在SqlSessionFacotry的一个属性中配置所有的拦截器。


具体配置示例如下:

1<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
 2    <property name="dataSource" ref="shardingDataSource"/>
 3    <property name="mapperLocations" value="classpath*:META-INF/mybatis/mappers/OrderMapper.xml"/>
 4
 5    <property name="plugins">
 6        <array>
 7            <bean id = "teneantInteceptor" class="com.demo.inteceptor.TenaInteceptor"></bean>
 8        </array>
 9    </property>
10</bean>

问题1已经解决。但后面两个问题似乎没有什么突破口。由于目前所涉及的三个类,显然不足以给我们提供答案,我们先将目光移到InterceptorChain所在包中的其他类,看看其他类的职责如何。

image.png

在org.apache.ibatis.plugin中存在如下两个注解类:Intercepts与Signature,从字面意思就是用来配置拦截的方法信息。

08467c8c4be13a612b9bae9c7e3ee154.jpg

  • Siganature注解的属性说明如下:
  • Class type :需要拦截目标对象的类。
  • String method:需要拦截目标类的方法名。
  • Class[] args:需要拦截目标类的方法名的参数类型签名。


备注:至于如何得知上述字段的含义,请看下文的Plugin#getSignatureMap方法。

但另外一个类型Plugin类确引起了我的注意。接下来我们将重点分析Plugin方法。

image.png

4.1 Plugin类图


828bc9b1bb7370b99ac809d4cb7b10a7.jpg

其中InvocationHandler为JDK的动态代理机制中的事件执行器,我们可以隐约阈值代理对象的生成将基于JDK内置的动态代理机制。


Plugin的核心属性如下:


  • Object target
    目标对象。
  • Interceptor interceptor
    拦截器对象。
  • Map, Set< Method>> signatureMap
    拦截器中的签名映射。


4.2  构造函数


1private Plugin(Object target, Interceptor interceptor, Map<Class<?>, Set<Method>> signatureMap) {
2    this.target = target;
3    this.interceptor = interceptor;
4    this.signatureMap = signatureMap;
5  }

注意:其构造函数为私有的,那如何构建Plugin呢,其构造方法为Plugin的镜头方法wrap中被调用。


4.3 核心方法详解


4.3.1 wrap


1public static Object wrap(Object target, Interceptor interceptor) {
 2  Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);  // @1
 3  Class<?> type = target.getClass();   
 4  Class<?>[] interfaces = getAllInterfaces(type, signatureMap);   // @2
 5  if (interfaces.length > 0) {
 6    return Proxy.newProxyInstance(    // @3
 7        type.getClassLoader(),
 8        interfaces,
 9        new Plugin(target, interceptor, signatureMap));
10  }
11  return target;
12}

代码@1:获取待包装的Interceptor的方法签名映射表,稍后详细分析。


代码@2:获取需要代理的对象的Class上声明的所有接口。


代码@3:使用JDK内置的Proxy创建代理对象。Proxy创建代理对象的方法声明如下:


1public static Object newProxyInstance(ClassLoader loader,Class<?>[] interfaces,InvocationHandler h),
2

注意其事件处理器为Plugin,故在动态运行过程中会执行Plugin的invoker方法。


在进入Plugin#invoker方法学习之前,我们先重点查看一下getSignatureMap、getAllInterfaces的实现。


4.3.2 getSignatureMap


1private static Map<Class<?>, Set<Method>> getSignatureMap(Interceptor interceptor) {
 2  Intercepts interceptsAnnotation = interceptor.getClass().getAnnotation(Intercepts.class);  // @1
 3  if (interceptsAnnotation == null) { // issue #251                                          // @2
 4    throw new PluginException("No @Intercepts annotation was found in interceptor " + interceptor.getClass().getName());      
 5  }
 6  Signature[] sigs = interceptsAnnotation.value();   // @3
 7  Map<Class<?>, Set<Method>> signatureMap = new HashMap<Class<?>, Set<Method>>(); 
 8  for (Signature sig : sigs) {
 9    Set<Method> methods = signatureMap.get(sig.type());
10    if (methods == null) {
11      methods = new HashSet<Method>();
12      signatureMap.put(sig.type(), methods);
13    }
14    try {
15      Method method = sig.type().getMethod(sig.method(), sig.args());    
16      methods.add(method);
17    } catch (NoSuchMethodException e) {
18      throw new PluginException("Could not find method on " + sig.type() + " named " + sig.method() + ". Cause: " + e, e);
19    }
20  }
21  return signatureMap;
22}

代码@1:首先从Interceptor的类上获取Intercepts注解。


代码@2:如果Interceptor的类上没有定义Intercepts注解,则抛出异常,说明我们在自定义插件时,必须要有Intercepts注解。


代码@3:解析Interceptor的values属性(Signature[])数组,然后存入HashMap, Set< Method>>容器内。


温馨提示:从这里可以得知:自定义的插件必须定义Intercepts注解,其注解的value值为Signature。


4.3.3 getAllInterfaces


1private static Class<?>[] getAllInterfaces(Class<?> type, Map<Class<?>, Set<Method>> signatureMap) {
 2  Set<Class<?>> interfaces = new HashSet<Class<?>>();
 3  while (type != null) {
 4    for (Class<?> c : type.getInterfaces()) {
 5      if (signatureMap.containsKey(c)) {
 6        interfaces.add(c);
 7      }
 8    }
 9    type = type.getSuperclass();
10  }
11  return interfaces.toArray(new Class<?>[interfaces.size()]);
12}

该方法的实现比较简单,并不是获取目标对象所实现的所有接口,而是返回需要拦截的方法所包括的接口。


4.3.4 invoke


1public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { // @1
 2  try {
 3    Set<Method> methods = signatureMap.get(method.getDeclaringClass());
 4    if (methods != null && methods.contains(method)) {   // @2
 5      return interceptor.intercept(new Invocation(target, method, args));   // @3
 6    }
 7    return method.invoke(target, args);                           // @4
 8  } catch (Exception e) {
 9    throw ExceptionUtil.unwrapThrowable(e);
10  }
11}

代码@1:首先对其参数列表做一个简单的说明:


  • Object proxy 当前的代理对象
  • Method method 当前执行的方法
  • Object[] args 当前执行方法的参数


代码@2:获取当前执行方法所属的类,并获取需要被拦截的方法集合。


代码@3:如果需被拦截的方法集合包含当前执行的方法,则执行拦截器的interceptor方法。


代码@4:如果不是,则直接调用目标方法的Invoke方法。


从该方法可以看出Interceptor接口的intercept方法就是拦截器自身需要实现的逻辑,其参数为Invocation,在该方法的结束,需要调用invocation#proceed()方法,进行拦截器链的传播。


从目前的学习中,我们已经了解了Plugin.wrap方法就是生成带来带来类的唯一入口,那该方法在什么地方调用呢?从代码类库中没有找到该方法的调用链,说明该方法是供用户调用的。


再看InterceptorChain方法的pluginAll方法:


1public Object pluginAll(Object target) {   // @1
2  for (Interceptor interceptor : interceptors) {   // @2
3    target = interceptor.plugin(target);           // @3
4  }
5  return target;
6}

该方法会遍历用户定义的插件实现类(Interceptor),并调用Interceptor的plugin方法,对target进行拆件化处理,即我们在实现自定义的Interceptor方法时,在plugin中需要根据自己的逻辑,对目标对象进行包装(代理),创建代理对象,那我们就可以在该方法中使用Plugin#wrap来创建代理类。


接下来我们再来用序列图来对上述源码分析做一个总结:

4d52e00ddd64e287f4ddf91702f46866.jpg

看到这里,大家是否对上面提出的3个问题都已经有了自己的答案了。


问题1:InterceptorChain中的interceptors是从什么时候初始化的呢,即拦截链中的拦截器从何而来。

答:在初始化SqlSesstionFactory的时候,会解析属性plugins属性,会加载所有的拦截器到InterceptorChain中。


问题2:从前面也得知,无论是创建Executor,还是创建StatementHandler等,都是调用InterceptorChain#pluginAll方法,那是不是拦截器中的拦截器都会作用与目标对象,这应该是有问题的,该如何处理?

答案是在各自订阅的Interceptor#plugin方法中,我们可以根据传入的目标对象,是否是该拦截器关注的,如果不关注,则直接返回目标对象,如果关注,则使用Plugin#wrap方法创建代理对象。


问题3:代理对象是如何创建的?


代理对象是使用JDK的动态代理机制创建,使用Plugin#wrap方法创建。

image.png

实践是检验真理的唯一标准,那到底如何使用Mybatis的插件机制呢?


创建自定义的拦截器Interceptor,实现Interceptor接口。


  • 实现plugin方法,在该方法中决定是否需要创建代理对象,如果创建,使用Plugin#wrap方法创建。
  • 实现interceptor方法,该方法中定义拦截器的逻辑,并且在最后请调用invocation.proceed()方法传递拦截器链。
  • 使用Intercepts注解,定义需要拦截目标对象的方法签名,支持多个。
    将实现的Interceptor在定义SqlSessionFactory的配置中,放入plugins属性。


最后给出一个Mybatis Plugin插件机制使用案例:基于Mycat+Mybatis的多租户方案,通过Mybatis的插件机制,动态改写SQL语句来实现多租户,其链接直接指向作者的CSDN博客:


https://blog.csdn.net/prestigeding/article/details/52662426

相关文章
|
5月前
|
Java 数据库连接 数据库
Spring boot 使用mybatis generator 自动生成代码插件
本文介绍了在Spring Boot项目中使用MyBatis Generator插件自动生成代码的详细步骤。首先创建一个新的Spring Boot项目,接着引入MyBatis Generator插件并配置`pom.xml`文件。然后删除默认的`application.properties`文件,创建`application.yml`进行相关配置,如设置Mapper路径和实体类包名。重点在于配置`generatorConfig.xml`文件,包括数据库驱动、连接信息、生成模型、映射文件及DAO的包名和位置。最后通过IDE配置运行插件生成代码,并在主类添加`@MapperScan`注解完成整合
894 1
Spring boot 使用mybatis generator 自动生成代码插件
|
6月前
|
SQL Java 数据安全/隐私保护
发现问题:Mybatis-plus的分页总数为0,分页功能失效,以及多租户插件的使用。
总的来说,使用 Mybatis-plus 确实可以极大地方便我们的开发,但也需要我们理解其工作原理,掌握如何合适地使用各种插件。分页插件和多租户插件是其中典型,它们的运用可以让我们的代码更为简洁、高效,理解和掌握好它们的用法对我们的开发过程有着极其重要的意义。
635 15
|
9月前
|
XML SQL Java
十二、MyBatis分页插件
十二、MyBatis分页插件
245 17
|
8月前
|
SQL Java 数据库连接
MyBatis 实现分页的机制
MyBatis 的分页机制主要依赖于 `RowBounds` 对象和分页插件。`RowBounds` 实现内存分页,适合小数据量场景,通过设定偏移量和限制条数对结果集进行筛选。而针对大数据量,则推荐使用分页插件(如 PageHelper),实现物理分页。插件通过拦截 SQL 执行,动态修改语句添加分页逻辑,支持多种数据库方言。配置插件后,无需手动调整查询方法即可完成分页操作,提升性能与灵活性。
173 0
|
12月前
|
SQL Java 数据库连接
【MyBatisPlus·最新教程】包含多个改造案例,常用注解、条件构造器、代码生成、静态工具、类型处理器、分页插件、自动填充字段
MyBatis-Plus是一个MyBatis的增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生。本文讲解了最新版MP的使用教程,包含多个改造案例,常用注解、条件构造器、代码生成、静态工具、类型处理器、分页插件、自动填充字段等核心功能。
1806 5
【MyBatisPlus·最新教程】包含多个改造案例,常用注解、条件构造器、代码生成、静态工具、类型处理器、分页插件、自动填充字段
|
12月前
|
SQL Java 数据库连接
Mybatis架构原理和机制,图文详解版,超详细!
MyBatis 是 Java 生态中非常著名的一款 ORM 框架,在一线互联网大厂中应用广泛,Mybatis已经成为了一个必会框架。本文详细解析了MyBatis的架构原理与机制,帮助读者全面提升对MyBatis的理解和应用能力。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
Mybatis架构原理和机制,图文详解版,超详细!
|
11月前
|
缓存 Java 数据库连接
深入探讨:Spring与MyBatis中的连接池与缓存机制
Spring 与 MyBatis 提供了强大的连接池和缓存机制,通过合理配置和使用这些机制,可以显著提升应用的性能和可扩展性。连接池通过复用数据库连接减少了连接创建和销毁的开销,而 MyBatis 的一级缓存和二级缓存则通过缓存查询结果减少了数据库访问次数。在实际应用中,结合具体的业务需求和系统架构,优化连接池和缓存的配置,是提升系统性能的重要手段。
426 4
|
11月前
|
缓存 Java 数据库连接
MyBatis缓存机制
MyBatis提供两级缓存机制:一级缓存(Local Cache)默认开启,作用范围为SqlSession,重复查询时直接从缓存读取;二级缓存(Second Level Cache)需手动开启,作用于Mapper级别,支持跨SqlSession共享数据,减少数据库访问,提升性能。
190 1
|
8月前
|
XML Java 数据库连接
微服务——SpringBoot使用归纳——Spring Boot集成MyBatis——基于注解的整合
本文介绍了Spring Boot集成MyBatis的两种方式:基于XML和注解的形式。重点讲解了注解方式,包括@Select、@Insert、@Update、@Delete等常用注解的使用方法,以及多参数时@Param注解的应用。同时,针对字段映射不一致的问题,提供了@Results和@ResultMap的解决方案。文章还提到实际项目中常结合XML与注解的优点,灵活使用两者以提高开发效率,并附带课程源码供下载学习。
648 0
|
10月前
|
前端开发 Java 数据库连接
Java后端开发-使用springboot进行Mybatis连接数据库步骤
本文介绍了使用Java和IDEA进行数据库操作的详细步骤,涵盖从数据库准备到测试类编写及运行的全过程。主要内容包括: 1. **数据库准备**:创建数据库和表。 2. **查询数据库**:验证数据库是否可用。 3. **IDEA代码配置**:构建实体类并配置数据库连接。 4. **测试类编写**:编写并运行测试类以确保一切正常。
415 2