注:本系列源碼分析基於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
攔截器相關分析就到這裏了。
本文原文鏈接:https://my.oschina.net/funcy/blog/4952678 ,限於作者個人水平,文中難免有錯誤之處,歡迎指正!原創不易,商業轉載請聯繫作者獲得授權,非商業轉載請註明出處。