查看原文
其他

Mybatis拦截器执行过程解析

tan日拱一兵 日拱一兵 2020-10-16

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

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

发现拦截器

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

配置文件方式

在 mybatis-config.xml 中添加 plugin (Mybatis 将拦截器叫做 plugin)

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

XMLConfigBuilder.java 负责解析 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();}

以上构造方法第 7 行 interceptorsProvider.getIfAvailable(); 获取所有注入的 Interceptor,之后在构建 SqlSessionFactory.java 的时候,添加我们的拦截器:

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 方法 (第二步)

@Overridepublic 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 类中的 newParameterHandler (第三步)和 newResultSetHandler (第四步)方法:

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

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

说了这么多还没有讲到拦截器是怎样被执行的,别急,以上的内容是必要的铺垫,先留有印象,也许有细心的小伙伴已经发现,在 Configuration 类中的那四个方法中,都有相同的一段代码:

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 方法,如果 interfaces.length > 0 (接口数组长度大于 0),则为 target 生成代理对象最后当在 DefaultSqlSession 中执行具体执行时,如 selectList 方法中, 此时的 executor 是刚刚生成的代理对象

return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);executor 调用的方法就会执行 Plugin 重写的 invoke 方法:
@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable { try { Set<Method> methods = signatureMap.get(method.getDeclaringClass()); if (methods != null && methods.contains(method)) {      //拦截器的 intercept 方法执行 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


推荐阅读:程序猿为什么要看源码
欢迎留言交流,你有一个思想,我有一个思想, 我们交换后就都有两个思想


    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存