code-learning/mybatis/27-mybatis-插件体系(一)之原理.md

421 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 精尽 MyBatis 源码分析 —— 插件体系(一)之原理
# 1. 概述
本文,我们来分享 MyBatis 的插件模块,对应 `plugin` 包。如下图所示:[![`scripting` 包](27-mybatis-插件体系(一)之原理.assets/01.png)](http://static.iocoder.cn/images/MyBatis/2020_03_18/01.png)`scripting`
在 [《精尽 MyBatis 源码解析 —— 项目结构一览》](http://svip.iocoder.cn/MyBatis/intro) 中,简单介绍了这个模块如下:
> Mybatis 自身的功能虽然强大,但是并不能完美切合所有的应用场景,因此 MyBatis 提供了插件接口,我们可以通过添加用户自定义插件的方式对 MyBatis 进行扩展。用户自定义插件也可以改变 Mybatis 的默认行为,例如,我们可以拦截 SQL 语句并对其进行重写。
>
> 由于用户自定义插件会影响 MyBatis 的核心行为,在使用自定义插件之前,开发人员需要了解 MyBatis 内部的原理,这样才能编写出安全、高效的插件。
- 总的来说MyBatis 的插件,还是基于**动态代理**来实现。所以,😈 胖友有没发现,动态代理,无处不在。嘿嘿。
- 另外,胖友先看看 [《MyBatis 官方文档 —— 插件》](http://www.mybatis.org/mybatis-3/zh/configuration.html#plugins) 文档,对 MyBatis 插件的概念和配置,再有一个了解。
本文涉及的类如下图所示:[![类图](27-mybatis-插件体系(一)之原理.assets/02.png)](http://static.iocoder.cn/images/MyBatis/2020_03_18/02.png)类图
下面,我们逐个类来瞅瞅。
# 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 的如下四处方法中调用:[![调用处](27-mybatis-插件体系(一)之原理.assets/03.png)](http://static.iocoder.cn/images/MyBatis/2020_03_18/03.png)调用处
- 一共可以有四种目标对象类型可以被拦截1Executor2StatementHandler3ParameterHandler4ResultSetHandler 。
# 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 并未提供自带的插件实现。
参考和推荐如下文章:
- 祖大俊 [《Mybatis3.4.x技术内幕十九Mybatis之plugin插件设计原理》](https://my.oschina.net/zudajun/blog/738973)
- 田小波 [《MyBatis 源码分析 - 插件机制》](https://www.tianxiaobo.com/2018/08/26/MyBatis-源码分析-插件机制/)
- 徐郡明 [《MyBatis 技术内幕》](https://item.jd.com/12125531.html) 的 [「4.1 插件模块」](https://svip.iocoder.cn/MyBatis/plugin-1/#) 小节