13 KiB
精尽 MyBatis 源码分析 —— 插件体系(一)之原理
1. 概述
本文,我们来分享 MyBatis 的插件模块,对应 plugin
包。如下图所示:scripting
包
在 《精尽 MyBatis 源码解析 —— 项目结构一览》 中,简单介绍了这个模块如下:
Mybatis 自身的功能虽然强大,但是并不能完美切合所有的应用场景,因此 MyBatis 提供了插件接口,我们可以通过添加用户自定义插件的方式对 MyBatis 进行扩展。用户自定义插件也可以改变 Mybatis 的默认行为,例如,我们可以拦截 SQL 语句并对其进行重写。
由于用户自定义插件会影响 MyBatis 的核心行为,在使用自定义插件之前,开发人员需要了解 MyBatis 内部的原理,这样才能编写出安全、高效的插件。
- 总的来说,MyBatis 的插件,还是基于动态代理来实现。所以,😈 胖友有没发现,动态代理,无处不在。嘿嘿。
- 另外,胖友先看看 《MyBatis 官方文档 —— 插件》 文档,对 MyBatis 插件的概念和配置,再有一个了解。
下面,我们逐个类来瞅瞅。
2. Interceptor
org.apache.ibatis.plugin.Interceptor
,拦截器接口。代码如下:
// Interceptor.java
public interface Interceptor {
/**
* 拦截方法
*
* @param invocation 调用信息
* @return 调用结果
* @throws Throwable 若发生异常
*/
Object intercept(Invocation invocation) throws Throwable;
/**
* 应用插件。如应用成功,则会创建目标对象的代理对象
*
* @param target 目标对象
* @return 应用的结果对象,可以是代理对象,也可以是 target 对象,也可以是任意对象。具体的,看代码实现
*/
Object plugin(Object target);
/**
* 设置拦截器属性
*
* @param properties 属性
*/
void setProperties(Properties properties);
}
- 关于三个方法,胖友看看代码注释。
2.1 示例
我们来看看 org.apache.ibatis.plugin.PluginTest
,提供了 AlwaysMapPlugin 示例。代码如下:
@Intercepts({
@Signature(type = Map.class, method = "get", args = {Object.class})}) // <1>
public static class AlwaysMapPlugin implements Interceptor {
@Override // <4>
public Object intercept(Invocation invocation) throws Throwable {
return "Always";
}
@Override // <2>
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override // <3>
public void setProperties(Properties properties) {
}
}
<1>
处,通过@Intercepts
和@Signature
注解,定义了需要拦截的方法为 Map 类型、方法为"get"
方法,方法参数为Object.class
。详细解析,见 TODO。<2>
处,在实现方法#plugin(Object target)
方法内部,他调用Plugin#wrap(Object target, Interceptor interceptor)
方法,执行代理对象的创建。详细解析,见 TODO。<3>
处,在实现方法#setProperties(Properties properties)
方法内部,暂未做任何实现。此处可以实现,若 AlwaysMapPlugin 有属性,可以从properties
获取一些需要的属性值。<4>
处,在实现方法#intercept(Invocation invocation)
方法,直接返回"Always"
字符串。也就是说,当所有的target
类型为 Map 类型,并且调用Map#get(Object)
方法时,返回的都是"Always"
。
这个示例,胖友可以跑下 PluginTest 单元测试里的方法,可以更加好理解。
2.2 Invocation
org.apache.ibatis.plugin.Invocation
,方法调用信息,作为 Interceptor#intercept(Invocation invocation)
的方法参数。代码如下:
// Invocation.java
public interface Interceptor {
/**
* 拦截方法
*
* @param invocation 调用信息
* @return 调用结果
* @throws Throwable 若发生异常
*/
Object intercept(Invocation invocation) throws Throwable;
/**
* 应用插件。如应用成功,则会创建目标对象的代理对象
*
* @param target 目标对象
* @return 应用的结果对象,可以是代理对象,也可以是 target 对象,也可以是任意对象。具体的,看代码实现
*/
Object plugin(Object target);
/**
* 设置拦截器属性
*
* @param properties 属性
*/
void setProperties(Properties properties);
}
3. InterceptorChain
org.apache.ibatis.plugin.InterceptorChain
,拦截器 Interceptor 链。
3.1 构造方法
// InterceptorChain.java
/**
* 拦截器数组
*/
private final List<Interceptor> interceptors = new ArrayList<>();
3.2 addInterceptor
#addInterceptor(Interceptor interceptor)
方法,添加拦截器。代码如下:
// InterceptorChain.java
public void addInterceptor(Interceptor interceptor) {
interceptors.add(interceptor);
}
该方法在 Configuration 的 #pluginElement(XNode parent)
方法中被调用,代码如下:
// XMLConfigBuilder.java
/**
* 解析 <plugins /> 标签,添加到 {@link Configuration#interceptorChain} 中
*
* @param parent 节点
* @throws Exception 发生异常时
*/
private void pluginElement(XNode parent) throws Exception {
if (parent != null) {
// 遍历 <plugins /> 标签
for (XNode child : parent.getChildren()) {
String interceptor = child.getStringAttribute("interceptor");
Properties properties = child.getChildrenAsProperties();
// <1> 创建 Interceptor 对象,并设置属性
Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).newInstance();
interceptorInstance.setProperties(properties);
// 添加到 configuration 中
configuration.addInterceptor(interceptorInstance);
}
}
}
-
<1>
处,创建 Interceptor 对象,并调用Interceptor#setProperties(properties)
方法,设置拦截器的属性。 -
<2>
处,调用Configuration#addInterceptor(interceptorInstance)
方法,添加到Configuration.interceptorChain
中。代码如下:// Configuration.java /** * 拦截器链 */ protected final InterceptorChain interceptorChain = new InterceptorChain(); public void addInterceptor(Interceptor interceptor) { interceptorChain.addInterceptor(interceptor); }
3.3 pluginAll
#pluginAll(Object target)
方法,应用所有拦截器到指定目标对象。代码如下:
// InterceptorChain.java
public Object pluginAll(Object target) {
for (Interceptor interceptor : interceptors) {
target = interceptor.plugin(target);
}
return target;
}
该方法被 Configuration 的如下四处方法中调用:调用处
- 一共可以有四种目标对象类型可以被拦截:1)Executor;2)StatementHandler;3)ParameterHandler;4)ResultSetHandler 。
4. @Intercepts
org.apache.ibatis.plugin.@Intercepts
,拦截器注解。代码如下:
// Intercepts.java
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Intercepts {
/**
* @return 拦截的方法签名的数组
*/
Signature[] value();
}
4.1 @Signature
org.apache.ibatis.plugin.@Signature
,方法签名的注解。代码如下:
// Signature.java
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({})
public @interface Signature {
/**
* @return 类
*/
Class<?> type();
/**
* @return 方法名
*/
String method();
/**
* @return 参数类型
*/
Class<?>[] args();
}
5. Plugin
org.apache.ibatis.plugin.Plugin
,实现 InvocationHandler 接口,插件类,一方面提供创建动态代理对象的方法,另一方面实现对指定类的指定方法的拦截处理。
注意,Plugin 是 MyBatis 插件体系的核心类。
5.1 构造方法
// Plugin.java
/**
* 目标对象
*/
private final Object target;
/**
* 拦截器
*/
private final Interceptor interceptor;
/**
* 拦截的方法映射
*
* KEY:类
* VALUE:方法集合
*/
private final 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;
}
5.2 wrap
#wrap(Object target, Interceptor interceptor)
静态方法,创建目标类的代理对象。代码如下:
// Plugin.java
public static Object wrap(Object target, Interceptor interceptor) {
// <1> 获得拦截的方法映射
Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
// <2> 获得目标类的类型
Class<?> type = target.getClass();
// <2> 获得目标类的接口集合
Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
// <3.1> 若有接口,则创建目标对象的 JDK Proxy 对象
if (interfaces.length > 0) {
return Proxy.newProxyInstance(
type.getClassLoader(),
interfaces,
new Plugin(target, interceptor, signatureMap)); // 因为 Plugin 实现了 InvocationHandler 接口,所以可以作为 JDK 动态代理的调用处理器
}
// <3.2> 如果没有,则返回原始的目标对象
return target;
}
-
<1>
处,调用#getSignatureMap(Interceptor interceptor)
方法,获得拦截的方法映射。代码如下:// Plugin.java private static Map<Class<?>, Set<Method>> getSignatureMap(Interceptor interceptor) { Intercepts interceptsAnnotation = interceptor.getClass().getAnnotation(Intercepts.class); // issue #251 if (interceptsAnnotation == null) { throw new PluginException("No @Intercepts annotation was found in interceptor " + interceptor.getClass().getName()); } Signature[] sigs = interceptsAnnotation.value(); Map<Class<?>, Set<Method>> signatureMap = new HashMap<>(); for (Signature sig : sigs) { Set<Method> methods = signatureMap.computeIfAbsent(sig.type(), k -> new HashSet<>()); 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); } } return signatureMap; }
- 基于
@Intercepts
和@Signature
注解。
- 基于
-
<2>
处,调用#getAllInterfaces(Class<?> type, Map<Class<?>, Set<Method>> signatureMap)
方法,获得目标类的接口集合。代码如下:// Plugin.java private static Class<?>[] getAllInterfaces(Class<?> type, Map<Class<?>, Set<Method>> signatureMap) { // 接口的集合 Set<Class<?>> interfaces = new HashSet<>(); // 循环递归 type 类,机器父类 while (type != null) { // 遍历接口集合,若在 signatureMap 中,则添加到 interfaces 中 for (Class<?> c : type.getInterfaces()) { if (signatureMap.containsKey(c)) { interfaces.add(c); } } // 获得父类 type = type.getSuperclass(); } // 创建接口的数组 return interfaces.toArray(new Class<?>[interfaces.size()]); }
- 从这里可以看出,
@Signature
注解的type
属性,必须是接口。
- 从这里可以看出,
-
<3.1>
处,情况一,若有接口,则创建目标对象的 JDK Proxy 对象。 -
<3.2>
处,情况二,若无接口,则返回原始的目标对象。
5.3 invoke
// Plugin.java
@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)) {
// 如果是,则拦截处理该方法
return interceptor.intercept(new Invocation(target, method, args));
}
// 如果不是,则调用原方法
return method.invoke(target, args);
} catch (Exception e) {
throw ExceptionUtil.unwrapThrowable(e);
}
}
- 具体,见代码注释。简单~
666. 彩蛋
在地铁上,撸完了这篇博客,小文一篇。后续,我们会找一些 MyBatis 的插件,详细解析。另外,MyBatis 并未提供自带的插件实现。
参考和推荐如下文章: