“犯罪心理”解讀 Mybatis 攔截器

發表於2019-09-02

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 攔截器的整個設計動機與理念,大道至簡.

靈魂追問

  1. 除了迪米特設計原則,你還知道哪些設計基本原則?
  2. 你在編寫程式碼時,考慮過怎樣利用那些設計原則來規範自己程式碼嗎?
  3. ...

提高效率工具


推薦閱讀


歡迎持續關注公眾號:「日拱一兵」

  • 前沿 Java 技術乾貨分享
  • 高效工具彙總 | 回覆「工具」
  • 面試問題分析與解答
  • 技術資料領取 | 回覆「資料」

以讀偵探小說思維輕鬆趣味學習 Java 技術棧相關知識,本著將複雜問題簡單化,抽象問題具體化和圖形化原則逐步分解技術問題,技術持續更新,請持續關注......

相關文章