【框架原理】mybatis 插件机制
插件
根据功能,可以将插件分为两大类:
-
第一类:插件是对系统的一种补充,例如在分布式系统中,可以使用插件的方式,实现内存插件、磁盘插件、线性网络插件、Paxos插件等。此类插件等同于组件。
-
第二类:插件是对系统默认功能的自定义修改,例如mybatis里面自定义插件,它实现的拦截器的功能。此类插件等同于拦截器。
MyBatis拦截器插件
mybatis里面自定义插件属于拦截器插件,大凡拦截器(Interceptor)都需要关注两个要点:
- 拦截的对象是谁,用代码行话来说,target目标是谁?
- 拦截的技术方案是什么?
拦截的对象
我们知道,MyBatis有四大核心对象:
- ParameterHandler:处理SQL的参数对象
- ResultSetHandler:处理SQL的返回结果集
- StatementHandler:数据库的处理对象,用于执行SQL语句
- Executor:MyBatis的执行器,用于执行增删改查操作
那么,MyBatis拦截器针对的对象就是上面“四大金刚”。
拦截的技术方案
在Java里面,我们想拦截某个对象,只需要把这个对象包装一下,用代码行话来说,就是重新生成一个代理对象。
下面,我们将代理对象成为 “变身”、将原生对象称为 “原生”
也就是说,一旦配置上插件,ParameterHandler,ResultSetHandler,StatementHandler,Executor这四大核心对象,将会生成 “变身”,是一种代理对象,而不再是 “原身”。
MyBatis拦截器插件整个运行过程
没有插件的运行过程
有插件的运行过程
可以可以理解了吧?
一旦配置上插件,ParameterHandler,ResultSetHandler,StatementHandler,Executor这四大核心对象,将会生成 “变身”,是一种代理对象,而不再是 “原身”。
时序图
下面时序图更加清晰反映情况,
如下是时序图,在整个时序图中,涉及到mybatis插件部分已标红,基本上就是体现在上文中提到的四个类上,对这些类上的方法进行拦截。
运行过程详细的实现机制
实现分三步:插件配置信息的加载、代理对象的生成、拦截逻辑的执行
插件配置信息的加载
假设是基于xml配置,有如下配置
<plugins>
<plugin interceptor="org.apache.ibatis.builder.ExamplePlugin">
<property name="plubinProperty" value="100">
</plugin>
</plugins>
那么通过XMLConfigBuilder解析。
- XMLConfigBuilder首先是读取配置,读到
<plugins>
标签会进入循环。 - 循环里面,为每一个
<plugin>
标签创建Interceptor对象实例 (比如上面的ExamplePlugin
) - 设置Interceptor属性变量 (比如上面的
plubinProperty
) - 最后把Interceptor对象添加到
Configuration的InterceptorChain
中
(InterceptorChain是MyBatis提供的一个类,用来管理一堆Interceptor的)
这里需要和Spring的Interceptor做区分
除了实现原理上是一样的外,MyBatis的拦截器链和Spring的拦截器没有半毛钱关系
代理对象的生成
以Executor为例
我们知道,MyBatis是通过SqlSession实现数据库访问的。
而SqlSession是通过SqlSessionFactory创建的。
在SqlSessionFactory创建SqlSession实例的过程中,就调用了我们上面提到的Configuration类来创建Executor,进而将Executor放入SqlSession的构造方法中。
而这个方法会调用上面提到的InterceptorChain的plugAll方法
而plugAll方法很简单,就是遍历拦截器链中的拦截器,调用拦截器的plugin方法对目标(本例的目标为Executor)进行封装。
最后把目标给返回
最后,拦截器(Interceptor)是个接口,而接口实现是根据业务情况决定的,因此原理部分就不需要继续了解每个接口的实现了。
代理对象的生成(总结)
可以总结为下面这幅图。
- SqlSessionFactory创建SqlSession期间,调用Configuration
- Configuration对InterceptorChain中的Interceptor进行遍历
- 遍历过程中,对符合条件的Executor进行封装,生成Executor代理对象
当然,除了创建SqlSession,还有其他情况会创建 Executor
至于其他的核心组件(ParameterHandler、ResultSetHandler、StatementHandle)
则是在Executor的被创建后,在不同阶段下被创建出来的联系上面这幅图
至于代理的创建,原理是一样的,均是调用 configuration.pluginAll() 方法
拦截逻辑的执行
拦截逻辑的执行看源码最清晰
拦截逻辑相关的部分源码
首先,看Mybatis提供的拦截器有些什么方法
package org.apache.ibatis.plugin;
import java.util.Properties;
/**
* MyBatis 拦截器接口源码
*
* @author Clinton Begin
*/
public interface Interceptor {
/**
* 这个方法是mybatis的核心方法
* 要实现自定义逻辑,基本都是改造这个方法,即具体的业务逻辑编写在这里
*
* @param invocation 可以通过反射要获取原始方法和对应参数信息
* 如:invocation.getTarget()可以获取对象原型
* @return 代理方法执行后的返回值。
* @throws Throwable 可能调用任何方法,因此可能抛出任意异常
*/
Object intercept(Invocation invocation) throws Throwable;
/**
* 作用是用来生成代理对象,使得被代理的对象一定会经过intercept方法
* 通常都会使用mybatis提供的工具类Plugin来获取代理对象
* 如:return Plugin.wrap(o, this);
* 如果有自己独特需求,可以自定义
*
* @param o 被代理对象的原型。
* 可能是:Executor、StatementHandler、ParemeterHandler、ResultHandler、甚至可以是前面四种类型的代理对象。
* @return 对o进行封装后的代理对象,
* 如果通过Plugin来构造代理对象:return Plugin.wrap(o, this);
* 那么返回的是 Plugin 类型或者 原类型
*/
Object plugin(Object target);
/**
* 就是用来设置插件的一些属性
*
* @param properties 解析出来的配置信息
*/
void setProperties(Properties properties);
}
由于真正去执行Executor、ParameterHandler、ResultSetHandler和StatementHandler类中的方法的对象是代理对象,而在执行方法时,首先调用的是Plugin类的invoke方法。因此我们还需要了解 Plugin类的运行原理。
下面是 Plugin 类的部分源码
// 继承java.lang.reflect.InvocationHandler
public class Plugin implements InvocationHandler {
// 自定义的插件,在plugin()中通常调用这个wrap方法。
// 这个wrap方法会返回Plugin类型对象,或者原target类型(当发现插件类型不匹配时)
public static Object wrap(Object target, Interceptor interceptor) {
Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
Class<?> type = target.getClass();
Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
if (interfaces.length > 0) {
// 当发现时匹配的类型时,返回Plugin类型封装的代理类
return Proxy.newProxyInstance(
type.getClassLoader(),
interfaces,
new Plugin(target, interceptor, signatureMap));
}
return target;
}
// 上面wrap方法,把代理对象封装为Plugin类型
// 所以在执行代理类方法时,首先调用的是Plugin类(实现了InvocationHandler接口)的invoke方法,如下:
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
Set<Method> methods = signatureMap.get(method.getDeclaringClass());
if (methods != null && methods.contains(method)) {
// 当发现是匹配的方法时,
// 调用interceptor的自定义方法intercept,执行业务逻辑
return interceptor.intercept(new Invocation(target, method, args));
}
// 否者调用原来的方法
return method.invoke(target, args);
} catch (Exception e) {
throw ExceptionUtil.unwrapThrowable(e);
}
}
拦截逻辑(总结)
-
首先根据执行方法所属类获取拦截器中声明需要拦截的方法集合;
-
判断当前方法需不需要执行拦截逻辑,需要的话,执行拦截逻辑方法(即Interceptor接口的intercept方法实现),不需要则直接执行原方法。
这里我们以执行executor对象的query方法为例,且假设有两个拦截器存在:
自定义MyBatis拦截器插件
自定义拦截器定位到目标需要指明两点:
- 拦截的对象
- 拦截的方法(包括参数)
然后对于前面讲的三个流程实现,mybatis均提供了接口方法,让我们自定义。
- 插件配置信息的加载
- 代理对象的生成
- 拦截逻辑的执行
于是,我们自定义mybatis拦截器插件,实质上就是需要把上面5个点给实现了。
指定拦截的对象、方法
可以拦截的对象也就那四大对象,每个对象有各自可以拦截的方法:
- 执行器Executor(update、query、commit、rollback等方法);
- 参数处理器ParameterHandler(getParameterObject、setParameters方法);
- 结果集处理器ResultSetHandler(handleResultSets、handleOutputParameters等方法);
- SQL语法构建器StatementHandler(prepare、parameterize、batch、update、query等方法);
每个方法还想需要指定对应的参数,来区分方法重载
具体每个对象有哪些方法,只需需要点到源码查找即可。
以Executor为例
至于每个方法的作用,可以通过SqlSession的实现上略知一二。
如:
DefaultSqlSession 有关于 executor.queryCursor的使用和注解
全面了解,则需要到网上找文章
如:
MyBatis 核心配置综述之Executor
mybatis四大接口之 Executor
(相对的,官网上找了很久,反而没找到此类介绍 😦)
指定插件的配置信息、生成方法、业务逻辑
直接以一个完整的代码作为例子吧
package com.vshop.veat.plugins;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.springframework.stereotype.Component;
import java.util.Arrays;
import java.util.Properties;
/**
* @author alan smith
* @version 1.0
* @date 2020/4/13 19:55
*/
@Intercepts({
// 拦截情况的签名。当情况符合,便进行拦截。
@Signature(
// 指定要拦截的类型,这里拦截执行器类型
type = Executor.class,
// 拦截执行器中更新的方法
method = "query",
args = {
MappedStatement.class, Object.class, RowBounds.class , ResultHandler.class
}
)
})
@Slf4j
@Component
public class MyInterceptor implements Interceptor {
/**
* 这个方法是mybatis的核心方法
* 要实现自定义逻辑,基本都是改造这个方法,即具体的业务逻辑编写在这里
*
* @param invocation 可以通过反射要获取原始方法和对应参数信息
* @return 代理方法执行后的返回值。
* @throws Throwable 可能调用任何方法,因此可能抛出任意异常
*/
@Override
public Object intercept(Invocation invocation) throws Throwable {
log.info("开始拦截....");
Object result = invocation.proceed();
log.info("结束拦截....");
return result ;
}
/**
* 作用是用来生成代理对象,使得被代理的对象一定会经过intercept方法
* 通常都会使用mybatis提供的工具类Plugin来获取代理对象
* 如:return Plugin.wrap(o, this);
* 如果有自己独特需求,可以自定义
*
* @param o 被代理对象的原型。
* 可能是:Executor、StatementHandler、ParemeterHandler、ResultHandler、甚至可以是前面四种类型的代理对象。
* @return 对o进行封装后的代理对象,
* 如果通过Plugin来构造代理对象:return Plugin.wrap(o, this);
* 那么返回的是 Plugin 类型或者 原类型
*/
@Override
public Object plugin(Object o) {
// 对目标对象进行包装,创建target对象的代理对象
// 目的是将当前拦截器加入到对象中,就可以执行拦截器
log.info("生成代理对象");
return Plugin.wrap(o, this);
}
/**
* 就是用来设置插件的一些属性
*
* @param properties 解析出来的配置信息
*/
@Override
public void setProperties(Properties properties) {
//Interceptor.super.setProperties(properties);//NOP
log.debug("插件配置的信息:{}", Arrays.toString(properties.stringPropertyNames().toArray()));
}
}
打印的数据
2020-04-14 16:28:35.895 [INFO ] [main] com.vshop.veat.plugins.MyInterceptor - 生成代理对象
2020-04-14 16:28:35.900 [INFO ] [main] com.vshop.veat.plugins.MyInterceptor - 开始拦截....
2020-04-14 16:28:35.908 [INFO ] [main] com.vshop.veat.plugins.MyInterceptor - 生成代理对象
2020-04-14 16:28:35.928 [INFO ] [main] com.vshop.veat.plugins.MyInterceptor - 生成代理对象
2020-04-14 16:28:35.929 [INFO ] [main] com.vshop.veat.plugins.MyInterceptor - 生成代理对象
2020-04-14 16:28:35.938 [DEBUG] [main] com.vshop.veat.mapper.ProductCategoryMapper.selectById - ==> Preparing: SELECT product_category_id AS id,product_category_name AS name,product_category_type AS type,create_time,update_time FROM ve_product_category WHERE product_category_id=?
2020-04-14 16:28:35.966 [DEBUG] [main] com.vshop.veat.mapper.ProductCategoryMapper.selectById - ==> Parameters: 1(Integer)
2020-04-14 16:28:36.036 [DEBUG] [main] com.vshop.veat.mapper.ProductCategoryMapper.selectById - <== Total: 1
2020-04-14 16:28:36.036 [INFO ] [main] com.vshop.veat.plugins.MyInterceptor - 结束拦截....
小结
简单的说,mybatis插件就是对ParameterHandler、ResultSetHandler、StatementHandler、Executor这四个接口上的方法进行拦截,利用JDK动态代理机制,为这些接口的实现类创建代理对象,在执行方法时,先去执行代理对象的方法,从而执行自己编写的拦截逻辑,所以真正要用好mybatis插件,主要还是要熟悉这四个接口的方法以及这些方法上的参数的含义;
另外,如果配置了多个拦截器的话,会出现层层代理的情况,即代理对象代理了另外一个代理对象,形成一个代理链条,执行的时候,也是层层执行;
关于mybatis插件涉及到的设计模式和软件思想如下:
- 设计模式:代理模式、责任链模式;
- 软件思想:AOP编程思想,降低模块间的耦合度,使业务模块更加独立;
一些注意事项:
- 不要定义过多的插件,代理嵌套过多,执行方法的时候,比较耗性能;
- 拦截器实现类的intercept方法里最后不要忘了执行invocation.proceed()方法,否则多个拦截器情况下,执行链条会断掉;
参考: