MyBatis 源码分析 - 插件机制
1.简介
一般情况下,开源框架都会提供插件或其他形式的拓展点,供开发者自行拓展。这样的好处是显而易见的,一是增加了框架的灵活性。二是开发者可以结合实际需求,对框架进行拓展,使其能够更好的工作。以 MyBatis 为例,我们可基于 MyBatis 插件机制实现分页、分表,监控等功能。由于插件和业务无关,业务也无法感知插件的存在。因此可以无感植入插件,在无形中增强功能。
开发 MyBatis 插件需要对 MyBatis 比较深了解才行,一般来说最好能够掌握 MyBatis 的源码,门槛相对较高。本篇文章在分析完 MyBatis 插件机制后,会手写一个简单的分页插件,以帮助大家更好的掌握 MyBatis 插件的编写。
2. 插件机制原理
我们在编写插件时,除了需要让插件类实现 Interceptor 接口,还需要通过注解标注该插件的拦截点。所谓拦截点指的是插件所能拦截的方法,MyBatis 所允许拦截的方法如下:
- Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
- ParameterHandler (getParameterObject, setParameters)
- ResultSetHandler (handleResultSets, handleOutputParameters)
- StatementHandler (prepare, parameterize, batch, update, query)
如果我们想要拦截 Executor 的 query 方法,那么可以这样定义插件。
@Intercepts({
@Signature(
type = Executor.class,
method = "query",
args ={MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}
)
})
public class ExamplePlugin implements Interceptor {
// 省略逻辑
}
除此之外,我们还需将插件配置到相关文件中。这样 MyBatis 在启动时可以加载插件,并保存插件实例到相关对象(InterceptorChain,拦截器链)中。待准备工作做完后,MyBatis 处于就绪状态。我们在执行 SQL 时,需要先通过 DefaultSqlSessionFactory 创建 SqlSession 。Executor 实例会在创建 SqlSession 的过程中被创建,Executor 实例创建完毕后,MyBatis 会通过 JDK 动态代理为实例生成代理类。这样,插件逻辑即可在 Executor 相关方法被调用前执行。
以上就是 MyBatis 插件机制的基本原理。接下来,我们来看一下原理背后对应的源码是怎样的。
3. 源码分析
3.1 植入插件逻辑
本节,我将以 Executor 为例,分析 MyBatis 是如何为 Executor 实例植入插件逻辑的。Executor 实例是在开启 SqlSession 时被创建的,因此,下面我们从源头进行分析。先来看一下 SqlSession 开启的过程。
// -☆- DefaultSqlSessionFactory
public SqlSession openSession() {
return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);
}
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
Transaction tx = null;
try {
// 省略部分逻辑
// 创建 Executor
final Executor executor = configuration.newExecutor(tx, execType);
return new DefaultSqlSession(configuration, executor, autoCommit);
}
catch (Exception e) {...}
finally {...}
}
Executor 的创建过程封装在 Configuration 中,我们跟进去看看看。
// -☆- Configuration
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
executorType = executorType == null ? defaultExecutorType : executorType;
executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
Executor executor;
// 根据 executorType 创建相应的 Executor 实例
if (ExecutorType.BATCH == executorType) {...}
else if (ExecutorType.REUSE == executorType) {...}
else {
executor = new SimpleExecutor(this, transaction);
}
if (cacheEnabled) {
executor = new CachingExecutor(executor);
}
// 植入插件
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
如上,newExecutor 方法在创建好 Executor 实例后,紧接着通过拦截器链 interceptorChain 为 Executor 实例植入代理逻辑。那下面我们看一下 InterceptorChain 的代码是怎样的。
public class InterceptorChain {
private final List<Interceptor> interceptors = new ArrayList<Interceptor>();
public Object pluginAll(Object target) {
// 遍历拦截器集合
for (Interceptor interceptor : interceptors) {
// 调用拦截器的 plugin 方法植入相应的插件逻辑
target = interceptor.plugin(target);
}
return target;
}
/** 添加插件实例到 interceptors 集合中 */
public void addInterceptor(Interceptor interceptor) {
interceptors.add(interceptor);
}
/** 获取插件列表 */
public List<Interceptor> getInterceptors() {
return Collections.unmodifiableList(interceptors);
}
}
以上是 InterceptorChain 的全部代码,比较简单。它的 pluginAll 方法会调用具体插件的 plugin 方法植入相应的插件逻辑。如果有多个插件,则会多次调用 plugin 方法,最终生成一个层层嵌套的代理类。形如下面:

当 Executor 的某个方法被调用的时候,插件逻辑会先行执行。执行顺序由外而内,比如上图的执行顺序为 plugin3 → plugin2 → Plugin1 → Executor。
plugin 方法是由具体的插件类实现,不过该方法代码一般比较固定,所以下面找个示例分析一下。
// -☆- ExamplePlugin
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
// -☆- Plugin
public static Object wrap(Object target, Interceptor interceptor) {
/*
* 获取插件类 @Signature 注解内容,并生成相应的映射结构。形如下面:
* {
* Executor.class : [query, update, commit],
* ParameterHandler.class : [getParameterObject, setParameters]
* }
*/
Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
Class<?> type = target.getClass();
// 获取目标类实现的接口
Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
if (interfaces.length > 0) {
// 通过 JDK 动态代理为目标类生成代理类
return Proxy.newProxyInstance(
type.getClassLoader(),
interfaces,
new Plugin(target, interceptor, signatureMap));
}
return target;
}
如上,plugin 方法在内部调用了 Plugin 类的 wrap 方法,用于为目标对象生成代理。Plugin 类实现了 InvocationHandler 接口,因此它可以作为参数传给 Proxy 的 newProxyInstance 方法。
到这里,关于插件植入的逻辑就分析完了。接下来,我们来看看插件逻辑是怎样执行的。
3.2 执行插件逻辑
Plugin 实现了 InvocationHandler 接口,因此它的 invoke 方法会拦截所有的方法调用。invoke 方法会对所拦截的方法进行检测,以决定是否执行插件逻辑。该方法的逻辑如下:
// -☆- Plugin
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
/*
* 获取被拦截方法列表,比如:
* signatureMap.get(Executor.class),可能返回 [query, update, commit]
*/
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);
}
}
invoke 方法的代码比较少,逻辑不难理解。首先,invoke 方法会检测被拦截方法是否配置在插件的 @Signature 注解中,若是,则执行插件逻辑,否则执行被拦截方法。插件逻辑封装在 intercept 中,该方法的参数类型为 Invocation。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;
}
// 省略部分代码
public Object proceed() throws InvocationTargetException, IllegalAccessException {
// 调用被拦截的方法
return method.invoke(target, args);
}
}
关于插件的执行逻辑就分析到这,整个过程不难理解,大家简单看看即可。
4. 实现一个分页插件
为了更好的向大家介绍 MyBatis 的插件机制,下面我将手写一个针对 MySQL 的分页插件。Talk is cheap. Show code.
@Intercepts({
@Signature(
type = Executor.class, // 目标类
method = "query", // 目标方法
args ={MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}
)
})
public class MySqlPagingPlugin implements Interceptor {
private static final Integer MAPPED_STATEMENT_INDEX = 0;
private static final Integer PARAMETER_INDEX = 1;
private static final Integer ROW_BOUNDS_INDEX = 2;
@Override
public Object intercept(Invocation invocation) throws Throwable {
Object[] args = invocation.getArgs();
RowBounds rb = (RowBounds) args[ROW_BOUNDS_INDEX];
// 无需分页
if (rb == RowBounds.DEFAULT) {
return invocation.proceed();
}
// 将原 RowBounds 参数设为 RowBounds.DEFAULT,关闭 MyBatis 内置的分页机制
args[ROW_BOUNDS_INDEX] = RowBounds.DEFAULT;
MappedStatement ms = (MappedStatement) args[MAPPED_STATEMENT_INDEX];
BoundSql boundSql = ms.getBoundSql(args[PARAMETER_INDEX]);
// 获取 SQL 语句,拼接 limit 语句
String sql = boundSql.getSql();
String limit = String.format("LIMIT %d,%d", rb.getOffset(), rb.getLimit());
sql = sql + " " + limit;
// 创建一个 StaticSqlSource,并将拼接好的 sql 传入
SqlSource sqlSource = new StaticSqlSource(ms.getConfiguration(), sql, boundSql.getParameterMappings());
// 通过反射获取并设置 MappedStatement 的 sqlSource 字段
Field field = MappedStatement.class.getDeclaredField("sqlSource");
field.setAccessible(true);
field.set(ms, sqlSource);
// 执行被拦截方法
return invocation.proceed();
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
}
}
上面的分页插件通过 RowBounds 参数获取分页信息,并生成相应的 limit 语句。之后拼接 sql,并使用该 sql 作为参数创建 StaticSqlSource。最后通过反射替换 MappedStatement 对象中的 sqlSource 字段。以上代码中出现了一些大家不太熟悉的类,比如 BoundSql,MappedStatement 以及 StaticSqlSource,这里简单解释一下吧。BoundSql 包含了经过解析后的 sql 语句,以及使用者运行时传入的参数,这些参数最终会被设置到 sql 中。MappedStatement 与映射文件中的 <select>,<insert> 等节点对应,包含了节点的配置信息,比如 id,fetchSize 以及 SqlSource。StaticSqlSource 是 SqlSource 实现类之一,包含完全解析后的 sql 语句。所谓完全解析是指 sql 语句中不包含 ${xxx} 或 #{xxx} 等占位符,以及其他一些未解析的动态节点,比如 <if>,<where> 等。关于这些类就介绍这么多,如果大家还是不怎么理解的话,可以看看我之前写的文章。接下里,写点测试代码验证一下插件是否可以正常运行。先来看一下 Dao 接口与映射文件的定义:
public interface StudentDao {
List<Student> findByPaging(@Param("id") Integer id, RowBounds rb);
}
<mapper namespace="xyz.coolblog.dao6.StudentDao">
<select id="findByPaging" resultType="xyz.coolblog.model5.Student">
SELECT
`id`, `name`, `age`
FROM
student
WHERE
id > #{id}
</select>
</mapper>
测试代码如下:
public class PluginTest {
private SqlSessionFactory sqlSessionFactory;
@Before
public void prepare() throws IOException {
String resource = "mybatis-plugin-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
inputStream.close();
}
@Test
public void testPlugin() {
SqlSession session = sqlSessionFactory.openSession();
try {
StudentDao studentDao = session.getMapper(StudentDao.class);
studentDao.findByPaging(1, new RowBounds(20, 10));
} finally {
session.close();
}
}
}
上面代码运行之后,会打印如下日志。

在上面的输出中,SQL 语句中包含了 LIMIT 字样,这说明插件生效了。
5. 总结
到此,关于 MyBatis 插件机制就分析完了。总体来说,MyBatis 插件机制比较简单。但实现一个插件却较为复杂,需要对 MyBatis 比较了解才行。因此,若想写出高效的插件,还需深入学习源码才行。
好了,本篇文章就先到这了。感谢大家的阅读。
附录:MyBatis 源码分析系列文章列表
| 更新时间 | 标题 |
|---|---|
| 2018-09-11 | MyBatis 源码分析系列文章合集 |
| 2018-07-16 | MyBatis 源码分析系列文章导读 |
| 2018-07-20 | MyBatis 源码分析 - 配置文件解析过程 |
| 2018-07-30 | MyBatis 源码分析 - 映射文件解析过程 |
| 2018-08-17 | MyBatis 源码分析 - SQL 的执行过程 |
| 2018-08-19 | MyBatis 源码分析 - 内置数据源 |
| 2018-08-25 | MyBatis 源码分析 - 缓存原理 |
| 2018-08-26 | MyBatis 源码分析 - 插件机制 |
本文在知识共享许可协议 4.0 下发布,转载需在明显位置处注明出处
作者:田小波
本文同步发布在我的个人博客:http://www.tianxiaobo.com

本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可。
MyBatis 源码分析 - 插件机制的更多相关文章
- 精尽MyBatis源码分析 - 插件机制
该系列文档是本人在学习 Mybatis 的源码过程中总结下来的,可能对读者不太友好,请结合我的源码注释(Mybatis源码分析 GitHub 地址.Mybatis-Spring 源码分析 GitHub ...
- MyBatis 源码分析 - 缓存原理
1.简介 在 Web 应用中,缓存是必不可少的组件.通常我们都会用 Redis 或 memcached 等缓存中间件,拦截大量奔向数据库的请求,减轻数据库压力.作为一个重要的组件,MyBatis 自然 ...
- MyBatis 源码分析 - 内置数据源
1.简介 本篇文章将向大家介绍 MyBatis 内置数据源的实现逻辑.搞懂这些数据源的实现,可使大家对数据源有更深入的认识.同时在配置这些数据源时,也会更清楚每种属性的意义和用途.因此,如果大家想知其 ...
- MyBatis 源码分析 - 映射文件解析过程
1.简介 在上一篇文章中,我详细分析了 MyBatis 配置文件的解析过程.由于上一篇文章的篇幅比较大,加之映射文件解析过程也比较复杂的原因.所以我将映射文件解析过程的分析内容从上一篇文章中抽取出来, ...
- MyBatis 源码分析 - 配置文件解析过程
* 本文速览 由于本篇文章篇幅比较大,所以这里拿出一节对本文进行快速概括.本篇文章对 MyBatis 配置文件中常用配置的解析过程进行了较为详细的介绍和分析,包括但不限于settings,typeAl ...
- MyBatis 源码分析系列文章导读
1.本文速览 本篇文章是我为接下来的 MyBatis 源码分析系列文章写的一个导读文章.本篇文章从 MyBatis 是什么(what),为什么要使用(why),以及如何使用(how)等三个角度进行了说 ...
- 精尽MyBatis源码分析 - 文章导读
该系列文档是本人在学习 Mybatis 的源码过程中总结下来的,可能对读者不太友好,请结合我的源码注释(Mybatis源码分析 GitHub 地址.Mybatis-Spring 源码分析 GitHub ...
- MyBatis 源码分析系列文章合集
1.简介 我从七月份开始阅读MyBatis源码,并在随后的40天内陆续更新了7篇文章.起初,我只是打算通过博客的形式进行分享.但在写作的过程中,发现要分析的代码太多,以至于文章篇幅特别大.在这7篇文章 ...
- MyBatis 源码分析-项目总览
MyBatis 源码分析-项目总览 1.概述 本文主要大致介绍一下MyBatis的项目结构.引用参考资料<MyBatis技术内幕> 此外,https://mybatis.org/mybat ...
随机推荐
- SpringBoot 试手(简易的SpringBoot搭建步骤)
SpringBoot 也算AI吧,它根据您架构中引用的依赖,自动化地按默认方案帮您完成了Spring那些复杂繁琐的配置工作.为了让您不会看低此 AI 水平,还特地喊出了“约定大于配置”的口号.从这个角 ...
- 初试Python语法小试牛刀之冒泡排序
Python很火,心里很慌,没吃过猪肉,也要见见猪走路. 看了几天Python的语法,大概初步了解了一点点,https://www.liaoxuefeng.com/wiki/0014316089557 ...
- ceph常用运维技巧总结1
格式 json 数据增强可读性 --format json-pretty -f json-pretty ceph quorum_status -f json-pretty ceph mon_statu ...
- Python实现EXCEL表格的排序功能
EXCEL的数值排序功能还是挺强大的,升序.降序,尤其自定义排序,能够对多个字段进行排序工作. 那么,在Python大法中,有没有这样强大的排序功能呢?答案是有的,而且本人觉得Python的排序功能, ...
- 常用的stm32库函数
//初始化的方式:先定义初始化机构体.再打开时钟使能.在对每一组GPIO口进行初始化. GPIO_InitTypeDef LED_GPIO; RCC_APB2PeriphClockCmd(RCC_AP ...
- java学习笔记(八):继承、extends、super、this、final关键字
继承解决代码重用的问题,方便管理和维护代码. 继承 子类拥有父类非private的属性,方法. 子类可以拥有自己的属性和方法,即子类可以对父类进行扩展. 子类可以用自己的方式实现父类的方法. Java ...
- 使用 Composer 安装Laravel扩展包的几种方法
使用 Composer 安装Laravel扩展包的几种方法 以下的三种方法都是需要你在项目的根目录运行 第一种:composer install 如有 composer.lock 文件,直接安装,否则 ...
- 114. Flatten Binary Tree to Linked List 把二叉树变成链表
[抄题]: Given a binary tree, flatten it to a linked list in-place. For example, given the following tr ...
- 利用redis制作消息队列
redis在游戏服务器中的使用初探(一) 环境搭建redis在游戏服务器中的使用初探(二) 客户端开源库选择redis在游戏服务器中的使用初探(三) 信息存储redis在游戏服务器中的使用初探(四) ...
- 字符编码中ASCII、Unicode和UTF-8的区别
1. ASCII码 我们知道,在计算机内部,所有的信息最终都表示为一个二进制的字符串.每一个二进制位(bit)有0和1两种状态,因此八个二进制位就可以组合出256种状态,这被称为一个字节(byte). ...