Mybatis插件
一、自定义插件
1.1 示例
- 如下是Mybatis分页插件里面实现的一个插件,我们从这里开始看实现自定义插件的方法
/**
* QueryInterceptor 规范
* 详细说明见文档:https://github.com/pagehelper/Mybatis-PageHelper/blob/master/wikis/zh/Interceptor.md
* @author liuzh/abel533/isea533
* @version 1.0.0
*/
@Intercepts(
{
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}),
}
)
public class QueryInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
Object[] args = invocation.getArgs();
MappedStatement ms = (MappedStatement) args[0];
Object parameter = args[1];
RowBounds rowBounds = (RowBounds) args[2];
ResultHandler resultHandler = (ResultHandler) args[3];
Executor executor = (Executor) invocation.getTarget();
CacheKey cacheKey;
BoundSql boundSql;
//由于逻辑关系,只会进入一次
if(args.length == 4){
//4 个参数时
boundSql = ms.getBoundSql(parameter);
cacheKey = executor.createCacheKey(ms, parameter, rowBounds, boundSql);
} else {
//6 个参数时
cacheKey = (CacheKey) args[4];
boundSql = (BoundSql) args[5];
}
//TODO 自己要进行的各种处理
//注:下面的方法可以根据自己的逻辑调用多次,在分页插件中,count 和 page 各调用了一次
return executor.query(ms, parameter, rowBounds, resultHandler, cacheKey, boundSql);
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
}
}
1.2 要点
- 要点1:通过注解给出拦截器需要拦截类型和需要拦截的方法,默认四种接口类型包括Executor、StatementHandler、ParameterHandler和ResultSetHandler自定义拦截器必须使用Mybatis提供的注解来指明我们要拦截的是四类中的哪一个类接口。
Signature指明拦截器需要拦截哪一个接口的哪一个方法,内部type对应四类接口中的某一个(比如是 Executor),
method对应接口中的方法 , args对应方法参数(重载)
- 要点2:实现Interceptor接口中的方法。
Interceptor接口的三个方法中, intercept实现插件的逻辑, plugin返回一个插件代理对象(插件的实现是基于动态代理的),setProperties是属性赋值方法。
对于plugin方法因为实现起来比较复杂,实际上Mybatis已经为我们提供了一种实现,只需要简单调用Plugin.wrap(target, this)方法即可,如此就实现了一个插件。
- 要点3:拦截器顺序。
不同类型拦截器顺序:Executor -> ParameterHandler -> StatementHandler -> ResultSetHandler
同一个类型拦截器顺序: 根据Mybatis核心配置文件配置的位置,从上往下
二、插件的配置和解析
2.1 配置
- 插件的使用如下,只需要在核心配置文件中配置插件即可
<!--配置分页插件-->
<plugins>
<plugin interceptor="com.github.pagehelper.PageInterceptor">
<!-- config params as the following -->
<!--<property name="param1" value="value1"/>-->
</plugin>
</plugins>
2.2 解析
2.2.1 XMLConfigBuilder#parseConfiguration
- Mybatis的插件在核心配置文件中配置,而核心配置文件的解析是主流程的第一阶段,可以参考16-Mybatis 核心流程01-初始化阶段。我们直接从主配置文件的解析方法:XMLConfigBuilder#parseConfiguration开始看,下面省去了其他的代码,从中可以看到解析插件的入口方法是XMLConfigBuilder#pluginElement
/**
* 解析核心配置文件的关键方法,
* 读取节点的信息,并通过对应的方法去解析配置,解析到的配置全部会放在configuration里面
* */
private void parseConfiguration(XNode root) {
try {
Properties settings = settingsAsPropertiess(root.evalNode("settings"));
//省略....
//解析<plugins>节点
pluginElement(root.evalNode("plugins"));
//省略....
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}
2.2.2 XMLConfigBuilder#pluginElement
- XMLConfigBuilder#pluginElement中完成插件的对象的实例化,赋值,并保存到Configuration对象里面的插件链中
/**
* 解析插件
* */
private void pluginElement(XNode parent) throws Exception {
if (parent != null) {
//1.遍历处理每个节点
for (XNode child : parent.getChildren()) {
//2.获取interceptor插件名称,实际上是全限定类名
String interceptor = child.getStringAttribute("interceptor");
//3.获取配置的属性
Properties properties = child.getChildrenAsProperties();
//4.反射获取到这个类的实例
Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).newInstance();
//5.给实例赋配置的属性值
interceptorInstance.setProperties(properties);
//6.保存到Configuration中的插件链对象
//protected final InterceptorChain interceptorChain = new InterceptorChain();
configuration.addInterceptor(interceptorInstance);
}
}
}
//下面是Configuration中的属性,保存全部插件
protected final InterceptorChain interceptorChain = new InterceptorChain();
- 由2.2这里面的2段代码我们看到,在配置解析阶段就会把插件都放到Configuration对象的interceptorChain属性中去,由此解析完成,后面就是代理阶段来生成相关的类实现功能增强。
三、代理
3.1 代理增强
- 前面提到过,插件是基于动态代理来实现的,假设我有一个插件需要对StatementHandler进行一定的处理,那么就会生成一个代理对象,这个代理对象在原有StatementHandler的功能基础上,添加了我这个插件想要做的工作。这个生成代理的过程实际上是在四种类型的对象初始化的时候做的,我们看下面源码(Configuration.java)
3.1.1 Configuration
- Configuration在配置解析时获取到了全部插件对象实例,在创建四大对象的时候,这些插件实例会排上用场。
/**
* 在创建ParameterHandler对象的时候,会调用interceptorChain.pluginAll来获取一个动态代理后的对象,实际返回的
* 是一个增强后的动态代理对象,不是一个最原始的ParameterHandler对象
* 这个代理对象描述如下:
* 1.代理对象是一个Plugin类的对象。从interceptorChain.pluginAll->interceptor.plugin->Plugin.wrap调用链跟踪下去可以看到
* 最后返回的是Proxy.newProxyInstance(type.getClassLoader(),interfaces, new Plugin(target, interceptor, signatureMap));
* 由此可以看出。
* 2.如果包含多个插件,这个代理对象是经过多层代理增强的。在pluginAll方法里面会依次遍历所有的插件,针对每个插件会生成一个增强的代理对象,
* 然后把这个代理对象交给下一个插件做增强,也就是有多层包装(有点像装饰器那种多层装饰的感觉)
* 3.假如没有插件,那么方法返回的就是原始对象,假如多个插件只有一部分对ParameterHandler需要增强,那么这个逻辑也会在plugin中进行处理,
* 并不是说几个插件就代理几次,会按照需要,这些信息在插件的Intercepts注解上会定义好
* 4.后面的其余三个对象原理也是一样。(ResultSetHandler,StatementHandler和Executor)
*
* */
public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {
ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement, parameterObject, boundSql);
parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler);
return parameterHandler;
}
public ResultSetHandler newResultSetHandler(Executor executor, MappedStatement mappedStatement, RowBounds rowBounds, ParameterHandler parameterHandler,
ResultHandler resultHandler, BoundSql boundSql) {
ResultSetHandler resultSetHandler = new DefaultResultSetHandler(executor, mappedStatement, parameterHandler, resultHandler, boundSql, rowBounds);
resultSetHandler = (ResultSetHandler) interceptorChain.pluginAll(resultSetHandler);
return resultSetHandler;
}
public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql);
statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
return statementHandler;
}
public Executor newExecutor(Transaction transaction) {
return newExecutor(transaction, defaultExecutorType);
}
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
executorType = executorType == null ? defaultExecutorType : executorType;
executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
Executor executor;
if (ExecutorType.BATCH == executorType) {
executor = new BatchExecutor(this, transaction);
} else if (ExecutorType.REUSE == executorType) {
executor = new ReuseExecutor(this, transaction);
} else {
executor = new SimpleExecutor(this, transaction);
}
if (cacheEnabled) {
executor = new CachingExecutor(executor);
}
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
3.1.2 InterceptorChain
- 在前面的3.1.1中获取代理对象调用的是InterceptorChain#pluginAll,我们来看看这个里面的细节。
/**
* 保存所有的Mybatis插件,Configuration对象中持有InterceptorChain实例,保存全部的插件。
* 该类主要提供2个作用:
* 1.提供添加插件的功能。添加功能在解析配置的时候会用到,将解析后的插件保存到一个集合里面
* 2.提供获取增强后的对象的功能。这个功能在实例化四大对象的时候会调用。pluginAll方法会对传入的对象进行代理增强,
* 每一次plugin都是一个代理的过程,前一个插件获取的代理后的结果是后一个插件进行增强的入参,最终返回的对象包含全
* 部插件的增强。
* @author Clinton Begin
*/
public class InterceptorChain {
//1.内部用于保存插件的list
private final List<Interceptor> interceptors = new ArrayList<Interceptor>();
//2.依次调用每个插件,让每个插件对target对象进行包装代理
//比如传进来的是一个parameterHandler,现在有三个插件interceptor1,interceptors2,interceptors3
//那么这里第一步会先调用interceptor1.plugin(parameterHandler),这个过程就会把parameterHandler进行一次代理,然后将代理对象返回,
//(如果不需要代理就会返回原来的对象,这个过程在plugin方法中会进行处理)
//然后在将第一步得到的对象应用于interceptors2,interceptors3...依次处理
//处理完毕之后获得的就是一个原始target对象的代理对象,Mybatis提供了一个简单的获取代理对象的方法Plugin.wrap(target, this);
//因此很多插件的plugin方法都是调用的这个方法
public Object pluginAll(Object target) {
for (Interceptor interceptor : interceptors) {
target = interceptor.plugin(target);
}
return target;
}
//添加插件的方法
public void addInterceptor(Interceptor interceptor) {
interceptors.add(interceptor);
}
//省略interceptors的get方法
}
- 由此我们看到,在初始化的时候会调用interceptorChain.pluginAll(object) 来获取增强后的object(Object属于四类中的某一类)。
3.2 Plugin类
3.2.1 细节
- 上面我们看到了初始化过程的代理增强对象的获取,不过我们没有看增强对象的生成细节,怎么增强的?我们知道插件的逻辑是在intercept中实现的,在1.2的要点2我们提到,plugin方法是用于返回增强的代理对象,那么我们先看看已有的插件这里是如何实现的。在1.1的分页插件中plugin方法是调用Plugin.wrap(target, this),在Mybatis源码工程中的插件测试代码ExamplePlugin中也是 return Plugin.wrap(target, this);如下所示:
@Intercepts({})
public class ExamplePlugin implements Interceptor {
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
//省略其他...
}
- 那么我们就想知道这个方法里面做了什么,下面分析。
3.2.2 Plugin类
- Plugin类使用了动态代理模式,它实现了InvocationHandler接口,内部持有Interceptor插件对象和目标对象,实际程序运行时期的对象就是一个Plugin实例,它会判断什么时候该执行插件的intercept方法,带注释源码如下:
/**
* 插件的代理类,主要负责:
* 1.生成插件拦截器对象Interceptor的代理对象(wrap方法)。简化插件开发者自行实现插件代理对象的逻辑,(自行实现的话,需要根
* 据插件的Intercepts注解里面的信息,根据需要代理的类,方法来插件一个代理对象,这个过程比较繁琐,因此Mybatis简化了这个逻辑,
* 在wrap方法中帮助我们实现了)
* 2.插件的调用。插件的调用就是调用interceptor的intercept方法,在invoke方法中实现。invoke调用方法时会判断,方法被拦截的情
* 况则会调用插件的interceptor方法,方法不需要被拦截那就直接调用目标对象的方法。在Plugin实例创建的时候,就已经把需要拦截的全
* 部方法保存到signatureMap里面了,因此很容易判断出来。
* @author Clinton Begin
*/
public class Plugin implements InvocationHandler {
private Object target;
private Interceptor interceptor;
private Map<Class<?>, Set<Method>> signatureMap;
private Plugin(Object target, Interceptor interceptor, Map<Class<?>, Set<Method>> signatureMap) {
this.target = target;
this.interceptor = interceptor;
this.signatureMap = signatureMap;
}
/**
* 返回一个代理增强后的对象,这个对象有以下特点:
* 1.这个对象内部持有的InvocationHandler是一个Plugin,表面来看是对这个Plugin进行的代理增强,但是实际上Plugin只是
* 一个壳子,Plugin内部封装了3个信息,一个target代表真正被代理的目标对象,interceptor代表了插件对象,signatureMap
* 代表了插件要拦截的方法集合(key是四大对象的类对象,value是需要拦截的方法集合)。
* 2.返回的代理对象真正代理的是target对象,从classLoad和interfaces参数可以知道。
* 3.这个代理对象内部即持有目标对象target,又持有插件对象interceptor(因为Plugin其实就是对这二者的封装)
* */
public static Object wrap(Object target, Interceptor interceptor) {
//1.signatureMap包含插件需要拦截的全部方法,Class对象为key,方法集合为value
Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
Class<?> type = target.getClass();
//2.获取目标类的需要被代理的接口。比如type是StatementHandler实现类,它一共实现了100个接口,但是我需要拦截的只有1个接口,
// 那么只返回1个。为什么呢?因为动态代理生成的代理对象是需要实现这些接口的(目标类实现了什么接口,代理类也要实现对应的接口,
// 才能实现动态代理),既然只需要增强1个接口的方法那就只实现这1个接口就好了,没有必要全部实现
Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
if (interfaces.length > 0) {
return Proxy.newProxyInstance(
type.getClassLoader(),
interfaces,
new Plugin(target, interceptor, signatureMap));
}
return target;
}
/**
* 代理对象被调用时走的方法。
* 因为代理对象即持有目标对象(比如StatementHandler或者Executor),又持有插件和插件要拦截的方法列表,因此
* 调用方法的时候要判断是不是要拦截这个方法,如果在拦截列表就要。
* */
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
//1.signatureMap是插件的方法集合,先判断需要调用的方法是不是插件的方法。比如method是StatementHandler的一个方法A,那么
//这里就会是get(StatementHandler.class),如果拦截器声明了拦截了StatementHandler的B,C方法,那么就会获取到这个包含B,C
//方法的集合,再来判断,发现不包含A,那么就执行最后面的逻辑直接目标对象调用,如果method是B方法,那么就会走if的逻辑,在
//intercept方法里面调用我们拦截器的逻辑,至于拦截器里面还要不要调用目标方法,这个是在拦截器里面控制的,因此2和3的逻辑是
//互斥的,并不需要调用2之后还调用3
Set<Method> methods = signatureMap.get(method.getDeclaringClass());
//2.这说明是插件的方法,那么就调用插件的
if (methods != null && methods.contains(method)) {
return interceptor.intercept(new Invocation(target, method, args));
}
//3.如果不是插件的方法,说明是目标对象自己的方法,比如是StatementHandler对象自己的一个方法,直接调用即可
return method.invoke(target, args);
} catch (Exception e) {
throw ExceptionUtil.unwrapThrowable(e);
}
}
/**
* 获取Interceptor插件对象的方法列表。以Class对象为key,方法集合的set为value保存到Map中。
* 这个set里面保存的方法是需要插件拦截的方法
* */
private static Map<Class<?>, Set<Method>> getSignatureMap(Interceptor interceptor) {
Intercepts interceptsAnnotation = interceptor.getClass().getAnnotation(Intercepts.class);
// issue #251
//1.插件需要注解,没有注解抛异常
if (interceptsAnnotation == null) {
throw new PluginException("No @Intercepts annotation was found in interceptor " + interceptor.getClass().getName());
}
//2.获取Signature数组
Signature[] sigs = interceptsAnnotation.value();
Map<Class<?>, Set<Method>> signatureMap = new HashMap<Class<?>, Set<Method>>();
//3.遍历处理每一个Signature
for (Signature sig : sigs) {
//4.第一次get肯定没有,就初始化里面的方法集合
Set<Method> methods = signatureMap.get(sig.type());
if (methods == null) {
methods = new HashSet<Method>();
signatureMap.put(sig.type(), methods);
}
//5.初始化完了到这一步,会获取对应的类型的全部方法和方法参数,封装成一个Method对象保存到method集合
try {
Method method = sig.type().getMethod(sig.method(), sig.args());
methods.add(method);
} catch (NoSuchMethodException e) {
throw new PluginException("Could not find method on " + sig.type() + " named " + sig.method() + ". Cause: " + e, e);
}
}
//6.最后返回的signatureMap就是以Class对象为key,方法列表为value的一个Map,这里的方法都是插件上的注解声明需要拦截的方法
return signatureMap;
}
/**
*返回type类型的所有方法中,那些需要被拦截的方法
*/
private static Class<?>[] getAllInterfaces(Class<?> type, Map<Class<?>, Set<Method>> signatureMap) {
Set<Class<?>> interfaces = new HashSet<Class<?>>();
while (type != null) {
//1.遍历type类型的全部方法,包括父类的方法
for (Class<?> c : type.getInterfaces()) {
//2.只有需要拦截的方法,才返回
if (signatureMap.containsKey(c)) {
interfaces.add(c);
}
}
//3.处理完子类,递归处理父类
type = type.getSuperclass();
}
//4.返回
return interfaces.toArray(new Class<?>[interfaces.size()]);
}
}
3.3 小结
- 我们梳理一下整个插件工作的机制
A.首先是配置解析。解析完毕之后所有的插件都会被实例化,保存到Configuration对象的interceptorChain属性里面(XMLConfigBuilder#pluginElement)
B.第二是初始化。四大对象的初始化都在Configuration中,初始化的时候会通过interceptorChain将四大对象进行增强(Configuration四大对象构造方法)
B1 这个增强获得的代理对象是Plugin类的实例,该实例内部持有四大对象和插件对象实例。(Plugin#wrap)
B2 增强对象初始化的时候就会把需要拦截的方法保存好,便于后面真正调用方法的时候判断(Plugin#wrap第一行代码)
B3 增强对象调用方法的时候会判断这个方法是否需要拦截,是就走插件对象的intercept方法,不需要就直接调用四大对象自己的方法(Plugin#invoke)
B4 增强对象很可能是一个多层的代理包装,每一个插件会代理一次(InterceptorChain#pluginAll)
B5 对B1的过程,返回代理对象是在Interceptor#plugin中需要用户自行完成的事情,需要根据插件注解等信息实现返回代理对象的逻辑,贴心的Mybatis已经在(Plugin#wrap )中实现好了,直接使用即可。(从入参Plugin.wrap(target, this)也可以看出,代理对象是持有目标对象和插件对象实例的,印证了B1的说法)
C.完成了B的初始化之后,程序运行阶段,四大对象就不再是原始的四大对象,而是经过了增强实现了对应插件功能的代理对象了。
四、设计模式
- 文章标题写了代理模式和责任链模式。代理模式其实在框架中几乎是无处不在,Mybatis中也用的非常多,责任链模式体现在InterceptorChain#pluginAll方法中,对四大对象目标类让插件进行逐个增强,不过感觉不是很明显,具体关于责任链模式可以参考: 03-行为型模式(上)
五、参考
Java 面试宝典是大明哥全力打造的 Java 精品面试题,它是一份靠谱、强大、详细、经典的 Java 后端面试宝典。它不仅仅只是一道道面试题,而是一套完整的 Java 知识体系,一套你 Java 知识点的扫盲贴。
它的内容包括:
- 大厂真题:Java 面试宝典里面的题目都是最近几年的高频的大厂面试真题。
- 原创内容:Java 面试宝典内容全部都是大明哥原创,内容全面且通俗易懂,回答部分可以直接作为面试回答内容。
- 持续更新:一次购买,永久有效。大明哥会持续更新 3+ 年,累计更新 1000+,宝典会不断迭代更新,保证最新、最全面。
- 覆盖全面:本宝典累计更新 1000+,从 Java 入门到 Java 架构的高频面试题,实现 360° 全覆盖。
- 不止面试:内容包含面试题解析、内容详解、知识扩展,它不仅仅只是一份面试题,更是一套完整的 Java 知识体系。
- 宝典详情:https://www.yuque.com/chenssy/sike-java/xvlo920axlp7sf4k
- 宝典总览:https://www.yuque.com/chenssy/sike-java/yogsehzntzgp4ly1
- 宝典进展:https://www.yuque.com/chenssy/sike-java/en9ned7loo47z5aw
目前 Java 面试宝典累计更新 400+ 道,总字数 42w+。大明哥还在持续更新中,下图是大明哥在 2024-12 月份的更新情况:
想了解详情的小伙伴,扫描下面二维码加大明哥微信【daming091】咨询
同时,大明哥也整理一套目前市面最常见的热点面试题。微信搜[大明哥聊 Java]或扫描下方二维码关注大明哥的原创公众号[大明哥聊 Java] ,回复【面试题】 即可免费领取。