【轉】由淺入深分析mybatis通過動態代理實現攔截器(插件)的原理

原文地址:http://zhangbo-peipei-163-com.iteye.com/blog/2033832?utm_source=tuicool&utm_medium=referral

在學習mybatis時看到的這個文檔,講得非常好,copy上來分享下

最近在用mybatis做項目,需要用到mybatis的攔截器功能,就順便把mybatis的攔截器源碼大致的看了一遍,爲了溫故而知新,在此就按照自己的理解由淺入深的理解一下它的設計。 

和大家分享一下,不足和謬誤之處歡迎交流。直接入正題。 
首先,先不管mybatis的源碼是怎麼設計的,先假設一下自己要做一個攔截器應該怎麼做。攔截器的實現都是基於代理的設計模式設計的,簡單的說就是要創造一個目標類的代理類,在代理類中執行目標類的方法並攔截執行攔截器代碼。 
那麼我們就用JDK的動態代理設計一個簡單的攔截器: 

將被攔截的目標接口: 
Java代碼  收藏代碼
  1. public interface Target {  
  2.     public void execute();  
  3. }  

目標接口的一個實現類: 
Java代碼  收藏代碼
  1.     public class TargetImpl implements Target {  
  2.     public void execute() {  
  3.         System.out.println("Execute");  
  4.     }  
  5. }  

利用JDK的動態代理實現攔截器: 
Java代碼  收藏代碼
  1. public class TargetProxy implements InvocationHandler {  
  2.     private Object target;  
  3.     private TargetProxy(Object target) {  
  4.         this.target = target;  
  5.     }  
  6.       
  7.     //生成一個目標對象的代理對象  
  8.     public static Object bind(Object target) {  
  9.         return Proxy.newProxyInstance(target.getClass() .getClassLoader(),   
  10.                 target.getClass().getInterfaces(),  
  11.                        new TargetProxy(target));  
  12.     }  
  13.       
  14.     //在執行目標對象方法前加上自己的攔截邏輯  
  15.     public Object invoke(Object proxy, Method method,  
  16.                              Object[] args) throws Throwable {  
  17.         System.out.println("Begin");  
  18.         return method.invoke(target, args);  
  19.     }  
  20. }  

客戶端調用: 
Java代碼  收藏代碼
  1. public class Client {  
  2. public static void main(String[] args) {  
  3.   
  4.     //沒有被攔截之前  
  5.     Target target = new TargetImpl();  
  6.     target.execute(); //Execute  
  7.       
  8.     //攔截後  
  9.     target = (Target)TargetProxy.bind(target);  
  10.     target.execute();   
  11.     //Begin  
  12.     //Execute  
  13. }  

上面的設計有幾個非常明顯的不足,首先說第一個,攔截邏輯被寫死在代理對象中: 
Java代碼  收藏代碼
  1. public Object invoke(Object proxy, Method method,  
  2.                            Object[] args) throws Throwable {  
  3.         //攔截邏輯被寫死在代理對象中,導致客戶端無法靈活的設置自己的攔截邏輯  
  4.         System.out.println("Begin");  
  5.        return method.invoke(target, args);  
  6.     }  

我們可以將攔截邏輯封裝到一個類中,客戶端在調用TargetProxy的bind()方法的時候將攔截邏輯一起當成參數傳入: 
定義一個攔截邏輯封裝的接口Interceptor,這纔是真正的攔截器接口。 
Java代碼  收藏代碼
  1.     public interface Interceptor {  
  2.     public void intercept();  
  3. }  

那麼我們的代理類就可以改成: 
Java代碼  收藏代碼
  1. public class TargetProxy implements InvocationHandler {  
  2.   
  3. private Object target;  
  4. private Interceptor interceptor;  
  5.   
  6. private TargetProxy(Object target, Interceptor interceptor) {  
  7.     this.target = target;  
  8.     this.interceptor = interceptor;  
  9. }  
  10.   
  11. //將攔截邏輯封裝到攔截器中,有客戶端生成目標類的代理類的時候一起傳入,這樣客戶端就可以設置不同的攔截邏輯。  
  12. public static Object bind(Object target, Interceptor interceptor) {  
  13.     return Proxy.newProxyInstance(target.getClass().getClassLoader(),   
  14.                        target.getClass().getInterfaces(),  
  15.                        new TargetProxy(target, interceptor));  
  16. }  
  17.   
  18. public Object invoke(Object proxy, Method method,   
  19.                       Object[] args) throws Throwable {  
  20.     //執行客戶端定義的攔截邏輯  
  21.     interceptor.intercept();  
  22.     return method.invoke(target, args);  
  23. }  

客戶端調用代碼: 
Java代碼  收藏代碼
  1. //客戶端可以定義各種攔截邏輯  
  2. Interceptor interceptor = new Interceptor() {  
  3.     public void intercept() {  
  4.         System.out.println("Go Go Go!!!");  
  5.     }  
  6. };  
  7. target = (Target)TargetProxy.bind(target, interceptor);  
  8. target.execute();  

當然,很多時候我們的攔截器中需要判斷當前方法需不需要攔截,或者獲取當前被攔截的方法參數等。我們可以將被攔截的目標方法對象,參數信息傳給攔截器。 
攔截器接口改成: 
Java代碼  收藏代碼
  1. public interface Interceptor {  
  2.     public void intercept(Method method, Object[] args);  
  3. }  

在代理類執行的時候可以將當前方法和參數傳給攔截,即TargetProxy的invoke方法改爲: 
Java代碼  收藏代碼
  1. public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {  
  2.     interceptor.intercept(method, args);  
  3.     return method.invoke(target, args);  
  4. }  

在Java設計原則中有一個叫做迪米特法則,大概的意思就是一個類對其他類知道得越少越好。其實就是減少類與類之間的耦合強度。這是從類成員的角度去思考的。 
什麼叫越少越好,什麼是最少?最少就是不知道。 
所以我們是不是可以這麼理解,一個類所要了解的類應該越少越好呢? 
當然,這只是從類的角度去詮釋了迪米特法則。 
甚至可以反過來思考,一個類被其他類瞭解得越少越好。 
A類只讓B類瞭解總要強於A類讓B,C,D類都去了解。 

舉個例子: 
我們的TargetProxy類中需要了解的類有哪些呢? 
1. Object target 不需要了解,因爲在TargetProxy中,target都被作爲參數傳給了別的類使用,自己不需要了解它。 
2. Interceptor interceptor 需要了解,需要調用其intercept方法。 
3. 同樣,Proxy需要了解。 
4. Method method 參數需要了解,需要調用其invoke方法。 
同樣,如果interceptor接口中需要使用intercept方法傳過去Method類,那麼也需要了解它。那麼既然Interceptor都需要使用Method,還不如將Method的執行也放到Interceptor中,不再讓TargetProxy類對其瞭解。Method的執行需要target對象,所以也需要將target對象給Interceptor。將Method,target和args封裝到一個對象Invocation中,將Invocation傳給Interceptor。 
Invocation: 
Java代碼  收藏代碼
  1. public class Invocation {  
  2.     private Object target;  
  3.     private Method method;  
  4.     private Object[] args;  
  5.       
  6.     public Invocation(Object target, Method method, Object[] args) {  
  7.         this.target = target;  
  8.         this.method = method;  
  9.         this.args = args;  
  10.     }  
  11.       
  12.     //將自己成員變量的操作儘量放到自己內部,不需要Interceptor獲得自己的成員變量再去操作它們,  
  13.     //除非這樣的操作需要Interceptor的其他支持。然而這兒不需要。  
  14.     public Object proceed() throws InvocationTargetException, IllegalAccessException {  
  15.         return method.invoke(target, args);  
  16.     }  
  17.         
  18.     public Object getTarget() {  
  19.         return target;  
  20.     }  
  21.     public void setTarget(Object target) {  
  22.         this.target = target;  
  23.     }  
  24.     public Method getMethod() {  
  25.         return method;  
  26.     }  
  27.     public void setMethod(Method method) {  
  28.         this.method = method;  
  29.     }  
  30.     public Object[] getArgs() {  
  31.         return args;  
  32.     }  
  33.     public void setArgs(Object[] args) {  
  34.         this.args = args;  
  35.     }  
  36. }  

Interceptor就變成: 
Java代碼  收藏代碼
  1. public interface Interceptor {  
  2.     public Object intercept(Invocation invocation)throws Throwable ;  
  3. }  

TargetProxy的invoke方法就變成: 
Java代碼  收藏代碼
  1. public Object invoke(Object proxy, Method method,   
  2.                           Object[] args) throws Throwable {  
  3.     return interceptor.intercept(new Invocation(target,   
  4.                                                    method, args));  
  5. }  

那麼就每一個Interceptor攔截器實現都需要最後執行Invocation的proceed方法並返回。 
客戶端調用: 
Java代碼  收藏代碼
  1. Interceptor interceptor = new Interceptor() {  
  2.     public Object intercept(Invocation invocation)  throws Throwable {  
  3.         System.out.println("Go Go Go!!!");  
  4.         return invocation.proceed();  
  5.     }  
  6. };  

好了,通過一系列調整,設計已經挺好了,不過上面的攔截器還是有一個很大的不足, 
那就是攔截器會攔截目標對象的所有方法,然而這往往是不需要的,我們經常需要攔截器 
攔截目標對象的指定方法。 
假設目標對象接口有多個方法: 
Java代碼  收藏代碼
  1. public interface Target {  
  2.     public void execute1();  
  3.     public void execute2();  
  4. }  

利用在Interceptor上加註解解決。 
首先簡單的定義一個註解: 
Java代碼  收藏代碼
  1. @Retention(RetentionPolicy.RUNTIME)  
  2. @Target(ElementType.TYPE)  
  3. public @interface MethodName {  
  4.     public String value();  
  5. }  

在攔截器的實現類加上該註解: 
Java代碼  收藏代碼
  1. @MethodName("execute1")  
  2. public class InterceptorImpl implements Interceptor {...}  

在TargetProxy中判斷interceptor的註解,看是否實行攔截: 
Java代碼  收藏代碼
  1. public Object invoke(Object proxy, Method method,  
  2.                          Object[] args) throws Throwable {  
  3.         MethodName methodName =   
  4.          this.interceptor.getClass().getAnnotation(MethodName.class);  
  5.         if (ObjectUtils.isNull(methodName))  
  6.             throw new NullPointerException("xxxx");  
  7.           
  8.         //如果註解上的方法名和該方法名一樣,才攔截  
  9.         String name = methodName.value();  
  10.         if (name.equals(method.getName()))  
  11.             return interceptor.intercept(new Invocation(target,    method, args));  
  12.           
  13.         return method.invoke(this.target, args);  
  14. }  

最後客戶端調用: 
Java代碼  收藏代碼
  1. Target target = new TargetImpl();  
  2. Interceptor interceptor = new InterceptorImpl();  
  3. target = (Target)TargetProxy.bind(target, interceptor);  
  4. target.execute();  

從客戶端調用代碼可以看出,客戶端首先需要創建一個目標對象和攔截器,然後將攔截器和目標對象綁定並獲取代理對象,最後執行代理對象的execute()方法。 
根據迪米特法則來講,其實客戶端根本不需要了解TargetProxy類。將綁定邏輯放到攔截器內部,客戶端只需要和攔截器打交道就可以了。 
即攔截器接口變爲: 
Java代碼  收藏代碼
  1. public interface Interceptor {  
  2. public Object intercept(Invocation invocation)  throws Throwable ;  
  3. public Object register(Object target);  

攔截器實現: 
Java代碼  收藏代碼
  1. @MethodName("execute1")  
  2. public class InterceptorImpl implements Interceptor {  
  3.       
  4.     public Object intercept(Invocation invocation)throws Throwable {  
  5.         System.out.println("Go Go Go!!!");  
  6.         return invocation.proceed();  
  7.     }  
  8.       
  9.     public Object register(Object target) {  
  10.         return TargetProxy.bind(target, this);  
  11.     }  
  12. }  

客戶端調用: 
Java代碼  收藏代碼
  1. Target target = new TargetImpl();  
  2. Interceptor interceptor = new InterceptorImpl();  
  3.   
  4. target = (Target)interceptor.register(target);  
  5. target.execute1();  



OK,上面的一系列過程其實都是mybatis的攔截器代碼結構,我只是學習了之後用最簡單的方法理解一遍罷了。 
上面的TargetProxy其實就是mybatis的Plug類。Interceptor和Invocation幾乎一樣。只是mybatis的Interceptor支持的註解 
更加複雜。 
mybatis最終是通過將自定義的Interceptor配置到xml文件中: 
Xml代碼  收藏代碼
  1. <!-- 自定義處理Map返回結果的攔截器 -->  
  2.  <plugins>  
  3.      <plugin interceptor="com.gs.cvoud.dao.interceptor.MapInterceptor" />  
  4.  </plugins>  

通過讀取配置文件中的Interceptor,通過反射構造其實例,將所有的Interceptor保存到InterceptorChain中。 
Java代碼  收藏代碼
  1. public class InterceptorChain {  
  2.   
  3.   private final List<Interceptor> interceptors = new ArrayList<Interceptor>();  
  4.   
  5.   public Object pluginAll(Object target) {  
  6.     for (Interceptor interceptor : interceptors) {  
  7.       target = interceptor.plugin(target);  
  8.     }  
  9.     return target;  
  10.   }  
  11.   
  12.   public void addInterceptor(Interceptor interceptor) {  
  13.     interceptors.add(interceptor);  
  14.   }  
  15.     
  16.   public List<Interceptor> getInterceptors() {  
  17.     return Collections.unmodifiableList(interceptors);  
  18.   }  
  19.   
  20. }  

mybatis的攔截器只能代理指定的四個類:ParameterHandler、ResultSetHandler、StatementHandler以及Executor。 
這是在mybatis的Configuration中寫死的,例如(其他三個類似): 
Java代碼  收藏代碼
  1. public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {  
  2.     ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement, parameterObject, boundSql);  
  3.       
  4.     //將配置文件中讀取的所有的Interceptor都註冊到ParameterHandler中,最後通過每個Interceptor的註解判斷是否需要攔截該ParameterHandler的某個方法。  
  5.     parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler);  
  6.     return parameterHandler;  
  7. }  

所以我們可以自定義mybatis的插件(攔截器)修改mybatis的很多默認行爲, 
例如, 
通過攔截ResultSetHandler修改接口返回類型; 
通過攔截StatementHandler修改mybatis框架的分頁機制; 
通過攔截Executor查看mybatis的sql執行過程等等。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章