2.3MyBatis——插件机制
插件机制是一款优秀框架不可或缺的组成部分,比如spring、dubbo,还有我们要聊的Mybatis等等。所谓插件,通俗一点说,就是框架提供了一个入口,允许你通过实现框架提供的扩展接口,来进行功能增强。比如spring的前置处理器允许你在Bean创建的过程中添加自定义逻辑。
具体到Mybatis框架,它的核心是ORM和SQL的映射。那么映射成功的SQL在具体执行的过程中,存在许多时机,比如参数处理阶段,SQL语句处理阶段,SQL执行阶段,返回结果的处理阶段等。在这些阶段或者说执行点,如果框架提供了扩展点,那么我们就可以通过插件在相应的环节添加一些能力。
1.基本用法
因为Mybatis的插件并不像SQL映射那样被经常使用,所以先看一下插件的基本用法。根据Mybatis官方的文档说明,只要实现Interceptor接口,然后注册插件即可。
//测试使用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对象中。我们浅浅看一下代码:
// 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
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执行的先后顺序 :
- 创建执行器对象,对应
newExecutor()
- 创建SQL语句对象,对应
newStatementHandler()
- 参数处理器, 对应
newParameterHandler()
- 结果集处理器,对应
newParameterHandler()
知道了各个阶段和对应的方法,这样才能精确的控制拦截器生效的时机。比如文章开头定义的插件,它将作用在
newExecutor()
执行时,即SQL执行的最开始阶段。
2.2.3 如何梳理出执行流程
前面提到,插件的执行流程中,由于涉及到多个代理对象的调用,所以调试流程不像普通的栈帧嵌套。调试过程如果觉得不顺,建议多去理解2.2Mybatis——代理与SQL映射这篇文章中关于JDK的代理实现。
代理对象对接口方法的调用就是调用处理器中invoke方法的执行
。明白了这一点,上述的插件执行点和SQL执行阶段都可以梳理出来
。我们简单分析一下流程:
- MapperProxy确定SQL映射关系后,调用MapperMethod.execute方法,后面开始执行SQL
- 由于使用的是
mybatis-spring-boot-starter
依赖,所以这里的sqlSession
是SqlSessionTemplate
实例,可以看到,SqlSessionTemplate
类中持有的sqlSessionProxy
就是一个JDK代理对象,所以this.sqlSessionProxy.selectOne()
实际是调用SqlSessionInterceptor
的invoke
方法
- 继续调试,当调用拦截器链的
pluginAll()
方法时,内部是调用拦截器的plugin
方法。MyBatis内部有Plugin
插件类,为啥自定义插件却要实现拦截器接口呢?(拦截器就是插件的逻辑部分
)
Plugin
对象也是一个调用处理器,即上面代码中Plugin.wrap
返回的是一个代理对象,在调用真实对象之前执行代理逻辑,这里的代理逻辑就是拦截器逻辑
- 看到这里你应该明白,方法依次返回后,
pluginAll
返回的是一个JDK代理对象,以executor
为例,代理对象代理了executor
,且代理的逻辑就是在executor
执行指定方法前,调用插件逻辑(第四步invoke
逻辑),这就是Mybatis插件的原理。