MyBatis - 插件机制(十六)
该系列文档是本人在学习 Mybatis 的源码过程中总结下来的,可能对各位读者会不太友好,阅读前需要对 MyBatis 和 Spring 有一定的了解。比较适合刚接触,会使用但是一直没去探究底层的同学。
MyBatis 版本:3.5.6
MyBatis-Spring 版本:2.0.3
MyBatis-Spring-Boot-Starter 版本:2.1.4
该系列其他文档请查看:《 MyBatis 系列 - 导读》
插件机制
开源框架一般都会提供插件或其他形式的扩展点,供开发者自行扩展,增加框架的灵活性
当然,MyBatis 也提供了插件机制,基于它开发者可以进行扩展,对 MyBatis 的功能进行增强,例如实现分页、SQL分析、监控等功能,本文会对 MyBatis 插件机制的原理以及如何实现一个自定义的插件来进行讲述
我们在编写插件时,除了需要让插件类实现 org.apache.ibatis.plugin.Interceptor
接口,还需要通过注解标注该插件的拦截点,也就是插件需要增强的方法,MyBatis 只提供下面这些类中定义的方法能够被增强:
- Executor:执行器
- ParameterHandler:参数处理器
- ResultSetHandler:结果集处理器
- StatementHandler:Statement 处理器
植入插件逻辑
在《MyBatis的SQL执行》一系列文档中,有讲到在创建Executor、ParameterHandler、ResultSetHandler和StatementHandler对象时,会调用InterceptorChain
的pluginAll
方法,遍历所有的插件,调用Interceptor
插件的plugin
方法植入相应的插件逻辑,所以在 MyBatis 中只有上面的四个对象中的方法可以被增强
代码如下:
// Configuration.java
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
// <1> 获得执行器类型
executorType = executorType == null ? defaultExecutorType : executorType;
executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
// <2> 创建对应实现的 Executor 对象
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);
}
// <3> 如果开启缓存,创建 CachingExecutor 对象,进行包装
if (cacheEnabled) {
executor = new CachingExecutor(executor);
}
// <4> 应用插件
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject,
BoundSql boundSql) {
// 创建 ParameterHandler 对象
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) {
// 创建 DefaultResultSetHandler 对象
ResultSetHandler resultSetHandler = new DefaultResultSetHandler(executor, mappedStatement,
parameterHandler, resultHandler, boundSql, rowBounds);
// 应用插件
resultSetHandler = (ResultSetHandler) interceptorChain.pluginAll(resultSetHandler);
return resultSetHandler;
}
public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement,
StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement,
parameterObject, rowBounds, resultHandler, boundSql);
// 将 Configuration 全局配置中的所有插件应用在 StatementHandler 上面
statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
return statementHandler;
}
分页插件示例
我们先来看一个简单的插件示例,代码如下:
@Intercepts({
@Signature(
type = Executor.class,
method = "query",
args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}
)
})
public class ExamplePlugin implements Interceptor {
// Executor的查询方法:
// public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler)
@Override
public Object intercept(Invocation invocation) throws Throwable {
Object[] args = invocation.getArgs();
RowBounds rowBounds = (RowBounds) args[2];
if (rowBounds == RowBounds.DEFAULT) { // 无需分页
return invocation.proceed();
}
/*
* 将query方法的 RowBounds 入参设置为空对象
* 也就是关闭 MyBatis 内部实现的分页(逻辑分页,在拿到查询结果后再进行分页的,而不是物理分页)
*/
args[2] = RowBounds.DEFAULT;
MappedStatement mappedStatement = (MappedStatement) args[0];
BoundSql boundSql = mappedStatement.getBoundSql(args[1]);
// 获取 SQL 语句,拼接 limit 语句
String sql = boundSql.getSql();
String limit = String.format("LIMIT %d,%d", rowBounds.getOffset(), rowBounds.getLimit());
sql = sql + " " + limit;
// 创建一个 StaticSqlSource 对象
SqlSource sqlSource = new StaticSqlSource(mappedStatement.getConfiguration(), sql, boundSql.getParameterMappings());
// 通过反射获取并设置 MappedStatement 的 sqlSource 字段
Field field = MappedStatement.class.getDeclaredField("sqlSource");
field.setAccessible(true);
field.set(mappedStatement, sqlSource);
// 执行被拦截方法
return invocation.proceed();
}
@Override
public Object plugin(Object target) {
// default impl
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
// default nop
}
}
在上面的分页插件中,@Intercepts
和@Signature
两个注解指定了增强的方法是Executor.query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler)
,也就是我们使用到的 Executor 执行数据库查询操作的方法
在实现的 intercept
方法中,通过 RowBounds
参数获取分页信息,并生成相应的 SQL(拼接了 limit) ,并使用该 SQL 作为参数重新创建一个 StaticSqlSource
对象,最后通过反射替换 MappedStatement
对象中的 sqlSource
字段,这样就实现了一个简单的分页插件
上面只是一个简单的示例,实际场景中慎用
Interceptor
org.apache.ibatis.plugin.Interceptor
:拦截器接口,代码如下:
public interface Interceptor {
/**
* 拦截方法
*
* @param invocation 调用信息
* @return 调用结果
* @throws Throwable 若发生异常
*/
Object intercept(Invocation invocation) throws Throwable;
/**
* 应用插件。如应用成功,则会创建目标对象的代理对象
*
* @param target 目标对象
* @return 应用的结果对象,可以是代理对象,也可以是 target 对象,也可以是任意对象。具体的,看代码实现
*/
default Object plugin(Object target) {
return Plugin.wrap(target, this);
}
/**
* 设置拦截器属性
*
* @param properties 属性
*/
default void setProperties(Properties properties) {
// NOP
}
}
- intercept方法:拦截方法,插件的增强逻辑
- plugin方法:应用插件,往目标对象中植入相应的插件逻辑,如果应用成功则返回一个代理对象(JDK动态代理),否则返回原始对象,默认调用
Plugin
的wrap
方法 - setProperties方法:设置拦截器属性
Invocation
org.apache.ibatis.plugin.Invocation
:被拦截的对象信息,代码如下:
public class Invocation {
/**
* 目标对象
*/
private final Object target;
/**
* 方法
*/
private final Method method;
/**
* 参数
*/
private final Object[] args;
public Invocation(Object target, Method method, Object[] args) {
this.target = target;
this.method = method;
this.args = args;
}
// 省略 getter setter 方法
}
Plugin
org.apache.ibatis.plugin.Plugin
:实现InvocationHandler接口,用于对拦截的对象进行,一方面提供创建动态代理对象的方法,另一方面实现对指定类的指定方法的拦截处理,MyBatis插件机制的核心类
构造方法
public class Plugin implements InvocationHandler {
/**
* 目标对象
*/
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;
}
}
wrap方法
wrap(Object target, Interceptor interceptor)
方法,创建目标类的代理对象,方法如下:
public static Object wrap(Object target, Interceptor interceptor) {
// <1> 获得拦截器中需要拦截的类的方法集合
Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
// <2> 获得目标对象的 Class 对象
Class<?> type = target.getClass();
// <3> 获得目标对象所有需要被拦截的 Class 对象(父类或者接口)
Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
// <4> 若存在需要被拦截的,则为目标对象的创建一个动态代理对象(JDK 动态代理),代理类为 Plugin 对象
if (interfaces.length > 0) {
// 因为 Plugin 实现了 InvocationHandler 接口,所以可以作为 JDK 动态代理的调用处理器
return Proxy.newProxyInstance(type.getClassLoader(), interfaces, new Plugin(target, interceptor, signatureMap));
}
// <5> 如果没有,则返回原始的目标对象
return target;
}
- 调用
getSignatureMap
方法,获得拦截器中需要拦截的类的方法集合,有就是通过@Intercepts
和@Signature
两个注解指定的增强的方法 - 获得目标对象的 Class 对象(父类或者接口)
- 获得目标对象所有需要被拦截的 Class 对象
- 如果需要被拦截,则为目标对象的创建一个动态代理对象(JDK 动态代理),代理类为
Plugin
对象,并返回该动态代理对象 - 否则返回原始的目标对象
getSignatureMap方法
getSignatureMap(Interceptor interceptor)
方法,获取插件需要增强的方法,方法如下:
private static Map<Class<?>, Set<Method>> getSignatureMap(Interceptor interceptor) {
// 获取 @Intercepts 注解
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());
}
// 获取 @Intercepts 注解中的 @Signature 注解
Signature[] sigs = interceptsAnnotation.value();
Map<Class<?>, Set<Method>> signatureMap = new HashMap<>();
for (Signature sig : sigs) {
// 为 @Signature 注解中定义类名创建一个方法数组
Set<Method> methods = signatureMap.computeIfAbsent(sig.type(), k -> new HashSet<>());
try {
// 获取 @Signature 注解中定义的方法对象
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
注解,获取到所有需要被拦截的对象中的需要增强的方法
getAllInterfaces方法
getAllInterfaces(Class<?> type, Map<Class<?>, Set<Method>> signatureMap)
方法,判断目标对象是否需要被插件应用,方法如下:
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()]);
}
- 入参
signatureMap
就是getSignatureMap
方法返回的该插件需要增强的方法 - 返回存在于
signatureMap
集合中所有目标对象的父类或者接口
invoke方法
invoke(Object proxy, Method method, Object[] args)
方法,动态代理对象的拦截方法,方法如下:
@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);
}
}
- 获得目标方法所在的类需要被拦截的方法
- 如果被拦截的方法包含当前方法,则将当前方法封装成
Invocation
对象,调用Interceptor
插件的intercept
方法,执行插件逻辑 - 否则执行原有方法
这样一来,当你调用了目标对象的对应方法时,则会进入该插件的intercept
方法,执行插件逻辑,扩展功能
InterceptorChain
org.apache.ibatis.plugin.InterceptorChain
:拦截器链,用于将所有的拦截器按顺序将插件逻辑植入目标对象,代码如下:
public class InterceptorChain {
private final List<Interceptor> interceptors = new ArrayList<>();
public Object pluginAll(Object target) {
// 遍历拦截器集合
for (Interceptor interceptor : interceptors) {
// 调用拦截器的 plugin 方法植入相应的插件逻辑
target = interceptor.plugin(target);
}
return target;
}
public void addInterceptor(Interceptor interceptor) {
interceptors.add(interceptor);
}
public List<Interceptor> getInterceptors() {
return Collections.unmodifiableList(interceptors);
}
}
配置MyBatis插件都会保存在interceptors
集合中,可以回顾到**MyBatis - 初始化(八)加载 mybatis-config.xml的XMLConfigBuilder**小节的pluginElement
方法,会将解析到的依次全部添加到Configuration
的InterceptorChain
对象中,代码如下:
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).getDeclaredConstructor().newInstance();
interceptorInstance.setProperties(properties);
// <2> 添加到 configuration 中
configuration.addInterceptor(interceptorInstance);
}
}
}
总结
本文分析了 MyBatis 中插件机制,总体来说比较简单的,想要实现一个插件,需要实现 Interceptor
接口,并通过@Intercepts
和@Signature
两个注解指定该插件的拦截点(支持对Executor、ParameterHandler、ResultSetHandler 和 StatementHandler 四个对象中的方法进行增强),在实现的intercept
方法中进行逻辑处理
在 MyBatis 初始化的时候,会扫描插件,将其添加到InterceptorChain
中
然后 MyBatis 在 SQL 执行过程中,创建上面四个对象的时候,会将创建的对象交由InterceptorChain
去处理,遍历所有的插件,通过插件的plugin
方法为其创建一个动态代理对象并返回,代理类是Plugin
对象
在Plugin
对象中的invoke
方法中,将请求交由插件的intercept
方法去处理
虽然 MyBatis 的插件机制比较简单,但是想要实现一个完善且高效的插件却比较复杂,可以参考PageHelper分页插件
到这里,相信大家对 MyBatis 的插件机制有了一定的了解,感谢大家的阅读!!!
参考文章:《MyBatis 源码分析》
--------------最后感谢大家的阅读,愿大家技术越来越流弊!--------------
--------------也希望大家给我点支持,谢谢各位大佬了!!!--------------