MyBatis 源码篇-插件模块
本章主要描述 MyBatis 插件模块的原理,从以下两点出发:
- MyBatis 是如何加载插件配置的?
- MyBatis 是如何实现用户使用自定义拦截器对 SQL 语句执行过程中的某一点进行拦截的?
示例准备
首先准备两个拦截器示例,代码如下。
@Intercepts({
@Signature(type = Executor.class, method = "query",
args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class,
ResultHandler.class, CacheKey.class, BoundSql.class})})
public class AInterceptor implements Interceptor {
private static final Logger LOGGER = LoggerFactory.getLogger(AInterceptor.class);
/**
* 执行拦截逻辑的方法
*
* @param invocation
* @return
* @throws Throwable
*/
@Override
public Object intercept(Invocation invocation) throws Throwable {
LOGGER.info("--------------执行拦截器A前--------------");
Object obj = invocation.proceed();
LOGGER.info("--------------执行拦截器A后--------------");
return obj;
}
/**
* 决定是否触发intercept()方法
*
* @param target
* @return
*/
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
/**
* 根据配置初始化Interceptor对象
*
* @param properties
*/
@Override
public void setProperties(Properties properties) {
}
}
@Intercepts({
@Signature(type = Executor.class, method = "query",
args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class,
ResultHandler.class, CacheKey.class, BoundSql.class})})
public class BInterceptor implements Interceptor {
private static final Logger LOGGER = LoggerFactory.getLogger(BInterceptor.class);
/**
* 执行拦截逻辑的方法
*
* @param invocation
* @return
* @throws Throwable
*/
@Override
public Object intercept(Invocation invocation) throws Throwable {
LOGGER.info("--------------执行拦截器B前--------------");
Object obj = invocation.proceed();
LOGGER.info("--------------执行拦截器B后--------------");
return obj;
}
/**
* 决定是否触发intercept()方法
*
* @param target
* @return
*/
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
/**
* 根据配置初始化Interceptor对象
*
* @param properties
*/
@Override
public void setProperties(Properties properties) {
}
}
MyBatis 配置文件 mybatis-config.xml 增加 plugin 配置。
<plugins>
<plugin interceptor="com.yjw.mybatis.test.mybatis.plugin.AInterceptor"/>
<plugin interceptor="com.yjw.mybatis.test.mybatis.plugin.BInterceptor"/>
</plugins>
加载插件配置
在 MyBatis 初始化时,会通过 XMLConfigBuilder#pluginElement 方法解析 mybatis-config.xml 配置文件中定义的 <plugin> 节点,得到相应的 Interceptor 对象,最后将 Interceptor 对象添加到 Configuration.interceptorChain 字段中保存。源码如下所示。
private void pluginElement(XNode parent) throws Exception {
if (parent != null) {
for (XNode child : parent.getChildren()) {
String interceptor = child.getStringAttribute("interceptor");
Properties properties = child.getChildrenAsProperties();
// 创建Interceptor对象
Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).newInstance();
interceptorInstance.setProperties(properties);
// 保存到Configuration.interceptorChain字段中
configuration.addInterceptor(interceptorInstance);
}
}
}
public void addInterceptor(Interceptor interceptor) {
interceptorChain.addInterceptor(interceptor);
}
拦截过程
继续介绍 MyBatis 的拦截器如何对 Exector、StatementHandler、ParameterHandler、ResultSetHandler 进行拦截。
在 MyBatis 中使用的这四类对象,都是通过 Configuration 创建的,方法如下图所示。如果配置了自定义拦截器,则会在该系列方法中,通过 InterceptorChain.pluginAll() 方法为目标对象创建代理对象,所以通过 Configuration.new*() 系列方法得到的对象实际是一个代理对象。

以 newExecutor() 方法为例进行分析,其他方法原理类似,newExecutor() 方法的具体实现如下所示。
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
executorType = executorType == null ? defaultExecutorType : executorType;
executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
Executor executor;
// 默认是SIMPLE
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);
}
if (cacheEnabled) {
executor = new CachingExecutor(executor);
}
// 通过InterceptorChain.pluginAll()方法创建Exector代理对象
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
在 InterceptorChain.pluginAll() 方法会遍历 interceptors 集合,并调用每个 interceptor 的 plugin() 方法创建代理对象,具体实现如下所示。
public Object pluginAll(Object target) {
for (Interceptor interceptor : interceptors) {
target = interceptor.plugin(target);
}
return target;
}
一般我们自定义拦截器的 plugin 方法,会使用 MyBatis 提供的 Plugin 工具类,它实现了 InvocationHandler 接口,并提供了 wrap() 静态方法用于创建代理对象,Plugin.wrap() 方法的具体实现如下所示。
public static Object wrap(Object target, Interceptor interceptor) {
// 获取用户自定义Interceptor中@Signature注解的信息
// getSignatureMap()方法负责处理@Signature注解
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;
}
private static Class<?>[] getAllInterfaces(Class<?> type, Map<Class<?>, Set<Method>> signatureMap) {
Set<Class<?>> interfaces = new HashSet<Class<?>>();
while (type != null) {
for (Class<?> c : type.getInterfaces()) {
if (signatureMap.containsKey(c)) {
interfaces.add(c);
}
}
type = type.getSuperclass();
}
return interfaces.toArray(new Class<?>[interfaces.size()]);
}
示例中 Exector 存在两个拦截器 AInterceptor 和 BInterceptor,在执行 InterceptorChain.pluginAll() 方法的时候,传给 getAllInterfaces() 方法的 type 字段第一次是 CacheExector 对象,第二次是 CacheExector 的代理对象,因为生成的代理对象也继承 Exector 接口,signatureMap.containsKey(c) 可以获得值,继续生成代理的代理对象,结构如下图所示。

在 Plugin.invoke() 方法中,会将当前调用方法与 signatureMap 集合中记录的方法信息进行比较,如果当前调用的方法是需要被拦截的方法,则调用其 intercept() 方法进行处理,如果不能被拦截则直接调用 target 的相应方法。Plugin.invoke() 方法的具体实现如下所示。
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
// 获取当前方法所在类或接口中,可被当前 Interceptor拦截的方法
Set<Method> methods = signatureMap.get(method.getDeclaringClass());
// 如果当前调用的方法需要被拦截,则调用interceptor.intercept()方法进行拦截处理
if (methods != null && methods.contains(method)) {
return interceptor.intercept(new Invocation(target, method, args));
}
// 如果当前调用的方法不能被拦截,则调用target对象的相应方法
return method.invoke(target, args);
} catch (Exception e) {
throw ExceptionUtil.unwrapThrowable(e);
}
}
Interceptor.intercept() 方法的参数是 Invocation 对象,其中封装了目标对象、目标方法以及调用目标方法的参数,并提供了 process() 方法调用目标方法,如下所示。
public Object proceed() throws InvocationTargetException, IllegalAccessException {
return method.invoke(target, args);
}
需要注意的是,在 Interceptor.intercept() 方法中执行完拦截处理之后,如果需要调用目标方法,则通过 Invocation.process() 方法实现。
根据上面的分析,就不难理解示例的如下输出日志了,同时配置文件中插件的执行顺序也清楚了。
[main] DEBUG org.apache.ibatis.logging.LogFactory - Logging initialized using 'class org.apache.ibatis.logging.slf4j.Slf4jImpl' adapter.
[main] DEBUG org.apache.ibatis.datasource.pooled.PooledDataSource - PooledDataSource forcefully closed/removed all connections.
[main] DEBUG org.apache.ibatis.datasource.pooled.PooledDataSource - PooledDataSource forcefully closed/removed all connections.
[main] DEBUG org.apache.ibatis.datasource.pooled.PooledDataSource - PooledDataSource forcefully closed/removed all connections.
[main] DEBUG org.apache.ibatis.datasource.pooled.PooledDataSource - PooledDataSource forcefully closed/removed all connections.
[main] INFO com.yjw.mybatis.test.mybatis.plugin.BInterceptor - --------------执行拦截器B前--------------
[main] INFO com.yjw.mybatis.test.mybatis.plugin.AInterceptor - --------------执行拦截器A前--------------
[main] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Opening JDBC Connection
[main] DEBUG org.apache.ibatis.datasource.pooled.PooledDataSource - Created connection 360067785.
[main] DEBUG com.yjw.mybatis.dao.StudentMapper.selectByPrimaryKey - ==> Preparing: select id, name, sex, selfcard_no, note from t_student where id = ?
[main] DEBUG com.yjw.mybatis.dao.StudentMapper.selectByPrimaryKey - ==> Parameters: 1(Long)
[main] DEBUG com.yjw.mybatis.dao.StudentMapper.selectByPrimaryKey - <== Total: 1
[main] INFO com.yjw.mybatis.test.mybatis.plugin.AInterceptor - --------------执行拦截器A后--------------
[main] INFO com.yjw.mybatis.test.mybatis.plugin.BInterceptor - --------------执行拦截器B后--------------
Student [Hash = 550752602, id=1, name=张三, sex=1, selfcardNo=111, note=zhangsan]
MyBatis 源码篇
MyBatis 源码篇-插件模块的更多相关文章
- MyBatis 源码篇-日志模块2
上一章的案例,配置日志级别为 debug,执行一个简单的查询操作,会将 JDBC 操作打印出来.本章通过 MyBatis 日志部分源码分析它是如何实现日志打印的. 在 MyBatis 的日志模块中有一 ...
- MyBatis 源码篇-日志模块1
在 Java 开发中常用的日志框架有 Log4j.Log4j2.Apache Common Log.java.util.logging.slf4j 等,这些日志框架对外提供的接口各不相同.本章详细描述 ...
- MyBatis 源码篇-MyBatis-Spring 剖析
本章通过分析 mybatis-spring-x.x.x.jar Jar 包中的源码,了解 MyBatis 是如何与 Spring 进行集成的. Spring 配置文件 MyBatis 与 Spring ...
- MyBatis 源码篇-Transaction
本章简单介绍一下 MyBatis 的事务模块,这块内容比较简单,主要为后面介绍 mybatis-spring-1.**.jar(MyBatis 与 Spring 集成)中的事务模块做准备. 类图结构 ...
- MyBatis 源码篇-DataSource
本章介绍 MyBatis 提供的数据源模块,为后面与 Spring 集成做铺垫,从以下三点出发: 描述 MyBatis 数据源模块的类图结构: MyBatis 是如何集成第三方数据源组件的: Pool ...
- MyBatis 源码篇-资源加载
本章主要描述 MyBatis 资源加载模块中的 ClassLoaderWrapper 类和 Java 加载配置文件的三种方式. ClassLoaderWrapper 上一章的案例,使用 org.apa ...
- MyBatis 源码篇-SQL 执行的流程
本章通过一个简单的例子,来了解 MyBatis 执行一条 SQL 语句的大致过程是怎样的. 案例代码如下所示: public class MybatisTest { @Test public void ...
- MyBatis 源码篇-整体架构
MyBatis 的整体架构分为三层, 分别是基础支持层.核心处理层和接口层,如下图所示. 基础支持层 反射模块 该模块对 Java 原生的反射进行了良好的封装,提供了更加简洁易用的 API ,方便上层 ...
- MyBatis 源码分析 - 插件机制
1.简介 一般情况下,开源框架都会提供插件或其他形式的拓展点,供开发者自行拓展.这样的好处是显而易见的,一是增加了框架的灵活性.二是开发者可以结合实际需求,对框架进行拓展,使其能够更好的工作.以 My ...
随机推荐
- iOS开发系列--地图与定位总结
现在很多社交.电商.团购应用都引入了地图和定位功能,似乎地图功能不再是地图应用和导航应用所特有的.的确,有了地图和定位功能确实让我们的生活更加丰富多彩,极大的改变了我们的生活方式.例如你到了一个陌生的 ...
- Redis字符串(String)
1.set SET key value [EX seconds] [PX milliseconds] [NX|XX] 将字符串值 value 关联到 key 可选参数: EX second :设置键的 ...
- 第11组 Beta冲刺(5/5)
第11组 Beta冲刺(5/5) 队名 不知道叫什么团队 组长博客 https://www.cnblogs.com/xxylac/p/12031050.html 作业博客 https://edu. ...
- 一百三十三:CMS系统之版块管理一
把模型创建到公共的models里面 class BoardModel(db.Model): __tablename__ = 'board' id = db.Column(db.Integer, pri ...
- PAT 甲级 1013 Battle Over Cities (25 分)(图的遍历,统计强连通分量个数,bfs,一遍就ac啦)
1013 Battle Over Cities (25 分) It is vitally important to have all the cities connected by highway ...
- Flutter之ExpansionTile组件
ExpansionTile组件 ExpansionTile Widget就是一个可以展开闭合的组件,常用的属性有如下几个. title:闭合时显示的标题,这个部分经常使用Text Widget. le ...
- IDEA配置编码
- Note 2: Complain
Note 2: Complain 1. The collection of Linkun's [1]: 1.1suck If someone says that something sucks, th ...
- 【首发】uExitCode解释
uExitCode 进程退出码,一般设置为0,不是的话也没有错误. 它的主要作用是,通知调用这个程序的其他程序退出的原因. 以前在dos下可能用的较多,现在windows下用的很少,而且windows ...
- 【Qt开发】【Gstreamer开发】Qt error: glibconfig.h: No such file or directory #include
今天遇到一个问题如题 但是明明安装了 glib2.0和gtk,但是仍然找不到glibconfig.h,自己在/usr/include下找来也确实没有,然后只能在全盘搜啦 位置在: /usr/lib/x ...