Mybatis攔截器原始碼深度解析
目錄:
一. 建立攔截器鏈1. 建立物件
2. 建立配置檔案
3. 載入攔截器鏈
二. 方法呼叫解析
1. 對請求物件進行攔截器包裝
2. 執行呼叫
三. 小結
Mybatis攔截器 可以幫助我們在執行sql語句過程中增加外掛以實現一些通用的邏輯,比如對查詢sql分頁、資料許可權處理等。
允許使用外掛攔截的方法呼叫包括:
- Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed) - ParameterHandler (getParameterObject, setParameters) - ResultSetHandler (handleResultSets, handleOutputParameters) - StatementHandler (prepare, parameterize, batch, update, query)
各方法的具體作用可以通過 Mybatis之sqlSession呼叫鏈分析 進行了解。
方法呼叫時載入攔截器鏈的總體時序圖如下:
一. 建立攔截器鏈
1. 建立物件
建立攔截器物件,對Executor介面的實現類上的update方法呼叫進行攔截:
// ExamplePlugin.java @Intercepts({@Signature( type= Executor.class, method = "update", args = {MappedStatement.class,Object.class})}) public class ExamplePlugin implements Interceptor { public Object intercept(Invocation invocation) throws Throwable { return invocation.proceed(); } public Object plugin(Object target) { return Plugin.wrap(target, this); } public void setProperties(Properties properties) { } }
Mybaits中通過註解Intercepts來設定當前攔截器是否對被攔截的請求進行處理,例子中的註解指定對Executor的所有update方法進行攔截處理。
- Intercepts
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Intercepts {
Signature[] value();
}
- Signature
@Documented @Retention(RetentionPolicy.RUNTIME) @Target({}) public @interface Signature { Class<?> type(); String method(); Class<?>[] args(); }
對註解的解析在Plugin類中。
2. 建立配置檔案
可以通過XMl和註解兩種方式進行配置,通過解析配置建立攔截器鏈。
- xml配置:
<!-- mybatis-config.xml -->
<plugins>
<plugin interceptor="org.mybatis.example.ExamplePlugin">
<property name="someProperty" value="100"/>
</plugin>
</plugins>
- 註解配置:
@Bean
public PaginationInterceptor paginationInterceptor() {
return new PaginationInterceptor();
}
3. 載入攔截器鏈
以XML配置好攔截器好,從XML解析原始碼中,可以看到通過xml標籤屬性 interceptor 把攔截器加入 Configuration的 InterceptorChain中,
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);
}
}
}
二. 方法呼叫解析
之前我們配置了一個對Executor的update方法進行攔截的外掛,那麼看下具體的執行過程。在呼叫Configuration生成執行物件時,通過攔截器鏈對物件進行包裝
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進行包裝,因為使用了JDK的動態代理,所以返回物件必須為介面
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
1. 對請求物件進行攔截器包裝
通過InterceptorChain的pluginAll方法,對interceptors集合迴圈,依次對target(也就是上面傳入的executor物件)進行代理:
public Object pluginAll(Object target) {
for (Interceptor interceptor : interceptors) {
// target通過攔截器鏈,迴圈迭代代理
target = interceptor.plugin(target);
}
return target;
}
自定義的攔截器需要實現Interceptor實現的plugin方法,該方法用來給target增加代理,推薦直接呼叫Plugin.wrap(target, this)方法(這裡將每一個代理物件的建立放在Plugin類中的靜態方法,但是每新增一個外掛都需要寫這個方法)
@Override
public Object plugin(Object target) {
// 注意第二個引數 為this,通過回撥自己,將引數傳遞給Plugin物件,plugin代理物件執行時,如果符合條件,將回調target的intercept,參見本文2小節
return Plugin.wrap(target, this);
}
Plugin實現了InvocationHandler介面,其類圖如下:
Plugin的wrap方法用來建立target的代理物件:
// 引數interceptor為 ExamplePlugin類物件
public static Object wrap(Object target, Interceptor interceptor) {
// 1. 獲取註解中的配置,class對應Signature中的type屬性,Set<Method>對應Signature中的method屬性
Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
// 2. 獲取被代理物件的class類物件,這裡為Executor的介面實現類
Class<?> type = target.getClass();
// 3. 獲取符合物件target介面的攔截器(這裡為Executor.class)
Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
// 如果註解包含目標物件介面,對Plugin物件進行代理,返回的Plugin代理物件,呼叫目標方法時會進行Pluing的invoke()方法
if (interfaces.length > 0) {
return Proxy.newProxyInstance(
type.getClassLoader(),
interfaces,
// 代理物件傳入引數包括 目標類、該攔截器物件、註解配置解析Map
new Plugin(target, interceptor, signatureMap));
}
// 如果該外掛註解中TYPE值不包含目標物件介面,則不處理,直接返回目標物件
return target;
}
其中getAllInterFaces方法用來判斷判斷註解中是否包含目標物件介面:
private static Class<?>[] getAllInterfaces(Class<?> type, Map<Class<?>, Set<Method>> signatureMap) {
Set<Class<?>> interfaces = new HashSet<>();
while (type != null) {
// 如果傳入Executor的實現類,那麼這裡為Executor介面
for (Class<?> c : type.getInterfaces()) {
// 1. 判斷註解中是否包含目標物件介面
if (signatureMap.containsKey(c)) {
interfaces.add(c);
}
}
// 2. 如果繼承父介面,繼續迴圈判斷
type = type.getSuperclass();
}
// 返回該外掛註解與被代理物件匹配的所有的介面類物件陣列
return interfaces.toArray(new Class<?>[interfaces.size()]);
}
2. 執行呼叫
當呼叫被代理物件Execute的所有方法,都會進入Plugin的invoke方法:
// 代理類只用來做流程判斷,不增加具體的業務邏輯,業務邏輯統一在實現Invocation介面的外掛類中增加
// 對該方法進行遞迴呼叫(包括所有已載入進攔截器鏈中的攔截器,首先判斷當前攔截器中的註解條件是否滿足,滿足的話執行當前攔截器,否則呼叫target的方法,進入下一個攔截器)
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
Set<Method> methods = signatureMap.get(method.getDeclaringClass());
// 判斷Signature引數method是否包含當前呼叫方法,如果包含,進入攔截器intercept方法;否則,跳過該攔截器繼續執行
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);
}
}
自定義攔截器中intercept實現如下:
public Object intercept(Invocation invocation) throws Throwable {
// TODO 增加自己的業務需求
return invocation.proceed();
}
Invocation相當於責任鏈中的請求類,其封裝反射引數,包含target(target通過InterceptorChain的pluginAll迴圈代理,因此可以是攔截器的代理類,當經過最後一個攔截器時,為實際呼叫物件)、method、args,該類主要用來減少intercept方法呼叫時傳入的引數數量
其中proceed方法如下:
public Object proceed() throws InvocationTargetException, IllegalAccessException {
// 通過反射執行target的方法
// 如果target依舊為Plugin代理類,則繼續進行代理類Plugin中的invoke方法中
return method.invoke(target, args);
}
三. 小結
Mybatis攔截器主要實現以下幾個點:
- 通過註解判斷被攔截的請求是否符合當前的攔截器。
- 支援橫向擴充套件,可以自定義攔截器並加入到攔截器鏈中。
支援請求在攔截器鏈中依次傳遞(Invocation類)。
其結合責任鏈模式使請求和處理解耦,但是每一次請求都要通過責任鏈上的所有攔截器,也就是一次呼叫需要所有攔截器進行判斷,因此也有一些侷限性。
相關文章:
一文讀懂JDBC
mybaits動態代理之最小demo實現
Mybatis之sqlSession呼叫鏈分析
瞭解更多請關注微信公眾號: