本章主要描述 MyBatis 插件模块的原理,从以下两点出发:

  1. MyBatis 是如何加载插件配置的?
  2. 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 源码篇-插件模块的更多相关文章

  1. MyBatis 源码篇-日志模块2

    上一章的案例,配置日志级别为 debug,执行一个简单的查询操作,会将 JDBC 操作打印出来.本章通过 MyBatis 日志部分源码分析它是如何实现日志打印的. 在 MyBatis 的日志模块中有一 ...

  2. MyBatis 源码篇-日志模块1

    在 Java 开发中常用的日志框架有 Log4j.Log4j2.Apache Common Log.java.util.logging.slf4j 等,这些日志框架对外提供的接口各不相同.本章详细描述 ...

  3. MyBatis 源码篇-MyBatis-Spring 剖析

    本章通过分析 mybatis-spring-x.x.x.jar Jar 包中的源码,了解 MyBatis 是如何与 Spring 进行集成的. Spring 配置文件 MyBatis 与 Spring ...

  4. MyBatis 源码篇-Transaction

    本章简单介绍一下 MyBatis 的事务模块,这块内容比较简单,主要为后面介绍 mybatis-spring-1.**.jar(MyBatis 与 Spring 集成)中的事务模块做准备. 类图结构 ...

  5. MyBatis 源码篇-DataSource

    本章介绍 MyBatis 提供的数据源模块,为后面与 Spring 集成做铺垫,从以下三点出发: 描述 MyBatis 数据源模块的类图结构: MyBatis 是如何集成第三方数据源组件的: Pool ...

  6. MyBatis 源码篇-资源加载

    本章主要描述 MyBatis 资源加载模块中的 ClassLoaderWrapper 类和 Java 加载配置文件的三种方式. ClassLoaderWrapper 上一章的案例,使用 org.apa ...

  7. MyBatis 源码篇-SQL 执行的流程

    本章通过一个简单的例子,来了解 MyBatis 执行一条 SQL 语句的大致过程是怎样的. 案例代码如下所示: public class MybatisTest { @Test public void ...

  8. MyBatis 源码篇-整体架构

    MyBatis 的整体架构分为三层, 分别是基础支持层.核心处理层和接口层,如下图所示. 基础支持层 反射模块 该模块对 Java 原生的反射进行了良好的封装,提供了更加简洁易用的 API ,方便上层 ...

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

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

随机推荐

  1. iOS开发系列--地图与定位总结

    现在很多社交.电商.团购应用都引入了地图和定位功能,似乎地图功能不再是地图应用和导航应用所特有的.的确,有了地图和定位功能确实让我们的生活更加丰富多彩,极大的改变了我们的生活方式.例如你到了一个陌生的 ...

  2. Redis字符串(String)

    1.set SET key value [EX seconds] [PX milliseconds] [NX|XX] 将字符串值 value 关联到 key 可选参数: EX second :设置键的 ...

  3. 第11组 Beta冲刺(5/5)

    第11组 Beta冲刺(5/5)   队名 不知道叫什么团队 组长博客 https://www.cnblogs.com/xxylac/p/12031050.html 作业博客 https://edu. ...

  4. 一百三十三:CMS系统之版块管理一

    把模型创建到公共的models里面 class BoardModel(db.Model): __tablename__ = 'board' id = db.Column(db.Integer, pri ...

  5. 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 ...

  6. Flutter之ExpansionTile组件

    ExpansionTile组件 ExpansionTile Widget就是一个可以展开闭合的组件,常用的属性有如下几个. title:闭合时显示的标题,这个部分经常使用Text Widget. le ...

  7. IDEA配置编码

  8. Note 2: Complain

    Note 2: Complain 1. The collection of Linkun's [1]: 1.1suck If someone says that something sucks, th ...

  9. 【首发】uExitCode解释

    uExitCode 进程退出码,一般设置为0,不是的话也没有错误. 它的主要作用是,通知调用这个程序的其他程序退出的原因. 以前在dos下可能用的较多,现在windows下用的很少,而且windows ...

  10. 【Qt开发】【Gstreamer开发】Qt error: glibconfig.h: No such file or directory #include

    今天遇到一个问题如题 但是明明安装了 glib2.0和gtk,但是仍然找不到glibconfig.h,自己在/usr/include下找来也确实没有,然后只能在全盘搜啦 位置在: /usr/lib/x ...