Mybatis plugin的使用及原理示例解析

 更新时间:2023年09月26日 08:51:29   作者:战斧  
这篇文章主要为大家介绍了 Mybatis plugin的使用及原理示例解析,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪

前言

上次,我们说过了mybatis+springboot时的启动与执行流程,也介绍过mybatis的执行器和缓存,今天,我们来看看mybatis 的另一个大功能 —— plugin

一、Mybatis Plugin 是什么

MyBatis的plugin插件是用来拦截SQL执行的,对SQL进行增强的一种机制。

MyBatis的Plugin实现基于JDK动态代理机制,在MyBatis初始化过程中,可以为指定的拦截对象生成代理对象,当拦截对象执行某个方法时,代理会先执行插件中的逻辑,再执行原有逻辑。插件可以在原有逻辑前后添加自己的逻辑或者完全替换原有逻辑

如果你使用过spring的话,会自然的想到spring的AOP特性,两者都是利用代理来实现功能的增强

二、Mybatis Plugin 的实例

这是一个旧项目,在后期对接Oracle后,有很多sql报了错,其原因是使用 instr() 函数时,由于参数是外部传入的,有时候可能会传来一个几千长度的字符串,从而导致instr 超长报错。因为这样的sql还有很多,不可能一一去改,所以必须使用功能增强的方式来解决

在直接上示例之前,我们先看看官方提供的接口 Interceptor.java ,只要实现了该接口,就可以在指定位置发挥作用

package org.apache.ibatis.plugin;
public interface Interceptor {
  /**
  * intercept方法就是要进行拦截的时候要执行的方法
  */
  Object intercept(Invocation invocation) throws Throwable;
  default Object plugin(Object target) {
    return Plugin.wrap(target, this);
  }
  default void setProperties(Properties properties) {
    // NOP
  }
}

当然,这个接口还需要配合另一个注解 @Intercepts 使用,我们结合案例写一个插件看看

@Component
@Intercepts({ @Signature(
        type = StatementHandler.class,
        method = "prepare",
        args = { Connection.class, Integer.class }) })
public class ExamplePlugin implements Interceptor {
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        StatementHandler statementHandler = (StatementHandler)invocation.getTarget();
        BoundSql boundSql = statementHandler.getBoundSql();
        String sql = boundSql.getSql();
        String newSql = null;
        // 改造sql部分省略,主要是将 instr() 拆分成 instr() or instr() 的形式以降低每个括号内	的长度...
        // newsql = sql.replace(...)
        // 把新的sql通过反射重新设置回去
        Field field = boundSql.getClass().getDeclaredField("sql");
        field.setAccessible(true);
        field.set(boundSql , newSql);
        Object returnVal = invocation.proceed();
        return returnVal;
    }
}

不难看出,配上注解后,该插件的意思就是针对 StatementHandler.prepare(Connection, Integer) 方法进行增强,我们实际运行下看看:

如图,最终走到了我们写的插件的 intercept 方法中

需要注意的是 @Intercepts 注解内支持配置 @Signature 数组,并以逗号分割。也就是说一个拦截器其实可以拦截多个类的方法,如下

@Intercepts({
        @Signature(
                type = ResultSetHandler.class,
                method = "handleResultSets", 
                args = {Statement.class}),
        @Signature(type = Executor.class,
                method = "query",
                args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})
})

三、Mybatis Plugin 原理

1. Mybatis 支持哪些 Plugin

其实从上面,案例看出,我们对插件的设置主要是通过 @Intercepts 内的 @Signature 注解实现的

@Intercepts({ @Signature(
        type = StatementHandler.class,
        method = "prepare",
        args = { Connection.class, Integer.class }) })

其中,type 就是作用的接口,method 和 args 则能确定唯一方法(单用方法名,可能有方法重载的情况)
但是,是不是这些属性可以随便填呢?其实不是的,mybatis没有做的那么自由,其更像Spring中的postProcessor机制,只在固定的几个位置有预留点,让你可以自定义增强,而不是开放所有位置

这里的Pulgin只针对以下四个接口有增强预留点,它们分别是

  • Statementhandler
    用于处理JDBC Statement对象的相关操作,将SQL语句中的占位符进行替换,然后使用Statement对象执行SQL语句
  • Resultsethandler
    主要负责将JDBC返回的ResultSet结果集转化为Java对象,然后返还给调用方
  • ParameterHandler
    主要用于处理Java对象与JDBC参数的映射,并将其转化为JDBC参数。
  • Executor
    更顶层的设计,能对上三种类进行调用,执行SQL语句,并获取执行结果

其具体调用链路如下:

2. myBatis 如何加载 Plugin

即我们自己创建了个 Interceptor 实现类,也使用了 @Intercepts 注解,但这个类是如何被mybatis加载的呢?

2.1 springboot 项目

我们仍以上篇文章的springboot+mybatis为例,那么此处便又要提到spring-boot的自动配置了,我们看下 MybatisAutoConfiguration (mybatis-spring-boot-autoconfigure2.1.4版本)这个自动配置类,看其构造方法

@org.springframework.context.annotation.Configuration
@ConditionalOnClass({ SqlSessionFactory.class, SqlSessionFactoryBean.class })
@ConditionalOnSingleCandidate(DataSource.class)
@EnableConfigurationProperties(MybatisProperties.class)
@AutoConfigureAfter({ DataSourceAutoConfiguration.class, MybatisLanguageDriverAutoConfiguration.class })
public class MybatisAutoConfiguration implements InitializingBean {
  // 省略部分代码
  public MybatisAutoConfiguration(MybatisProperties properties, ObjectProvider<Interceptor[]> interceptorsProvider,
      ObjectProvider<TypeHandler[]> typeHandlersProvider, ObjectProvider<LanguageDriver[]> languageDriversProvider,
      ResourceLoader resourceLoader, ObjectProvider<DatabaseIdProvider> databaseIdProvider,
      ObjectProvider<List<ConfigurationCustomizer>> configurationCustomizersProvider) {
    this.properties = properties;
    this.interceptors = interceptorsProvider.getIfAvailable();
    this.typeHandlers = typeHandlersProvider.getIfAvailable();
    this.languageDrivers = languageDriversProvider.getIfAvailable();
    this.resourceLoader = resourceLoader;
    this.databaseIdProvider = databaseIdProvider.getIfAvailable();
    this.configurationCustomizers = configurationCustomizersProvider.getIfAvailable();
  }
}

其构造方法的第二个参数 :ObjectProvider<Interceptor[]>

ObjectProvider 是在spring 4.3 引入的一种注入方式,它可以检索指定的类型。

然后通过 getIfAvailable 和 getIfUnique 从spring容器中检索出对应对象

因为我们已经在自定义的 ExamplePlugin 上使用了@Component 的注解,所以此处使用自动注入,能获取到我们的插件理所当然。

而后再把该值赋给 sqlSessionFactoryBean, 然后再赋给 mybatis 真正的配置类 Configuration。至此,我们的插件就被 mybatis 系统所成功加载了。

2.2 spring 项目

如果还没有使用上spring-boot,没有所谓的自动配置,那也无妨,只是需要手动额外配置一点参数也是同样的。

如:已经在 application.properties 配置了mybatis 配置文件

mybatis.config.location: classpath:/mybatis-config.xml

然后在mybatis-config.xml 里加上如下配置

<configuration>    
	<plugins>
        <plugin interceptor="com.zhanfu.spring.demo.utils.ExamplePlugin"/>
    </plugins>
</configuration>

这样也能达到,将指定插件放入 mybatis 框架的效果

3. Plugin 生效原理

上面我们讲了,如何写一个插件,以及插件是怎么交给 myBatis框架的,现在要谈最重要的内容了。即myBatis 是如何利用插件的。

上文我们已经了解到了,所有的插件实例都被放入了 myBatis 的总配置类 Configuration 去管理,成为了该类的一个属性interceptorChain ,该类详情如下:

public class InterceptorChain {
  // 所有的插件都存在这个 List 中
  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);
  }
}

所以我们只要看该类拿这些插件做了什么即可,可以看到,该类对所有新建的目标对象,都进行了 pluginAll 操作,结合上图,我们不难看出,该方法其实就是遍历所有插件,然后调用每个插件的 plugin() 方法 生成一个新对象,然后下一个插件拿这个新对象再 plugin() 生成一个新对象,实际上构成了一套链式的嵌套

那么plugin() 方法到底做了什么呢?我们来看看回头再来看看 Interceptor 接口里,该方法的默认实现

  // Interceptor.java
  default Object plugin(Object target) {
    return Plugin.wrap(target, this);
  }
  // Plugin.java
  public static Object wrap(Object target, Interceptor interceptor) {
    // 从插件的注解中,解析出该插件可作用的接口,以及该类下的哪些方法
    Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
    Class<?> type = target.getClass();
    // 找到插件可作用的接口和目标类的中所有重合的接口
    Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
    // 如果有重合的接口,则生成jdk代理并返回。注意,interceptor是我们的插件对象,signatureMap是插件注释解析到的类与方法
    if (interfaces.length > 0) {
      return Proxy.newProxyInstance(
          type.getClassLoader(),
          interfaces,
          new Plugin(target, interceptor, signatureMap));
    }
    // 如果没有,则返回原对象
    return target;
  }

综上,不难看出,只有生成指定的四种实例时,才会进入上述代码生成代理,最后返还的其实就是代理对象。需要注意的是,此时的代理是能够代理这些接口的所有方法的,要想实现指定方法才使用代理,还得依靠代理的 invoke 方法内去筛选

  // Plugin.java
  @Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    try {
      // 只有注解上指定方法才能走插件对象的 intercept 方法
      Set<Method> methods = signatureMap.get(method.getDeclaringClass());
      if (methods != null && methods.contains(method)) {
        return interceptor.intercept(new Invocation(target, method, args));
      }
      // 其他方法尽管经过代理,但其实什么也没做,直接调用原对象去了
      return method.invoke(target, args);
    } catch (Exception e) {
      throw ExceptionUtil.unwrapThrowable(e);
    }
  }

以上就是 Mybatis plugin 的使用及原理解析的详细内容,更多关于 Mybatis plugin使用原理的资料请关注脚本之家其它相关文章!

相关文章

  • springBoot2.6.2自动装配之注解源码解析

    springBoot2.6.2自动装配之注解源码解析

    对于springboot个人认为它就是整合了各种组件,然后提供对应的自动装配和启动器(starter),基于这个流程去实现一个定义的装配组件,下面这篇文章主要给大家介绍了关于springBoot2.6.2自动装配之注解源码解析的相关资料,需要的朋友可以参考下
    2022-01-01
  • Java中indexOf()方法详解及其日常使用举例

    Java中indexOf()方法详解及其日常使用举例

    这篇文章主要给大家介绍了关于Java中indexOf()方法详解及其日常使用举例的相关资料,indexOf()方法是JavaScript字符串的内置方法之一,它用于查找给定子字符串在原始字符串中第一次出现的位置,需要的朋友可以参考下
    2023-12-12
  • java N皇后实现问题解析

    java N皇后实现问题解析

    将 n 个皇后摆放在一个 n x n 的棋盘上,使得每一个皇后都无法攻击到其他皇后,N皇后问题是一个典型的约束求解问题,利用递归机制,可以很快的得到结果,本文将详细介绍,需要了解的朋友可以参考下
    2012-11-11
  • Java 浅复制和深复制的实例详解

    Java 浅复制和深复制的实例详解

    这篇文章主要介绍了Java 浅复制和深复制的实例详解的相关资料,这里提供实例帮助大家学习理解这部分内容,需要的朋友可以参考下
    2017-08-08
  • Java 超详细讲解字符流

    Java 超详细讲解字符流

    字符流就是在字节流的基础上,加上编码,形成的数据流,字符流出现的意义是因为字节流在操作字符时,可能会有中文导致的乱码,所以由字节流引申出了字符流
    2022-04-04
  • SpringBoot解析yml全流程详解

    SpringBoot解析yml全流程详解

    本文主要介绍了SpringBoot解析yml全流程详解,文中通过示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2022-03-03
  • Java中常用的设计模式之装饰器模式详解

    Java中常用的设计模式之装饰器模式详解

    这篇文章主要为大家详细介绍了Java中常用的设计模式之装饰器模式,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下,希望能够给你带来帮助
    2022-02-02
  • Java Map 按照Value排序的实现方法

    Java Map 按照Value排序的实现方法

    Map是键值对的集合接口,它的实现类主要包括:HashMap,TreeMap,Hashtable以及LinkedHashMap等。这篇文章主要介绍了Java Map 按照Value排序的实现方法,需要的朋友可以参考下
    2016-08-08
  • Spring 跨域配置请求详解

    Spring 跨域配置请求详解

    这篇文章主要介绍了Spring 跨域配置请求详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2019-01-01
  • Java支持方法重载的原因

    Java支持方法重载的原因

    今天给大家带来的是关于Java的相关知识,文章围绕着Java方法重载展开,文中有非常详细的介绍及代码示例,需要的朋友可以参考下
    2021-06-06

最新评论