搞懂MyBatis攔截器的工作原理
在日常開(kāi)發(fā)過(guò)程中,我們經(jīng)常會(huì)碰到這樣一種場(chǎng)景,在對(duì)某一個(gè)請(qǐng)求的處理過(guò)程中添加一定的特殊邏輯,但又不想對(duì)整個(gè)處理流程中的其他步驟造成影響。比如,我們希望在兩個(gè)業(yè)務(wù)操作之間嵌入一個(gè)安全控制機(jī)制。
請(qǐng)求處理流程中嵌入定制化操作的示意圖
顯然,想要實(shí)現(xiàn)這種效果的方式有很多種。今天,我們就來(lái)介紹一種非常實(shí)用的實(shí)現(xiàn)方法,也就是攔截器(Interceptor)?,F(xiàn)在,讓我們從攔截器的設(shè)計(jì)思想開(kāi)始講起。
攔截器的設(shè)計(jì)思想
對(duì)于攔截過(guò)程而言,我們首先要明確的是它的攔截點(diǎn)。在攔截器運(yùn)行過(guò)程中,攔截點(diǎn)表示應(yīng)用執(zhí)行過(guò)程中能夠插入攔截器的一個(gè)點(diǎn)。這種攔截點(diǎn)可以是普通的方法調(diào)用、類初始化或?qū)ο髮?shí)例化,也可以是針對(duì)異常的處理。
一旦捕獲了攔截點(diǎn),我們就可以通過(guò)反射機(jī)制獲取這個(gè)攔截點(diǎn)對(duì)應(yīng)的執(zhí)行方法、輸入?yún)?shù)、目標(biāo)返回值等元數(shù)據(jù),從而根據(jù)這些元數(shù)據(jù)來(lái)實(shí)現(xiàn)一系列自定義攔截操作。
攔截點(diǎn)結(jié)構(gòu)圖
最后,將攔截點(diǎn)和攔截操作結(jié)合在一起就構(gòu)成了攔截器。本質(zhì)上,攔截器用于定義應(yīng)用程序中的業(yè)務(wù)邏輯及其執(zhí)行的位置。我們可以通過(guò)一張圖來(lái)展示攔截器的組成結(jié)構(gòu)。
攔截器組成結(jié)構(gòu)示意圖
攔截器的設(shè)計(jì)思想非常通用,所以它在各大主流開(kāi)源框架中的應(yīng)用也非常廣泛。今天,我們就以常見(jiàn)的 ORM 框架——MyBatis 為例,詳細(xì)分析一下 ORM 框架中所具備的攔截器的工作原理。
MyBatis 中的攔截器工作原理
MyBatis 中內(nèi)置了一組常用的攔截器,而開(kāi)發(fā)人員也可以通過(guò) Plugin 配置項(xiàng),來(lái)嵌入各種定制化的攔截器。我們先來(lái)看一下在 MyBatis 中使用攔截器的方式。通常,就是在配置文件中添加類似如下所示的配置項(xiàng)??梢钥吹剑?Plugin 配置段中可以添加一個(gè)自定義的 interceptor 配置項(xiàng),并設(shè)置對(duì)應(yīng)的屬性。
<plugins>
<plugin interceptor="com.xiaoyiran.Mybatis.interceptor.MyInterceptor”>
<proper ty name=”prop1″ value="prop1″/>
<property name="prop2" value="prop2"/>
</plugin>
</plugins>MyBatis 中的 Configuration 類會(huì)根據(jù)配置的攔截器屬性,實(shí)例化 Interceptor 對(duì)象,并添加到 MyBatis 的運(yùn)行上下文中。我們跟蹤代碼,發(fā)現(xiàn) Configuration 中定義了一個(gè) InterceptorChain 對(duì)象。顯然,所有的 Interceptor 實(shí)例最終會(huì)被添加到這個(gè) InterceptorChain 中。
protected final InterceptorChain interceptorChain = new InterceptorChain();這樣,MyBatis 中,代表攔截器的 Interceptor 和代表攔截器鏈的 InterceptorChain 的這兩個(gè)核心對(duì)象都出現(xiàn)了。其中 Interceptor 是個(gè)接口,InterceptorChain 是個(gè)實(shí)體類,它們的代碼看上去都不多。讓我們先來(lái)看一下 InterceptorChain 類:
public class InterceptorChain {
privatefinal List<Interceptor> 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<Interceptor> getInterceptors() {
return Collections.unmodifiableList(interceptors);
}
}這段代碼中,InterceptorChain 提供了 addInterceptor 方法,用于將攔截器添加到鏈中。這個(gè)類持有一個(gè) interceptors 數(shù)組,用于把新加入的 Interceptor 保存起來(lái)。通過(guò)這種實(shí)現(xiàn)方式,在 pluginAll 方法中,我們就可以直接遍歷 interceptors 數(shù)組,并利用每個(gè) interceptor 執(zhí)行攔截邏輯。
這里我們不明確的就是 interceptor.plugin(target) 方法的邏輯,讓我們把思路回到 Interceptor 接口。
public interface Interceptor {
Object intercept(Invocation invocation) throws Throwable;
default Object plugin(Object target) {
return Plugin.wrap(target, this);
}
default void setProperties(Properties properties) {
// NOP
}
}可以看到,Interceptor 接口中的 plugin 方法實(shí)際上存在一個(gè)默認(rèn)實(shí)現(xiàn),這里它通過(guò) Plugin.wrap 方法完成了對(duì)目標(biāo)對(duì)象的攔截。Plugin.wrap 是一個(gè)靜態(tài)方法,實(shí)現(xiàn)過(guò)程如下所示:
public class Plugin implements InvocationHandler {
//省略變量定義和構(gòu)造函數(shù)
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) {
//執(zhí)行動(dòng)態(tài)代理
return Proxy.newProxyInstance(
type.getClassLoader(),
interfaces,
new Plugin(target, interceptor, signatureMap));
}
return target;
}
...
}這里我們看到了熟悉的 InvocationHandler 接口和 Proxy.newProxyInstance 實(shí)現(xiàn)方法,從而明白了,原來(lái)這里用到了 JDK 的動(dòng)態(tài)代理機(jī)制。我們通過(guò) getSignatureMap 方法從攔截器的注解中獲取攔截的類名和方法信息,然后,通過(guò) getAllInterfaces 方法獲取接口。最后,通過(guò)動(dòng)態(tài)代理機(jī)制產(chǎn)生代理。這樣使得只有是 Interceptor 注解的接口實(shí)現(xiàn)類才會(huì)產(chǎn)生代理。
講完 Interceptor 和 InterceptorChain 之后,讓我們?cè)俅位氐?Configuration 類,并找到以下代碼:
public ParameterHandler newParameterHandler(...) {
ParameterHandler parameterHandler = ...;
parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler);
return parameterHandler;
}
public ResultSetHandler newResultSetHandler(...) {
ResultSetHandler resultSetHandler = ...;
resultSetHandler = (ResultSetHandler) interceptorChain.pluginAll(resultSetHandler);
return resultSetHandler;
}
public StatementHandler newStatementHandler(...) {
StatementHandler statementHandler = ...;
statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
return statementHandler;
}
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
Executor executor = ...;
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}講解這段代碼的目的是在說(shuō)明這樣一個(gè)事實(shí):在 MyBatis 中,攔截器只能攔截 ParameterHandler、StatementHandler、ResultSetHandler 和 Executor 這四種類型的接口,這點(diǎn)在 Configuration 類中是通過(guò)以上代碼預(yù)先定義好的。這些就構(gòu)成了 MyBatis 中針對(duì)攔截器的各個(gè)攔截點(diǎn)。如果我們想要實(shí)現(xiàn)自定義攔截器,也只能圍繞上述四種接口添加邏輯。這四個(gè)接口之間的關(guān)系和攔截順序如下圖所示:
MyBatis 中能夠累計(jì)額的四種接口類型及其順序
對(duì)于 SQL 的執(zhí)行過(guò)程而言,這四個(gè)環(huán)節(jié)的攔截機(jī)制基本可以滿足日常的定制化需求了。
自定義 MyBatis 攔截器的實(shí)現(xiàn)方法
雖然 MyBatis 已經(jīng)內(nèi)置了一組強(qiáng)大的攔截器,我們可以基于這組攔截器來(lái)應(yīng)對(duì)常見(jiàn)需求。但針對(duì)某些特定的應(yīng)用場(chǎng)景,有時(shí)候我們就需要自己來(lái)實(shí)現(xiàn)定制化的攔截器。接下來(lái),我們就來(lái)看一下如何在 MyBatis 中自定義一個(gè) Interceptor。
如果想要在 MyBatis 中實(shí)現(xiàn)一個(gè)自定義攔截器,我們要做的事情就是實(shí)現(xiàn)上面介紹的 Interceptor 接口,并在這個(gè)接口上指定相應(yīng)的 Signature 信息。一個(gè)空白的 Interceptor 實(shí)現(xiàn)類模版如下所示:
@Intercepts({@Signature(type = Executor.class, method ="update", args = {MappedStatement.class, Object.class})})
public class MyInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
return invocation.proceed();
}
@Override
public Object plugin(Object target) {
returnPlugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
}
}在上面這個(gè) MyInterceptor 類的 intercept 方法中,我們需要調(diào)用 invocation.proceed() 方法來(lái)完成 InterceptChain 的執(zhí)行流程,而我們可以在這個(gè)方法的前后添加定制化處理過(guò)程。
然后,我們來(lái)考慮一個(gè)攔截器的常見(jiàn)應(yīng)用場(chǎng)景。
在實(shí)現(xiàn)數(shù)據(jù)庫(kù)插入和更新操作時(shí),我們往往需要對(duì)這條記錄的更新時(shí)間進(jìn)行同步更新。我們當(dāng)然可以為每句 SQL 添加相應(yīng)的時(shí)間處理方法,但更好的一種方式是通過(guò)自定義攔截器的方式來(lái)自動(dòng)完成這一步操作。
顯然,這一步操作應(yīng)該是在 Executor 中進(jìn)行完成。根據(jù)上面對(duì) Plugin 類的介紹,我們首先需要明確 Executor 中需要攔截的方法,而這方法就是如下所示的 update 方法:
int update(MappedStatement ms, Object parameter) throws SQLException;明確了 Signature 信息之后,我們就可以來(lái)著手實(shí)現(xiàn)整個(gè)流程了。為了方便起見(jiàn),我們可以提供一個(gè)如下所示的注解,專門用來(lái)標(biāo)識(shí)具體需要進(jìn)行攔截的字段:
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD})
public @interface UpdateTimeStamp {
String value() default "";
}然后,我們創(chuàng)建一個(gè)業(yè)務(wù)領(lǐng)域類,把該注解作用于具體的更新時(shí)間字段上。
public class MyDomain {
//省略其他字段定義
@UpdateTimeStamp
public D ate updateTimeStamp;
}完整的 UpdateTimeStampInterceptor 實(shí)現(xiàn)如下,我們對(duì)關(guān)鍵代碼都添加了注釋:
@Intercepts({@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class})})
public class UpdateTimeStampInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
//獲取 MappedStatement
MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];
//獲取 SqlCommandType
SqlCommandType sqlCommandType = mappedStatement.getSqlCommandType();
//獲取 Parameter
Object parameter = invocation.getArgs()[1];
if (parameter != null) {
Field[] declaredFields = parameter.getClass().getDeclaredFields();
for (Field field : declaredFields) {
//獲取 UpdateTimeStamp 注解
if (field.getAnnotation(UpdateTimeStamp.class) != null) {
//如果是 Insert 或 Update 操作,則更新操作時(shí)間
if (SqlCommandType.INSERT.equals(sqlCommandType) || SqlCommandType.UPDATE.equals(sqlCommandType)) {
field.setAccessible(true);
if (field.get(parameter) == null) {
//設(shè)置參數(shù)值
field.set(parameter, new Date());
}
}
}
}
}
//繼續(xù)執(zhí)行攔截器鏈
return invocation.proceed();
}
...
}上面這個(gè) UpdateTimeStampInterceptor 類的實(shí)現(xiàn)過(guò)程,展示了如何獲取與 Executor 相關(guān)的 Statement、SQL 類型以及所攜帶的參數(shù)。通過(guò)這種方法,我們可以實(shí)現(xiàn)在新增或者刪除數(shù)據(jù)庫(kù)記錄時(shí),動(dòng)態(tài)地添加所需要的字段值。同樣,這種處理方式可以擴(kuò)展到任何我們想要處理的字段和參數(shù)。
總結(jié)
從本質(zhì)上講,MyBatis 中實(shí)現(xiàn)攔截的基本手段是構(gòu)建了一個(gè)攔截器鏈,這和設(shè)計(jì)模式中的責(zé)任鏈模式比較類似。而在底層原理上,攔截操作的實(shí)現(xiàn)還是基于動(dòng)態(tài)代理機(jī)制,通過(guò)獲取對(duì)應(yīng)方法的簽名、輸入的接口和參數(shù)等信息來(lái)生成代理,從而確保我們可以在代理對(duì)象中添加各種自定義的攔截邏輯。
基于 MyBatis 中的攔截器機(jī)制,還針對(duì) Executor 的 update 方法給出了一個(gè)自定義的 Interceptor 實(shí)現(xiàn),用于動(dòng)態(tài)設(shè)置數(shù)據(jù)庫(kù)中某些數(shù)據(jù)項(xiàng)的值。這些做法都可以直接應(yīng)用到日常開(kāi)發(fā)過(guò)程中。



























