注:本系列源码分析基于mybatis 3.5.6,源码的gitee仓库仓库地址:funcy/mybatis.
mybatis的拦截器为org.apache.ibatis.plugin.Interceptor
,可以拦截一些操作,对应的mybatis文档为mybatis插件
对照着文档,我们先来准备一个demo吧!
1. 准备拦截器demo:打印执行的sql
按照文档内容,我们准备一个拦截器:
// 指定拦截的方法:query 与 update
@Intercepts({
@Signature(type= Executor.class, method = "query",
args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
@Signature(type= Executor.class, method = "update",
args = {MappedStatement.class, Object.class})
})
public class SqlInterceptor implements Interceptor {
private Properties properties;
/**
* 处理拦截操作
*/
@Override
public Object intercept(Invocation invocation) throws Throwable {
Object[] args = invocation.getArgs();
MappedStatement ms = (MappedStatement)args[0];
BoundSql boundSql = ms.getBoundSql(args[1]);
String sql = boundSql.getSql();
System.out.println("执行的sql为:" + sql);
return invocation.proceed();
}
/**
* 设置一些属性
*/
@Override
public void setProperties(Properties properties) {
this.properties = properties;
}
}
复制代码
把拦截器添加到mybatis
配置文件:
<configuration>
<properties resource="org/apache/ibatis/demo/config.properties">
</properties>
<settings>
...
</settings>
<!-- 配置拦截器 -->
<plugins>
<plugin interceptor="org.apache.ibatis.demo.SqlInterceptor">
</plugin>
</plugins>
<!-- 省略其他配置 -->
...
</configuration>
复制代码
运行Test01
,结果如下:
执行的sql为:select id, login_name as loginName, nick from user
where id = ?
limit ?
[User{id=3, loginName='test', nick='HelloWorld'}]
复制代码
可以看到,执行的sql成功打印了。
2. 拦截器装配
2.1 解析
拦截器配置在mybatis配置文件中,因此在解析配置文件的XMLConfigBuilder#parseConfiguration
中也会解析拦截器:
private void parseConfiguration(XNode root) {
try {
...
pluginElement(root.evalNode("plugins"));
...
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}
/**
* 解析拦截器节点
*/
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).getDeclaredConstructor().newInstance();
interceptorInstance.setProperties(properties);
// 添加拦截器
configuration.addInterceptor(interceptorInstance);
}
}
}
复制代码
解析操作比较常规,就不多作分析了我们主要来看configuration.addInterceptor(...)
方法,看看拦截器最终的去向:
public class Configuration {
/**
* 保存拦截器
*/
protected final InterceptorChain interceptorChain = new InterceptorChain();
/**
* 将拦截器添加到 interceptorChain 中
*/
public void addInterceptor(Interceptor interceptor) {
interceptorChain.addInterceptor(interceptor);
}
...
}
复制代码
拦截器最终都保存在了Configuration
类中的成员变量interceptorChain
中了。
这个InterceptorChain
又是个啥呢?我们继续:
public class InterceptorChain {
/**
* 保存拦截器的list
*/
private final List<Interceptor> interceptors = new ArrayList<>();
...
/**
* 添加拦截器
*/
public void addInterceptor(Interceptor interceptor) {
interceptors.add(interceptor);
}
/**
* 获取所有的拦截器
*/
public List<Interceptor> getInterceptors() {
return Collections.unmodifiableList(interceptors);
}
}
复制代码
从代码来看,它内部维护了一个List
类型的成员变量,对外提供了addXxx(...)
与getXxx()
操作方法。
2.2 装配
mybatis的拦截器保存在Configuration
类中的成员变量interceptorChain
中,这些拦截器如何装配到mybatis
的执行链路上的呢?让我们回到Configuration#newExecutor(...)
方法,其中有这么一行:
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
...
// 处理 plugin(插件)
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
复制代码
InterceptorChain#pluginAll
就是用来处理拦截器的加载了:
public Object pluginAll(Object target) {
for (Interceptor interceptor : interceptors) {
// 调用各个拦截器的 plugin(...) 方法
target = interceptor.plugin(target);
}
return target;
}
复制代码
这里传入的target
类型是Executor
,返回的也是Executor
.
继续进入interceptor.plugin()
方法:
public interface Interceptor {
default Object plugin(Object target) {
return Plugin.wrap(target, this);
}
...
}
复制代码
这是接口的默认方法,调用的是Plugin.wrap(...)
,我们继续:
public class Plugin implements InvocationHandler {
/** 目标对象,如:executor */
private final Object target;
/** 拦截器 */
private final Interceptor interceptor;
/** 保存拦截的方法 */
private final Map<Class<?>, Set<Method>> signatureMap;
/**
* 私有的构造方法,在 wrap(...) 方法中调用
*/
private Plugin(Object target, Interceptor interceptor,
Map<Class<?>, Set<Method>> signatureMap) {
this.target = target;
this.interceptor = interceptor;
this.signatureMap = signatureMap;
}
/**
* 组装代理对象
*/
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,
// InvocationHandler 实例
new Plugin(target, interceptor, signatureMap));
}
return target;
}
/**
* 解析拦截器类上的标签
* @param interceptor
* @return
*/
private static Map<Class<?>, Set<Method>> getSignatureMap(Interceptor interceptor) {
// 获取拦截器上的 @Intercepts 注解
Intercepts interceptsAnnotation = interceptor.getClass().getAnnotation(Intercepts.class);
// issue #251
if (interceptsAnnotation == null) {
throw new PluginException(...);
}
// 处理 @Intercepts 中的 @Signature 注解
Signature[] sigs = interceptsAnnotation.value();
Map<Class<?>, Set<Method>> signatureMap = new HashMap<>();
for (Signature sig : sigs) {
Set<Method> methods = signatureMap.computeIfAbsent(sig.type(), k -> new HashSet<>());
try {
// 获取拦截的方法
Method method = sig.type().getMethod(sig.method(), sig.args());
methods.add(method);
} catch (NoSuchMethodException e) {
throw new PluginException(...);
}
}
return signatureMap;
}
...
}
复制代码
从代码来看,Plugin.wrap(...)
会使用jdk的动态代理功能生成代理对象,Plugin
实现了InvocationHandler
,它的invoke(...)
方法是拦截的关键,我们后面再分析。
示例中,传入的是原始的executor
,调用executor = (Executor) interceptorChain.pluginAll(executor)
得到的executor
就是动态代理类了:
本文使用的示例是拦截Executor
的query
与update
方法,实际上还有其他方法可拦截,mybatis拦截器支持的方法如下:
Executor
(update
,query
,flushStatements
,commit
,rollback
,getTransaction
,close
,isClosed
)ParameterHandler
(getParameterObject
,setParameters
)ResultSetHandler
(handleResultSets
,handleOutputParameters
)StatementHandler
(prepare
,parameterize
,batch
,update
,query
)
这些方法的拦截配置同Executor
相差不大,就不多作分析了。
3. 执行
在上一节的分析中,我们得到的是executor
的动态代理对象,那么它是何时被执行的呢?
实际上,被动态代理的对象,执行方法时,都会调用其InvocationHandler
实例的invoke(...)
方法,executor
的动态代理对象的InvocationHandler
为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);
}
}
复制代码
在Plugin#invoke(...)
方法中,先会判断方法要不要拦截,对于需要拦截的方法,调用其interceptor.intercept(...)
方法,否则就直接调用该方法。
在interceptor.intercept(...)
的执行中,传入的参数是Invocation
,我们来看看它是个啥:
public class Invocation {
private final Object target;
private final Method method;
private final Object[] args;
public Invocation(Object target, Method method, Object[] args) {
this.target = target;
this.method = method;
this.args = args;
}
public Object getTarget() {
return target;
}
public Method getMethod() {
return method;
}
public Object[] getArgs() {
return args;
}
/**
* 执行目标方法
*/
public Object proceed() throws InvocationTargetException, IllegalAccessException {
return method.invoke(target, args);
}
}
复制代码
这个类主要是保存了目标对象
、目标方法
以及传入目标方法的参数
,在重写Interceptor#intercept(...)
方法时,我们就可以根据这些内容完成一系列操作。
在Invocation
还有一个重要的方法:proceed()
,这个方法会执行目标方法的逻辑,像我们在实现SqlInterceptor
时,是这要使用的:
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 从 invocation 中获取参数
Object[] args = invocation.getArgs();
MappedStatement ms = (MappedStatement)args[0];
BoundSql boundSql = ms.getBoundSql(args[1]);
String sql = boundSql.getSql();
System.out.println("执行的sql为:" + sql);
// 处理完拦截逻辑后,要执行目标方法
return invocation.proceed();
}
复制代码
处理完拦截操作后,不要忘了调用invocation.proceed()
来执行原始逻辑。
4. 总结
本文是分析了mybatis
拦截器机制,分析了解析、装配、执行的流程,说到底,拦截器还是使用了jdk提供的动态代理功能。
关于mybatis
拦截器相关分析就到这里了。
本文原文链接:my.oschina.net/funcy/blog/… ,限于作者个人水平,文中难免有错误之处,欢迎指正!原创不易,商业转载请联系作者获得授权,非商业转载请注明出处。