原文連結:"犯罪心理"解讀Mybatis攔截器
Mybatis攔截器執行過程解析 文章寫過之後,我覺得 “Mybatis 攔截器案件”背後一定還隱藏著某種設計動機,裡面大量的使用了 Java 動態代理手段,它是怎樣應用這個手段優雅的設計出整個攔截事件的?就像抓到罪犯要了解它犯罪動機是什麼一樣,我們需要解讀 Mybatis攔截器的設計理念:
設計解讀
Java 動態代理我們都懂得,我們先用它設計一個基本攔截器
首先定義目標物件介面:
public interface Target {
public void execute();
}
然後,定義實現類實現其介面:
public class TargetImpl implements Target {
public void execute() {
System.out.println("Execute");
}
}
最後,使用 JDK 動態代理定義一個代理類,用於為目標類生成代理物件:
public class TargetProxy implements InvocationHandler {
private Object target;
private TargetProxy(Object target) {
this.target = target;
}
//代理物件生成目標物件
public static Object bind(Object target) {
return Proxy.newProxyInstance(target.getClass() .getClassLoader(),
target.getClass().getInterfaces(),
new TargetProxy(target));
}
//
public Object invoke(Object proxy, Method method,
Object[] args) throws Throwable {
System.out.println("Begin");
return method.invoke(target, args);
}
}
這時,客戶端呼叫方式如下:
public class Client {
public static void main(String[] args) {
//沒被代理之前
Target target = new TargetImpl();
target.execute();
//執行結果:
//Execute
//被代理之後
target = (Target)TargetProxy.bind(target);
target.execute();
//執行結果:
//Begin
//Execute
}
}
應用上面的設計方式,攔截邏輯是寫死在代理類中的:
public Object invoke(Object proxy, Method method,
Object[] args) throws Throwable {
//攔截邏輯在代理物件中寫死了,這樣到這客戶端沒有靈活的設定來攔截其邏輯
System.out.println("Begin");
return method.invoke(target, args);
}
這樣的設計方式不夠靈活和高可用,可能滿足 ClientA 的攔截需求,但是不能滿足 ClientB 的攔截需求,這不是一個好的攔截方案,所以我們需要進一步更改設計方案:
將攔截邏輯封裝成一個類,客戶端繫結在呼叫TargetProxy()方法時將攔截邏輯一起作為引數,這樣客戶端可以靈活定義自己的攔截邏輯,為實現此功能,我們需要定一個攔截器介面 Interceptor
public interface Interceptor {
public void intercept();
}
將代理類做一個小改動,在客戶端例項化 TargetProxy 的時候可以傳入自定義的攔截器:
public class TargetProxy implements InvocationHandler {
private Object target;
//攔截器
private Interceptor interceptor;
private TargetProxy(Object target, Interceptor interceptor) {
this.target = target;
this.interceptor = interceptor;
}
//通過傳入客戶端封裝好 interceptor 的方式為 target 生成代理物件,使得客戶端可以靈活使用不同的攔截器邏輯
public static Object bind(Object target, Interceptor interceptor) {
return Proxy.newProxyInstance(target.getClass().getClassLoader(),
target.getClass().getInterfaces(),
new TargetProxy(target, interceptor));
}
public Object invoke(Object proxy, Method method,
Object[] args) throws Throwable {
//客戶端實現自定義的攔截邏輯
interceptor.intercept();
return method.invoke(target, args);
}
}
通過這樣,就解決了“攔截內容固定死”的問題了,再來看客戶端的呼叫方式:
//客戶端可以在此處定義多種攔截邏輯
Interceptor interceptor = new Interceptor() {
public void intercept() {
System.out.println("Go Go Go!!!");
}
};
target = (Target)TargetProxy.bind(target, interceptor);
target.execute();
上面的 interceptor()
是個無參方法,難道犯罪分子冒著生命危險攔截目標只為聽目標說一句話 System.out.println(“Go Go Go!!!”)
? 很顯然它需要了解目標行為(Method)和注意目標的身外之物(方法引數),繼續設定"圈套",將攔截介面做個改善:
public interface Interceptor {
public void intercept(Method method, Object[] args);
}
同樣需要改變代理類中攔截器的呼叫方式,將 method 和 args 作為引數傳遞進去
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//攔截器拿到method和args資訊可以做更多事情,而不只是打招呼了
interceptor.intercept(method, args);
return method.invoke(target, args);
}
進行到這裡,方案看似已經不錯了,靜待客戶上鉤,但這違背了做一名有追求罪犯的基本原則:「迪米特法則」
迪米特法則(Law of Demeter)又叫作最少知識原則(Least Knowledge Principle 簡寫LKP),就是說一個物件應當對其他物件有儘可能少的瞭解, 不和陌生人說話。英文簡寫為: LoD,是一種解耦的方式.
上面程式碼中,method 需要知道 target 和 args
;interceptor 需要知道 method 和 args
,這樣就可以在 interceptor 中呼叫 method.invoke,但是攔截器中並沒有 invoke 方法需要的關鍵引數 target,所以我們將 target,method,args
再進行一次封裝成 Invocation
類,這樣攔截器只需要關注 Invocation
即可.
public class Invocation {
private Object target;
private Method method;
private Object[] args;
public Invocation(Object target, Method method, Object[] args) {
this.target = target;
this.method = method;
this.args = args;
}
//成員變數儘可能在自己的內部操作,而不是 Intereptor 獲取自己的成員變數來操作他們
public Object proceed() throws InvocationTargetException, IllegalAccessException {
return method.invoke(target, args);
}
public Object getTarget() {
return target;
}
public void setTarget(Object target) {
this.target = target;
}
public Method getMethod() {
return method;
}
public void setMethod(Method method) {
this.method = method;
}
public Object[] getArgs() {
return args;
}
public void setArgs(Object[] args) {
this.args = args;
}
}
這樣攔截器介面變了樣子:
public interface Interceptor {
public Object intercept(Invocation invocation)throws Throwable ;
}
代理類也隨之做了改變:
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
return interceptor.intercept(new Invocation(target, method, args));
}
這樣客戶端呼叫,在攔截器中,攔截器寫了自己攔截邏輯之後,執行 invocation.proceed()
即可觸發原本 target 的方法執行:
Interceptor interceptor = new Interceptor() {
public Object intercept(Invocation invocation) throws Throwable {
System.out.println("Go Go Go!!!");
return invocation.proceed();
}
};
到這裡,我們經過一系列的調整和設計,結果已經很好了,但仔細想,這種攔截方式會攔截 target 的所有方法,假如 Target 介面有多個方法:
public interface Target {
/**
* 去銀行存款
*/
public void execute1();
/**
* 倒垃圾
*/
public void execute2();
}
以上兩個方法,當然是攔截 target 去銀行存款才是利益價值最大化的攔截,攔截 target 去倒垃圾有什麼用呢?(避免沒必要的攔截開銷),所以我們標記攔截器只有在發生去銀行存款的行為時才採取行動,先自定義一個註解用來標記攔截器
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface MethodName {
public String value();
}
在攔截器實現類上新增該標識:
//去銀行存款時攔截
@MethodName("execute1")
public class InterceptorImpl implements Interceptor {
...
}
修改代理類,如果註解標記的方法是否與 method 的方法一致,則執行攔截器:
public Object invoke(Object proxy, Method method,
Object[] args) throws Throwable {
MethodName methodName = this.interceptor.getClass().getAnnotation(MethodName.class);
if (ObjectUtils.isNull(methodName)){
throw new NullPointerException("xxxx");
}
//如果方法名稱和註解標記的方法名稱相同,則攔截
String name = methodName.value();
if (name.equals(method.getName())){
return interceptor.intercept(new Invocation(target, method, args));
}
return method.invoke(this.target, args);
}
到這裡,戶端的呼叫變成了這個樣子:
Target target = new TargetImpl();
Interceptor interceptor = new InterceptorImpl();
target = (Target)TargetProxy.bind(target, interceptor);
target.execute();
從上面可以看出,客戶端第一步建立 target 物件和 interceptor 物件,通過傳入 target 和 interceptor 呼叫 bind 方法生成代理物件,最終代理物件呼叫 execute 方法,根據迪米特法則,客戶端不需要了解 TargetProxy,只需要關注攔截器的內部邏輯和可呼叫的方法即可,所以我們需要繼續修改設計方案,新增 register(Object object)
方法,:
public interface Interceptor {
public Object intercept(Invocation invocation) throws Throwable ;
public Object register(Object target);
}
修改攔截器的實現,攔截器物件通過呼叫 register 方法為 target 生成代理物件:
@MethodName("execute1")
public class InterceptorImpl implements Interceptor {
public Object intercept(Invocation invocation)throws Throwable {
System.out.println("Go Go Go!!!");
return invocation.proceed();
}
public Object register(Object target) {
return TargetProxy.bind(target, this);
}
}
現在,客戶端呼叫變成了這個樣子:
Target target = new TargetImpl();
Interceptor interceptor = new InterceptorImpl();
target = (Target)interceptor.register(target);
target.execute1();
客戶端只需要例項化攔截器物件,並呼叫攔截器相應的方法即可,非常清晰明朗
一系列的設計改變,恰巧符合 Mybatis攔截器的設計思想,我們只不過用一個非常簡單的方式去理解它
Mybatis 將自定義的攔截器配置新增到 XML 檔案中,或者通過註解的方式新增到上下文中,以 XML 形式舉例:
<plugins>
<plugin interceptor="com.gs.cvoud.dao.interceptor.MapInterceptor" />
</plugins>
通過讀取配置檔案,將所有攔截器都新增到 InterceptorChain 中
public class InterceptorChain {
private final List<Interceptor> interceptors = new ArrayList<Interceptor>();
public Object pluginAll(Object target) {
for (Interceptor interceptor : interceptors) {
// 該方法和我們上面自定義攔截器中 register 方法功能一致
target = interceptor.plugin(target);
}
return target;
}
public void addInterceptor(Interceptor interceptor) {
interceptors.add(interceptor);
}
public List<Interceptor> getInterceptors() {
return Collections.unmodifiableList(interceptors);
}
}
但 Mybatis 框架邏輯限制,只能為:ParameterHandler,ResultSetHandler,StatementHandler 和 Executor 建立代理物件
我們在此將我們的簡單實現與 Mybatis 實現的核心內容做個對比:
生成代理物件:
攔截指定方法,如果找不到方法,丟擲異常:
執行目標方法:
到這裡,沒錯,犯罪現場完美推測出,真相就是這樣!!!
牆裂建議先看 Mybatis攔截器執行過程解析 ,然後回看該文章,瞭解 Mybatis 攔截器的整個設計動機與理念,大道至簡.
靈魂追問
- 除了迪米特設計原則,你還知道哪些設計基本原則?
- 你在編寫程式碼時,考慮過怎樣利用那些設計原則來規範自己程式碼嗎?
- ...
推薦閱讀
- 不得不知的責任鏈設計模式
- Mybatis攔截器執行過程解析
- 如何使用Mybatis的攔截器實現資料加密與解密
- 如何設計好的RESTful API
- 輕鬆高效玩轉DTO(Data Transfer Object)
那些可以提高效率的工具
關注公眾號瞭解更多可以提高工作效率的工具,同時帶你像看偵探小說一樣趣味學習 Java 技術