上一篇文章 Mybatis拦截器之数据加密解密 介绍了 Mybatis 拦截器的简单使用,这篇文章将透彻的分析 Mybatis 是怎样发现拦截器以及调用拦截器的 intercept 方法的

小伙伴先按照文章内容细致但不入微的了解整个拦截器执行过程,在纸上勾勒出各个点,再细致入微的读源码,将这些点用线串起来,这样站在上帝视角后,理解的更加深刻

发现拦截器

按照官网说明,我们通过实现 org.apache.ibatis.plugin.Interceptor 接口自定义的拦截器,有两种方式将自定义拦截器添加到 Mybatis 的 configuration 中

配置文件方式

mybatis-config.xml 中添加 plugin

<!-- mybatis-config.xml -->
<plugins>
<plugin interceptor="org.mybatis.example.ExamplePlugin">
<property name="someProperty" value="100"/>
</plugin>
</plugins>

XMLConfigBuilder 负责解析 Mybatis 全局配置文件,其中有 pluginElement 方法:

  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 interceptorInstance = (Interceptor) resolveClass(interceptor).newInstance();
interceptorInstance.setProperties(properties);
configuration.addInterceptor(interceptorInstance);
}
}
}

从该方法中可以看出,遍历 XML XNode,如果该 node 有 interceptor 属性,说明我们配置了拦截器,通过 configuration.addInterceptor(interceptorInstance); 将拦截器实例添加到 Mybatis Configuration 中

注解方式

文章 Mybatis拦截器之数据加密解密中看到我在自定义的拦截器类上添加了 @Component 注解, 当下微服务框架中多以 Spring Boot 添加 Mybatis Starter 依赖的形式存在,来看MybatisAutoConfiguration.java 的构造方法:

public MybatisAutoConfiguration(MybatisProperties properties,
ObjectProvider<Interceptor[]> interceptorsProvider,
ResourceLoader resourceLoader,
ObjectProvider<DatabaseIdProvider> databaseIdProvider,
ObjectProvider<List<ConfigurationCustomizer>> configurationCustomizersProvider) {
this.properties = properties;
this.interceptors = interceptorsProvider.getIfAvailable();
this.resourceLoader = resourceLoader;
this.databaseIdProvider = databaseIdProvider.getIfAvailable();
this.configurationCustomizers = configurationCustomizersProvider.getIfAvailable();
}

构造方法中 interceptorsProvider.getIfAvailable(); 获取所有注入的 Interceptor,同时在构建 SqlSessionFactory 的时候,添加我们的拦截器:

if (!ObjectUtils.isEmpty(this.interceptors)) {
factory.setPlugins(this.interceptors);
}

调用过程解析

Configuration 类包含 Mybatis 的一切配置信息,里面有 4 个非常重要的方法,也是拦截器拦截的方法

public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {
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) {
ResultSetHandler resultSetHandler = new DefaultResultSetHandler(executor, mappedStatement, parameterHandler, resultHandler, boundSql, rowBounds);
resultSetHandler = (ResultSetHandler) interceptorChain.pluginAll(resultSetHandler);
return resultSetHandler;
} public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql);
statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
return statementHandler;
} public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
executorType = executorType == null ? defaultExecutorType : executorType;
executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
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);
}
if (cacheEnabled) {
executor = new CachingExecutor(executor);
}
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}

他们的执行顺序是 newExecutor -> StatementHandler -> ParameterHandler -> ResultSetHandler -> StatementHandler,为什么是这个顺序,且看:

我们知道在 MyBatis 中,使用 SqlSessionFactory 来创建 SqlSession。 一旦有了会话,就可以使用它来执行映射语句,提交或回滚连接,最后,当不再需要时,关闭会话。

且看,在 DefaultSqlSessionFactory.java 类中的 openSessionFromDataSource 方法中调用 Configuration 类的 newExecutor 方法

private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
Transaction tx = null;
try {
final Environment environment = configuration.getEnvironment();
final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
//调用 Configuration 类的 newExecutor 方法创建一个执行器
final Executor executor = configuration.newExecutor(tx, execType);
return new DefaultSqlSession(configuration, executor, autoCommit);
} catch (Exception e) {
closeTransaction(tx); // may have fetched a connection so lets call close()
throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}

SqlSessionTemplate.java 实现了 SqlSession 接口,里面有一个 私有内部类 SqlSessionInterceptor 并实现了 InvocationHandler, 很明显,这是 Java 动态代理的实现方式,关注重写的 invoke 方法,这里是方法真正被调用的地方,跟踪调用栈发现最终调用到 Configuration 类的 newStatementHandler 方法

    @Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
SqlSession sqlSession = getSqlSession(
SqlSessionTemplate.this.sqlSessionFactory,
SqlSessionTemplate.this.executorType,
SqlSessionTemplate.this.exceptionTranslator);
try {
// 真正的方法执行,顺着方法走下去,就会调用 Configuration 类的 newStatementHandler 方法
Object result = method.invoke(sqlSession, args);
...
} catch (Throwable t) {
...
} finally {
if (sqlSession != null) {
closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
}
}
}
}

在 Configuration 类的 newStatementHandler 方法中,通过 new RoutingStatementHandler(...) 方法来构建 StatementHandler,在该方法中,根据 statementType 来判断生成哪一种 StatementHandler

    public RoutingStatementHandler(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {

    switch (ms.getStatementType()) {
case STATEMENT:
delegate = new SimpleStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
break;
case PREPARED:
delegate = new PreparedStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
break;
case CALLABLE:
delegate = new CallableStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
break;
default:
throw new ExecutorException("Unknown statement type: " + ms.getStatementType());
} }

三种类型 StatementHandler 都继承了 BaseStatementHandler.java, 看下面的类关系图

在实例化具体的 StatementHandler 的时候都会先调用父类 BaseStatementHandler 的构造器,在父类的构造器中分别顺序调用了 Configuration 类中的 newParameterHandlernewResultSetHandler 方法:

this.parameterHandler = configuration.newParameterHandler(mappedStatement, parameterObject, boundSql);
this.resultSetHandler = configuration.newResultSetHandler(executor, mappedStatement, rowBounds, parameterHandler, resultHandler, boundSql);

所以说整个调用过程是这样的:newExecutor -> StatementHandler -> ParameterHandler -> ResultSetHandler -> StatementHandler

说了这么多还没有讲到拦截器是怎样被执行的,别急,前面这些都是铺垫,也许有细心的小伙伴已经发现,在 Configuratin 类中的那四个方法中,都有相同的一段代码:

interceptorChain.pluginAll(...)

没错,通过名字我们也猜测得到,这是拦截器的关键,interceptorChain 是 Configuration 类的成员变量,且看 InterceptorChain.java 类:

public class InterceptorChain {

  private final List<Interceptor> interceptors = new ArrayList<Interceptor>();

  public Object pluginAll(Object target) {
for (Interceptor interceptor : interceptors) {
target = interceptor.plugin(target);
}
return target;
} public void addInterceptor(Interceptor interceptor) {
interceptors.add(interceptor);
} public List<Interceptor> getInterceptors() {
return Collections.unmodifiableList(interceptors);
} }

在 pluginAll 方法中遍历所有拦截器的 plugin 方法,在自定义的拦截器中,我们重写 plugin 方法,这里以 通用分页拦截器 讲解调用拦截器过程,来看关键代码:

@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 PageInterceptor implements Interceptor { @Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
}

Plugin.java 实现了 InvocationHandler 接口,看的出也是 Java 动态代理,调用其静态方法 wrap:

public static Object wrap(Object target, Interceptor interceptor) {
Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
Class<?> type = target.getClass();
Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
if (interfaces.length > 0) {
return Proxy.newProxyInstance(
type.getClassLoader(),
interfaces,
new Plugin(target, interceptor, signatureMap));
}
return target;
}

如果 interfaces.length > 0 也就会为 target 生成代理对象,也就是说为 Configuration类 四个方法调用的 executor/parameterHandler/resultSetHandler/statementHandler 生成代理对象,这里需要单独分析里面的两个很重要的方法 getSignatureMap(interceptor)getAllInterfaces(type, signatureMap)

先看 getSignatureMap(interceptor) 方法:

private static Map<Class<?>, Set<Method>> getSignatureMap(Interceptor interceptor) {
Intercepts interceptsAnnotation = interceptor.getClass().getAnnotation(Intercepts.class);
if (interceptsAnnotation == null) {
throw new PluginException("No @Intercepts annotation was found in interceptor " + interceptor.getClass().getName());
}
Signature[] sigs = interceptsAnnotation.value();
Map<Class<?>, Set<Method>> signatureMap = new HashMap<Class<?>, Set<Method>>();
for (Signature sig : sigs) {
Set<Method> methods = signatureMap.get(sig.type());
if (methods == null) {
methods = new HashSet<Method>();
signatureMap.put(sig.type(), methods);
}
try {
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;
}

该方法通过 Java 反射读取拦截器类上的注解信息,最终返回一个以 Type 为 key,Method 集合为 Value 的HashMap, 以上面分页拦截器为例子, key 是 org.apache.ibatis.executor.Executor, Value 是两个重载的 query 方法

再看 getAllInterfaces(type, signatureMap)

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()]);
}

该方法返回根据目标实例 target 和它的父类们的接口数组,回看 Plugin.wrap 方法,如果接口数组长度大于 0,则为 target 生成代理对象

最后当在 DefaultSqlSession 中执行具体执行时,如 selectList 方法中, 此时的 executor 是刚刚生成的代理对象

return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);

executor 调用的方法就会执行 Plugin 重写的 invoke 方法:

  @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);
}
}

最终,执行自定义拦截器的 intercept 方法,拦截器就是这样被执行的.

我们发现,在 Mybatis 框架中,大量的使用了 Java 动态代理,比如只需在 Mapper 接口中定义方法,并没有具体的实现类,这一切都是应用 Java 动态代理,所以理解动态代理,能更好的理解整个执行过程.

拦截器注解详解

本文中截取了分页拦截器的部分关键代码,看到该拦截器的注解内容是:

@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}),
}
)

而在 Mybatis拦截器之数据加密解密中请求参数拦截器和返回结果集拦截器的内容分别是:

@Intercepts({
@Signature(type = ParameterHandler.class, method = "setParameters", args = PreparedStatement.class),
})
@Intercepts({
@Signature(type = ResultSetHandler.class, method = "handleResultSets", args={Statement.class})
})

每种拦截器拦截的方法签名(Signature)都不一样,我要怎样写呢?其实很简单,这些都是接口 Executor/ParameterHandler/ResultSetHandler 中的方法,按照相应的接口方法在这里配置就好了,在通过反射解析拦截器的时候会判断能否找到相应的方法签名,如果找不到会报 NoSuchMethodException 异常

举例来看 Executor 接口,里面有两个重载的 query 方法,再回看注解中的内容,是不是豁然开朗呢?

<E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey cacheKey, BoundSql boundSql) throws SQLException;

<E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException;

结合文章开头说的铺垫,拦截器拦截 Configuration 类中的四个方法,没错,就是 Executor/ParameterHandler/ResultSetHandler/StatementHandler,继续回看 Mybatis拦截器之数据加密解密开篇拦截器介绍内容,充分理解 Executor/ParameterHandler/ResultSetHandler/StatementHandler 的作用,我们就可以应用拦截器玩出我们自己的花样了,

问题彩蛋

我们看到调用拦截器的时候通过 interceptorChain 进行调用,直译过来就是 拦截器链, 其实这是设计模式之责任链模式

  1. 你了解责任链模式吗?
  2. 你能想到哪些框架或场景中应用了责任链模式吗?
  3. 现实业务中有哪些地方应用责任链模式能让我们代码更灵活健壮呢?
  4. 如果我们定义多个同类型的拦截器,比如多个 Executor 类型拦截器,那么多个拦截器的顺序要怎样把控呢?

提高效率工具

关注公众号,回复工具获取更多那些可以帮助我们高效工作的工具

Free Mybatis Plugin

我们在使用 Mybatis 并需要手写 SQL 时需要在 Mapper 接口中定义方法,同时在 XML 中定义同名 statementId 的 SQL,该Intellij IDEA 插件帮助我们快速定位方法和 XML,并来回切换:

从 Java 到 SQL

从 SQL 到 Java

推荐阅读:

程序猿为什么要看源码


欢迎关注公众号,趣谈coding那些事,提升硬实力

Mybatis拦截器执行过程解析的更多相关文章

  1. "犯罪心理"解读Mybatis拦截器

    原文链接:"犯罪心理"解读Mybatis拦截器 Mybatis拦截器执行过程解析 文章写过之后,我觉得 "Mybatis 拦截器案件"背后一定还隐藏着某种设计动 ...

  2. MyBatis拦截器原理探究

    MyBatis拦截器介绍 MyBatis提供了一种插件(plugin)的功能,虽然叫做插件,但其实这是拦截器功能.那么拦截器拦截MyBatis中的哪些内容呢? 我们进入官网看一看: MyBatis 允 ...

  3. 【公众号转载】MyBatis拦截器原理探究

    MyBatis拦截器介绍 MyBatis提供了一种插件(plugin)的功能,虽然叫做插件,但其实这是拦截器功能.那么拦截器拦截MyBatis中的哪些内容呢? 我们进入官网看一看: MyBatis 允 ...

  4. 详解Mybatis拦截器(从使用到源码)

    详解Mybatis拦截器(从使用到源码) MyBatis提供了一种插件(plugin)的功能,虽然叫做插件,但其实这是拦截器功能. 本文从配置到源码进行分析. 一.拦截器介绍 MyBatis 允许你在 ...

  5. Mybatis拦截器 mysql load data local 内存流处理

    Mybatis 拦截器不做解释了,用过的基本都知道,这里用load data local主要是应对大批量数据的处理,提高性能,也支持事务回滚,且不影响其他的DML操作,当然这个操作不要涉及到当前所lo ...

  6. Mybatis拦截器(一)

    拦截器需要实现 interceptor接口 public interface Interceptor { //3 对目标对象拦截进行处理的内容 Object intercept(Invocation ...

  7. mybatis拦截器使用

    目录 mybatis 拦截器接口Interceptor spring boot + mybatis整合 创建自己的拦截器MyInterceptor @Intercepts注解 mybatis拦截器入门 ...

  8. 玩转SpringBoot之整合Mybatis拦截器对数据库水平分表

    利用Mybatis拦截器对数据库水平分表 需求描述 当数据量比较多时,放在一个表中的时候会影响查询效率:或者数据的时效性只是当月有效的时候:这时我们就会涉及到数据库的分表操作了.当然,你也可以使用比较 ...

  9. Mybatis拦截器实现原理深度分析

    1.拦截器简介 拦截器可以说使我们平时开发经常用到的技术了,Spring AOP.Mybatis自定义插件原理都是基于拦截器实现的,而拦截器又是以动态代理为基础实现的,每个框架对拦截器的实现不完全相同 ...

随机推荐

  1. Android ListView拉到顶/底部,像橡皮筋一样弹性回弹复位

     <Android ListView拉到顶/底部,像橡皮筋一样弹性回弹复位> Android本身的ListView拉到顶部或者底部会在顶部/底部边缘间隙出现一道"闪光&quo ...

  2. linux高级技巧:rsync同步(二)

    1.配置两个节点同步         上个帖子已经展示了同步一个主机的方法.这次我们再加入一个要同步的主机. 方法上并无二质.         1.首先显示我们的控制台:                ...

  3. jqm文件上传,上传图片,jqm的表单操作,jqm的ajax的使用,jqm文件操作大全,文件操作demo

    近期在论坛中看到.在使用html5中上传图片或文件,出现各种问题. 这一方面,我也一直没有做过,今天就抽出了一点时间来学习一下.如今的演示样例已经ok了,我就给大家分享一下,希望对大家有帮助. 好吧. ...

  4. 手游server之数据IO进化

    这里数据IO是指游戏数据存盘和读取. 假设IO处理不好.server在IO时会导致.游戏卡顿较长的时间,严重影响游戏体验. 近期服务端刚好对IO这一块做了优化,把优化过程记录一下. 一 原始版 刚開始 ...

  5. 求int型数据在内存中存储时1的个数

    1.求int型数据在内存中存储时1的个数 输入一个int型数据,计算出该int型数据在内存中存储时1的个数. 我们非常easy想到例如以下方法: #include <iostream> u ...

  6. Softmax回归——logistic回归模型在多分类问题上的推广

    Softmax回归 Contents [hide] 1 简介 2 代价函数 3 Softmax回归模型参数化的特点 4 权重衰减 5 Softmax回归与Logistic 回归的关系 6 Softma ...

  7. Three学习之曲线

    曲线 属性 1. .arcLengthDivisions 当通过.getLengths计算曲线的累积段长度时,此值决定了分割的数量.为了确保在使用.getSpacedPoint等方法时的精度,如果曲线 ...

  8. INFORMIX MATCHES的使用详解

    MATCHES  运算符返回 TRUE ,如果一个字符串与给定的掩码匹配. 语法 expr  [NOT]  MATCHES  mask   [ ESCAPE "char" ] ex ...

  9. javascript实现双击网页自动滚动,单击滚动停止

    当网页中有长篇文章时,浏览起来就比较吃劲了,想想一边忙着拖动滚动条,一边忙着浏览,确实挺累人的.为了客人能够轻松的浏览,我们可以使用script代码实现网页的自动滚屏,当双击网页的时候,网页将会自动向 ...

  10. Tomcat应用通过Nat123部署到外网(Tomcat+Nat123)

    这里吐槽下,我先想到的方式是用花生壳域名解析,但是花生壳坑太多不易新手操作,用户体验做的不好.然后度娘后才知道有Nat123这个比花生壳简易操作的软件. 1.到nat123官网下载客户端  http: ...