该系列文档是本人在学习 Mybatis 的源码过程中总结下来的,可能对读者不太友好,请结合我的源码注释(Mybatis源码分析 GitHub 地址Mybatis-Spring 源码分析 GitHub 地址Spring-Boot-Starter 源码分析 GitHub 地址)进行阅读

MyBatis 版本:3.5.2

MyBatis-Spring 版本:2.0.3

MyBatis-Spring-Boot-Starter 版本:2.1.4

插件机制

开源框架一般都会提供插件或其他形式的扩展点,供开发者自行扩展,增加框架的灵活性

当然,MyBatis 也提供了插件机制,基于它开发者可以进行扩展,对 MyBatis 的功能进行增强,例如实现分页、SQL分析、监控等功能,本文会对 MyBatis 插件机制的原理以及如何实现一个自定义的插件来进行讲述

我们在编写插件时,除了需要让插件类实现 org.apache.ibatis.plugin.Interceptor 接口,还需要通过注解标注该插件的拦截点,也就是插件需要增强的方法,MyBatis 只提供下面这些类中定义的方法能够被增强:

  • Executor:执行器

  • ParameterHandler:参数处理器

  • ResultSetHandler:结果集处理器

  • StatementHandler:Statement 处理器

植入插件逻辑

《MyBatis的SQL执行过程》一系列文档中,有讲到在创建Executor、ParameterHandler、ResultSetHandler和StatementHandler对象时,会调用InterceptorChainpluginAll方法,遍历所有的插件,调用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动态代理),否则返回原始对象,默认调用Pluginwrap方法
  • 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;
}
  1. 调用getSignatureMap方法,获得拦截器中需要拦截的类的方法集合,有就是通过@Intercepts@Signature两个注解指定的增强的方法
  2. 获得目标对象的 Class 对象(父类或者接口)
  3. 获得目标对象所有需要被拦截的 Class 对象
  4. 如果需要被拦截,则为目标对象的创建一个动态代理对象(JDK 动态代理),代理类为 Plugin 对象,并返回该动态代理对象
  5. 否则返回原始的目标对象

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);
}
}
  1. 获得目标方法所在的类需要被拦截的方法
  2. 如果被拦截的方法包含当前方法,则将当前方法封装成Invocation对象,调用Interceptor插件的intercept方法,执行插件逻辑
  3. 否则执行原有方法

这样一来,当你调用了目标对象的对应方法时,则会进入该插件的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-config.xml》XMLConfigBuilder小节的pluginElement方法,会将解析到的依次全部添加到ConfigurationInterceptorChain对象中,代码如下:

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 源码分析》

精尽MyBatis源码分析 - 插件机制的更多相关文章

  1. MyBatis 源码分析 - 插件机制

    1.简介 一般情况下,开源框架都会提供插件或其他形式的拓展点,供开发者自行拓展.这样的好处是显而易见的,一是增加了框架的灵活性.二是开发者可以结合实际需求,对框架进行拓展,使其能够更好的工作.以 My ...

  2. 精尽MyBatis源码分析 - 文章导读

    该系列文档是本人在学习 Mybatis 的源码过程中总结下来的,可能对读者不太友好,请结合我的源码注释(Mybatis源码分析 GitHub 地址.Mybatis-Spring 源码分析 GitHub ...

  3. 精尽MyBatis源码分析 - MyBatis-Spring 源码分析

    该系列文档是本人在学习 Mybatis 的源码过程中总结下来的,可能对读者不太友好,请结合我的源码注释(Mybatis源码分析 GitHub 地址.Mybatis-Spring 源码分析 GitHub ...

  4. 精尽 MyBatis 源码分析 - 整体架构

    该系列文档是本人在学习 Mybatis 的源码过程中总结下来的,可能对读者不太友好,请结合我的源码注释(Mybatis源码分析 GitHub 地址.Mybatis-Spring 源码分析 GitHub ...

  5. 精尽 MyBatis 源码分析 - 基础支持层

    该系列文档是本人在学习 Mybatis 的源码过程中总结下来的,可能对读者不太友好,请结合我的源码注释(Mybatis源码分析 GitHub 地址.Mybatis-Spring 源码分析 GitHub ...

  6. 精尽MyBatis源码分析 - MyBatis初始化(四)之 SQL 初始化(下)

    该系列文档是本人在学习 Mybatis 的源码过程中总结下来的,可能对读者不太友好,请结合我的源码注释(Mybatis源码分析 GitHub 地址.Mybatis-Spring 源码分析 GitHub ...

  7. 精尽MyBatis源码分析 - SQL执行过程(四)之延迟加载

    该系列文档是本人在学习 Mybatis 的源码过程中总结下来的,可能对读者不太友好,请结合我的源码注释(Mybatis源码分析 GitHub 地址.Mybatis-Spring 源码分析 GitHub ...

  8. 精尽 MyBatis 源码分析 - SqlSession 会话与 SQL 执行入口

    该系列文档是本人在学习 Mybatis 的源码过程中总结下来的,可能对读者不太友好,请结合我的源码注释(Mybatis源码分析 GitHub 地址.Mybatis-Spring 源码分析 GitHub ...

  9. 精尽 MyBatis 源码分析 - MyBatis 初始化(一)之加载 mybatis-config.xml

    该系列文档是本人在学习 Mybatis 的源码过程中总结下来的,可能对读者不太友好,请结合我的源码注释(Mybatis源码分析 GitHub 地址.Mybatis-Spring 源码分析 GitHub ...

随机推荐

  1. 一些免费API接口

    转载自:https://www.cnblogs.com/haimishasha/p/6351403.html 天气接口 聚合数据: http://op.juhe.cn/onebox/weather/q ...

  2. 一篇理解什么是CanSet, CanAddr?

    什么是可设置( CanSet ) 首先需要先明确下,可设置是针对 reflect.Value 的.普通的变量要转变成为 reflect.Value 需要先使用 reflect.ValueOf() 来进 ...

  3. Spring Boot学习笔记(二)——HelloWorld实现

    提示:要在Eclipse里使用Spring Boot,首先要安装STS插件,前面我们已经安装了STS插件了,可以创建Spring Boot项目了. 1.创建项目: 新建项目,选择Spring Boot ...

  4. liunx命令的运用

    工作中用到了一些命令,记忆才深刻 1.查看服务器内存:free -h 2.查看服务器磁盘空间:df -h 3.切root用户:sudo su root 输入密码 4.查看liunx服务器下的所有用户: ...

  5. 【总结】HTTP

    一.HTTP 1.http HTTP 是一种 超文本传输协议(Hypertext Transfer Protocol),HTTP 是一个在计算机世界里专门在两点之间传输文字.图片.音频.视频等超文本数 ...

  6. ThreeJS系列2_effect插件集简介( 3d, vr等 )

    ThreeJS系列2_effect插件集简介( 3d, vr等 ) ThreeJS 官方案例中有一些 js库 可以替代 render 将场景中的物质变换为其他效果的物质 目录 ThreeJS系列2_e ...

  7. CF618F Double Knapsack

    题意简化 给定两个大小为 n 的集合A,B,要求在每个集合中选出一个子集,使得两个选出来的子集元素和相等 元素范围在 1~n ,n<=1e5 题目连接 题解 考虑前缀和 令A集合的前缀和为SA, ...

  8. 机器学习 第5篇:knn回归

    基于最邻近算法的分类,本质上是对离散的数据标签进行预测,实际上,最邻近算法也可以用于对连续的数据标签进行预测,这种方法叫做基于最邻近数据的回归,预测的值(即数据的标签)是连续值,通过计算数据点最临近数 ...

  9. Linux下开发环境的搭建(For C++ OIer)

    说句实话,对于OIer来说,Linux真的是个很好的开发平台. 这里既没有游戏的喧嚣,也没有广告的打扰,gcc/g++早已预装,一切已为你准备好......(???)即使对于日常使用,也绰绰有余. 如 ...

  10. linux的mysql修改默认端口3306

    linux 修改mysql默认端口3306 cd /etc/mysql/my.cnf 修改两处 客户端的port=3306 和mysqld的服务器端口port=3306 [client] port=3 ...