原文連接:"犯罪心理"解讀Mybatis攔截器java
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);
}
}
複製代碼
這時,客戶端調用方式以下:this
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()方法時將攔截邏輯一塊兒做爲參數,這樣客戶端能夠靈活定義本身的攔截邏輯,爲實現此功能,咱們須要定一個攔截器接口 Interceptorspa
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 攔截器的整個設計動機與理念,大道至簡.
關注公衆號瞭解更多能夠提升工做效率的工具,同時帶你像看偵探小說同樣趣味學習 Java 技術