"犯罪心理"解讀Mybatis攔截器

tan日拱一兵發表於2019-06-27

原文連結:"犯罪心理"解讀Mybatis攔截器

Mybatis攔截器執行過程解析 文章寫過之後,我覺得 “Mybatis 攔截器案件”背後一定還隱藏著某種設計動機,裡面大量的使用了 Java 動態代理手段,它是怎樣應用這個手段優雅的設計出整個攔截事件的?就像抓到罪犯要了解它犯罪動機是什麼一樣,我們需要解讀 Mybatis攔截器的設計理念:
police-2167968_640.jpg

設計解讀

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 實現的核心內容做個對比:
生成代理物件:
Xnip2019-06-24_21-41-09.jpg

攔截指定方法,如果找不到方法,丟擲異常:
Xnip2019-06-24_21-44-02.jpg

執行目標方法:
Xnip2019-06-24_21-46-30.jpg

到這裡,沒錯,犯罪現場完美推測出,真相就是這樣!!!
牆裂建議先看 Mybatis攔截器執行過程解析 ,然後回看該文章,瞭解 Mybatis 攔截器的整個設計動機與理念,大道至簡.

靈魂追問

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

推薦閱讀

  1. 不得不知的責任鏈設計模式
  2. Mybatis攔截器執行過程解析
  3. 如何使用Mybatis的攔截器實現資料加密與解密
  4. 如何設計好的RESTful API
  5. 輕鬆高效玩轉DTO(Data Transfer Object)

那些可以提高效率的工具

關注公眾號瞭解更多可以提高工作效率的工具,同時帶你像看偵探小說一樣趣味學習 Java 技術

a


a (1).png

相關文章