2.3MyBatis——插件机制

2.3MyBatis------插件机制

插件机制是一款优秀框架不可或缺的组成部分,比如spring、dubbo,还有我们要聊的Mybatis等等。所谓插件,通俗一点说,就是框架提供了一个入口,允许你通过实现框架提供的扩展接口,来进行功能增强。比如spring的前置处理器允许你在Bean创建的过程中添加自定义逻辑。

具体到Mybatis框架,它的核心是ORM和SQL的映射。那么映射成功的SQL在具体执行的过程中,存在许多时机,比如参数处理阶段,SQL语句处理阶段,SQL执行阶段,返回结果的处理阶段等。在这些阶段或者说执行点,如果框架提供了扩展点,那么我们就可以通过插件在相应的环节添加一些能力。

1.基本用法

因为Mybatis的插件并不像SQL映射那样被经常使用,所以先看一下插件的基本用法。根据Mybatis官方的文档说明,只要实现Interceptor接口,然后注册插件即可。

java 复制代码
//测试使用Springboot,插件放入Springboot容器后通过ObjectProvider注入到Configuration对象中完成注册
@Component 
@Intercepts({ // 声明作用点,即拦截哪些方法
        @Signature(
                type = Executor.class, // 这里拦截执行器的query方法
                method = "query", 
                args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}
        )
})
public class MyPlugin implements Interceptor {
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        System.out.println("自定义插件:统计运行时长");
        long begin = System.currentTimeMillis();
        Object result = invocation.proceed();
        long end = System.currentTimeMillis();
        System.out.println((end - begin) /1000 + "s");
        return result;
    }
}

这里定义拦截器的方式类似定义切点,type的常用取值为(下文会详细介绍):

  • Executor
  • StatementHandler
  • ParameterHandler
  • ResultSetHandler

这些接口涵盖了SQL执行的各个阶段。method取值则是type接口中对应的方法,如果方法存在重载,还可以通过args进行限定。

2.原理探究

当mapper接口中的方法执行时,插件被调用并统计时长。关于Mybatis插件,使用时很自然的疑问是:它是如何被加载注册,又是如何被调用执行的(即它的设计原理是什么)?如果你没有疑问,那就现在发出疑问...

然后我们从这两点去分析和学习。

2.1加载过程

我们知道在Mybatis中,不管是以哪种方式配置Mybatis,Mybatis的配置信息和mapper.xml的数据最终都会以Configuration对象的形式存在,Configuration它是Mybatis的核心组件之一。

如果不使用Springboot,集成Mybatis需要创建相应的配置文件,在配置文件中进行Mybatis的相关配置,包括插件注册。所以猜想插件信息也是被收集到Configuration对象中。我们浅浅看一下代码:

java 复制代码
// 1. configuration对象提供了添加拦截器的方法
public void addInterceptor(Interceptor interceptor) {
    interceptorChain.addInterceptor(interceptor);
  }
// 2 XML 配置文件解析时会调用addInterceptor,解析后通过add进行注册
pluginElement(root.evalNode("plugins"));// 解析plugins节点

// 3.1 springboot 项目中,通过ObjectProvider获取所有的Interceptor实现类,然后完成注册
public MybatisAutoConfiguration(ObjectProvider<Interceptor[]> interceptorsProvider,
      ....) {
    this.interceptors = interceptorsProvider.getIfAvailable();
    .....
  }
@Bean
@ConditionalOnMissingBean
 public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
...
if (!ObjectUtils.isEmpty(this.interceptors)) {
  	factory.setPlugins(this.interceptors);// 注册所有拦截器实现类
}
...
}

这里只需要明白一点,拦截器的实现类(即插件),最终都会通过addInterceptor被添加到核心组件Configuration对象的interceptorChain属性中。其他的诸如SQL映射(即mappedStatements)、转换器等都是同样的加载逻辑。

2.2执行过程

2.2Mybatis------代理与SQL映射这篇博客中,只聊到动态代理MapperProxy是如何将mapper接口与mapper.xml进行关联的。至于SQL的后续执行没有描述,下面我们通过研究插件的执行,顺带了解一下SQL的执行过程

2.2.1 插件的执行点

既然是介绍插件机制,自然是以插件的执行作为切入点。由于Mybatis的执行过程中涉及多个代理对象的调用,如果在调试过程中发现程序反复横跳,可以先不纠结,你应该至少能发现一点:在Configuration对象中,有多个方法调用了pluginAll

java 复制代码
public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {
    ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement, parameterObject, boundSql);
    // 遍历执行插件逻辑
    parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler);
    return parameterHandler;
  }
  // newResultSetHandler newStatementHandler newExecutor
  
// pluginAll
public Object pluginAll(Object target) {
  for (Interceptor interceptor : interceptors) {
    target = interceptor.plugin(target);
  }
  return target;
}

newResultSetHandler newStatementHandler newExecutor newParameterHandler四个方法中,都调用了pluginAll,也就是说在这些方法被调用时,都可以植入插件逻辑。

2.2.2 SQL执行的几个阶段

如果想要精确控制插件在SQL执行的哪个环节生效,就必须要知道Mybatis中SQL执行分几个阶段,并且每个阶段和newResultSetHandler newStatementHandler newExecutor newParameterHandler的对应关系。
SQL执行的先后顺序 :

  1. 创建执行器对象,对应 newExecutor()
  2. 创建SQL语句对象,对应 newStatementHandler()
  3. 参数处理器, 对应newParameterHandler()
  4. 结果集处理器,对应newParameterHandler()
    知道了各个阶段和对应的方法,这样才能精确的控制拦截器生效的时机。比如文章开头定义的插件,它将作用在newExecutor()执行时,即SQL执行的最开始阶段。

2.2.3 如何梳理出执行流程

前面提到,插件的执行流程中,由于涉及到多个代理对象的调用,所以调试流程不像普通的栈帧嵌套。调试过程如果觉得不顺,建议多去理解2.2Mybatis------代理与SQL映射这篇文章中关于JDK的代理实现。代理对象对接口方法的调用就是调用处理器中invoke方法的执行。明白了这一点,上述的插件执行点和SQL执行阶段都可以梳理出来。我们简单分析一下流程:

  1. MapperProxy确定SQL映射关系后,调用MapperMethod.execute方法,后面开始执行SQL
  2. 由于使用的是mybatis-spring-boot-starter依赖,所以这里的sqlSessionSqlSessionTemplate实例,可以看到,SqlSessionTemplate类中持有的sqlSessionProxy就是一个JDK代理对象,所以this.sqlSessionProxy.selectOne()实际是调用SqlSessionInterceptorinvoke方法
  3. 继续调试,当调用拦截器链的pluginAll()方法时,内部是调用拦截器的plugin方法。MyBatis内部有Plugin插件类,为啥自定义插件却要实现拦截器接口呢?(拦截器就是插件的逻辑部分)
  4. Plugin对象也是一个调用处理器,即上面代码中Plugin.wrap返回的是一个代理对象,在调用真实对象之前执行代理逻辑,这里的代理逻辑就是拦截器逻辑
  5. 看到这里你应该明白,方法依次返回后,pluginAll返回的是一个JDK代理对象,executor为例,代理对象代理了executor,且代理的逻辑就是在executor执行指定方法前,调用插件逻辑(第四步invoke逻辑) ,这就是Mybatis插件的原理
相关推荐
七星静香12 分钟前
laravel chunkById 分块查询 使用时的问题
java·前端·laravel
Jacob程序员13 分钟前
java导出word文件(手绘)
java·开发语言·word
ZHOUPUYU13 分钟前
IntelliJ IDEA超详细下载安装教程(附安装包)
java·ide·intellij-idea
stewie616 分钟前
在IDEA中使用Git
java·git
Elaine20239132 分钟前
06 网络编程基础
java·网络
G丶AEOM33 分钟前
分布式——BASE理论
java·分布式·八股
落落鱼201334 分钟前
tp接口 入口文件 500 错误原因
java·开发语言
想要打 Acm 的小周同学呀35 分钟前
LRU缓存算法
java·算法·缓存
镰刀出海38 分钟前
Recyclerview缓存原理
java·开发语言·缓存·recyclerview·android面试
阿伟*rui3 小时前
配置管理,雪崩问题分析,sentinel的使用
java·spring boot·sentinel