Mybatis plugin 的使用及原理

举报
战斧 发表于 2023/09/21 18:52:04 2023/09/21
【摘要】 前言上次,我们说过了mybatis+springboot时的启动与执行流程,也介绍过mybatis的执行器和缓存,今天,我们来看看mybatis 的另一个大功能 —— plugin一、Mybatis Plugin 是什么MyBatis的plugin插件是用来拦截SQL执行的,对SQL进行增强的一种机制。MyBatis的Plugin实现基于JDK动态代理机制,在MyBatis初始化过程中,可以...

前言

上次,我们说过了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);
    }
  }
【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。