上一篇文章 Mybatis拦截器之数据加密解密 介绍了 Mybatis 拦截器的简单使用,这篇文章将透彻的分析 Mybatis 是怎样发现拦截器以及调用拦截器 intercept 方法的
小伙伴先按照文章内容细致但不入微的了解整个拦截器执行过程,在纸上勾勒出各个点,再细致入微的读源码,将这些点用线串起来,这样站在上帝视角后,理解的更加深刻
发现拦截器
按照官网说明,我们通过实现 org.apache.ibatis.plugin.Interceptor 接口自定义的拦截器,有两种方式将自定义拦截器添加到 Mybatis 的 configuration 中
配置文件方式
在 mybatis-config.xml 中添加 plugin (Mybatis 将拦截器叫做 plugin)
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 interceptorsProvider,
ResourceLoader resourceLoader,
ObjectProvider databaseIdProvider,
ObjectProvider> 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 方法 (第二步)
@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 类中的 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 interceptors = new ArrayList();
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 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, Set> 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, Set> 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, Set> signatureMap = new HashMap, Set>();
for (Signature sig : sigs) {
Set methods = signatureMap.get(sig.type());
if (methods == null) {
methods = new HashSet();
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, Set> signatureMap) {
Set> interfaces = new HashSet>();
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 方法:
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
Set 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 方法,再回看注解中的内容,是不是豁然开朗呢?
List query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey cacheKey, BoundSql boundSql) throws SQLException;
List query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException;
结合文章开头说的铺垫,拦截器拦截 Configuration 类中的四个方法,没错,就是 Executor/ParameterHandler/ResultSetHandler/StatementHandler,继续回看 Mybatis拦截器之数据加密解密开篇拦截器介绍内容,充分理解 Executor/ParameterHandler/ResultSetHandler/StatementHandler 的作用,我们就可以应用拦截器玩出我们自己的花样了
问题彩蛋
我们看到调用拦截器的时候通过 interceptorChain 进行调用,直译过来就是 拦截器链, 其实这是设计模式之责任链模式,那么:
你了解责任链模式吗?
你能想到哪些框架或场景中应用了责任链模式吗?
现实业务中有哪些地方应用责任链模式能让我们代码更灵活健壮呢?
如果我们定义多个同类型的拦截器,比如多个 Executor 类型拦截器,那么多个拦截器的顺序要怎样把控呢?
提高效率工具
关注公众号,回复“工具”获取更多那些可以帮助我们高效工作的工具
Free Mybatis Plugin
我们在使用 Mybatis 并需要手写 SQL 时需要在 Mapper 接口中定义方法,同时在 XML 中定义同名 statementId 的 SQL,该Intellij IDEA 插件帮助我们快速定位方法和 XML,并来回切换
从 Java 到 SQL
推荐阅读:
程序猿为什么要看源码
欢迎留言交流,你有一个思想,我有一个思想, 我们交换后就都有两个思想
tan日拱一兵
转发在看也很赞
喜欢作者