動態代理與責任鏈模式
動態代理和責任鏈設計模式適用範圍廣,在Spring和MyBatis有著重要的應用,比如SpringAOP、Mybatis的外掛技術,想要搞懂當中的技術原理必須掌握上面兩個設計模式。
代理模式可以理解為您要操作一個物件,但是要經過這個物件的“代理”物件去操作。就好似你在一家軟體公司做開發,客戶發現程式有Bug,會找到商務對接人說,最後商務的同事再找到你去解決問題。“商務”是代理物件,“你”是真實物件。代理模式分為靜態代理和動態代理,其作用是可以在真實物件訪問之前或者之後加入自定義的邏輯,又或者根據自定義規則來控制是否使用真實物件。
靜態代理是真實物件與代理物件(Proxy)實現相同的介面,代理物件包含真實物件的引用,客戶端通過代理物件去訪問真實物件,代理物件可以在真實物件訪問之前或之後執行其他操作。假設要你設計一個對外開放的商品庫存資訊查詢介面,並且要限制呼叫方在一天時間內的呼叫次數。用代理模式代理庫存介面,首先在庫存查詢前檢查使用者是否有許可權訪問,然後在查詢後要記錄使用者查詢日誌,以便根據查詢次數,判斷呼叫上限。
動態代理是在程式執行時,通過反射機制建立代理物件,實現動態代理方法。動態代理相比於靜態代理的好處,是代理物件不用實現真實物件的介面,這樣能代理更多方法。因為靜態代理是一個介面對應一個型別,如果介面新增新方法,則所有代理類都要實現此方法,所以動態代理脫離了介面實現,一個代理類就能代理更多方法。一些公共程式碼邏輯也就可以在多個代理方法裡複用,例如:資料庫事務開啟、提交、回滾,這些公共程式碼都分別是在真實方法呼叫的前後出現,而動態代理會幫我們把功能程式碼織入到方法裡。在Java中最常用動態代理有兩種,一種是JDK動態代理,這是JDK自帶的功能;另一種是CGLIB,由第三方提供的一個技術,Spring是用了JDK代理和CGLIB兩種,兩者的區別是,JDK代理要提供介面作為於代理引數才能使用,而CGLIB不需要提供介面,只要一個非抽象類就能代理,適用於一些不能提供介面的場景。
1. JDK動態代理
(1)定義真實物件介面,因為JDK動態代理要藉助接口才能代理物件
public interface SayService { void sayHello(); } public class SayServiceImpl implements SayService { @override public void sayHello() { System.out.println("Hello Friend"); } }
(2)建立代理類,實現java.lang.reflect.InvocationHandler介面
public class JdkProxyExample implements InvocationHandler { // 真實物件 private Object target = null; public Object bind(Object target) { this.target = target; return Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), this); } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("代理前"); System.out.println("呼叫真實物件"); Object r = method.invoke(target, args); System.out.println("代理後"); return r; } }
Proxy.newProxyInstance方法的作用是建立代理物件,並建立代理物件與真實物件的關係。包含3個引數
-
第1個是類載入器,用於把Java位元組碼轉換成Class類例項物件,這裡用了target物件所屬的類載入器
-
第2個是生成的動態代理物件需要掛載到哪些介面下,上面是取了真實物件的介面
-
第3個是實現InvocationHandler介面的代理類,this表示當前物件
InvocationHandler介面的invoke方法實現代理邏輯,invoke其中引數含義如下
-
proxy:代理物件,就是bind方法生成的物件
-
method:當前呼叫方法,method.invoke(target, args)呼叫真實物件方法
-
args:當前呼叫方法引數
(3)測試JDK動態代理
public void testJdk() { JdkProxyExample jdkProxy = new JdkProxyExample(); SayService proxy = (SayService) jdkProxy.bind(new SayServiceImpl()); proxy.sayHello(); } /* 代理前 呼叫真實物件 Hello Friend 代理後 */
2. CGLIB動態代理
JDK動態代理要提供接口才能代理,但在一些不能提供介面的場景下,CGLIB動態代理技術不需要提供介面,只要一個非抽象類就能動態代理。新建類實現MethodInterceptor介面(spring框架的cglib包),使用Enhacer建立代理物件。程式碼實現如下:
import org.springframework.cglib.proxy.Enhancer; import org.springframework.cglib.proxy.MethodInterceptor; import org.springframework.cglib.proxy.MethodProxy; import java.lang.reflect.Method; public class CglibProxyExample implements MethodInterceptor { public Object bind(Class classz) { Enhancer enhancer = new Enhancer(); enhancer.setCallback(this); enhancer.setSuperclass(classz); return enhancer.create(); } @Override public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable { System.out.println("呼叫真實物件前"); Object result = methodProxy.invokeSuper(proxy, args); System.out.println("呼叫真實物件後"); return result; } } @Test public void testCglib() { CglibProxyExample cglibProxy = new CglibProxyExample(); TestService proxy = (TestService)cglibProxy.bind(TestService.class); proxy.sayHello(); } /* 呼叫真實物件前 Hi hello 呼叫真實物件後 */
3. 攔截器
由於動態代理一般不好理解,通常會設計一個攔截器介面提供給開發者使用,這樣只需要知道攔截器介面方法、含義和作用即可,無須知道動態代理是怎麼實現。攔截器介面設計如下:
public interface Interceptor { boolean before(Object target); Object around(Object target, Method method, Object[] args); void after(Object target); void afterThrowing(Object target); void afterReturning(Object target); }
假定攔截器使用規則是:在呼叫真實方法前,先訪問before方法,如果返回true,執行真實物件方法,否則執行around方法代替真實方法的呼叫。after方法在真實方法或around呼叫後執行,如果真實方法或around呼叫異常,執行afterThrowing方法,否則執行afterReturning方法。
定義了以上規則,開發者只要知道攔截器怎樣使用,不需要知道動態代理怎麼實現。
下面程式碼演示如何使用上面定義的攔截器
public class SayServiceInterceptor implements Interceptor { @Override public boolean before(Object target) { System.out.println("代理前執行before方法"); // 不執行真實物件的方法 return false; } @Override public Object around(Object target, Method method, Object[] args) { System.out.println("真實物件方法被替換,執行around"); return null; } @Override public void after(Object target) { System.out.println("代理後執行after方法"); } @Override public void afterThrowing(Object target) { System.out.println("真實物件方法呼叫異常,執行afterThrowing方法"); } @Override public void afterReturning(Object target) { System.out.println("最後執行afterReturning方法"); } } public void testJdk() { SayService say = new SayServiceImpl(); SayServiceInterceptor interceptor = new SayServiceInterceptor(); SayService proxy = (SayService) JdkProxyExample.bind(say, interceptor); proxy.sayHello(); } /** 代理前執行before方法 真實物件方法被替換,執行around 代理後執行after方法 最後執行afterReturning方法 */
上面規則用動態代理實現程式碼如下:
public class JdkProxyExample implements InvocationHandler { // 真實物件 private Object target = null; // 攔截器 private Interceptor interceptor; public JdkProxyExample(Object target, Interceptor interceptor) { this.target = target; this.interceptor = interceptor; } public static Object bind(Object target, Interceptor interceptor) { return Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), new JdkProxyExample(target, interceptor)); } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { if(interceptor == null) { return method.invoke(target, args); } Object result = null; boolean exceptionFlag = false; try { if (interceptor.before(target)) { result = method.invoke(target, args); }else { result = interceptor.around(target, method, args); } }catch (Exception e) { exceptionFlag = true; } finally { interceptor.after(target); } if(exceptionFlag) { interceptor.afterThrowing(target); }else { interceptor.afterReturning(target); } return result; } }
設計攔截器能簡化了動態代理的使用方法,使程式更簡單。在實際使用場景中攔截器實現後,要在開發者的程式進行xml配置,或者在實現類添加註解標識等方式,來查詢到攔截器實現類,然後反射建立並載入到程式裡。SpringAOP也是通過@Aspect註解建立切面(攔截器),@execution定義連線點攔截方法。
4. 責任鏈模式
設計攔截器去代替動態代理,然後將攔截器的介面提供給開發者用,從而簡化開發者的開發難度,但是攔截器可能會有多個,舉個例子,您要請假一週,然後在OA上提交請假申請單,要經過專案經理、部門經理、人事等多個角色的審批,每個角色都會對申請單攔截、修改、審批等。如果把請假申請單看做一個物件,則它會經過三個攔截器的攔截處理。當一個物件在一條鏈上被多個攔截器攔截處理時,我們把這樣的設計模式稱為責任鏈模式。前一個攔截器的返回結果會作用於後一個攔截器,程式碼上的實現是用後一個攔截器去代理了前一個攔截器的方法,以此類推,層層代理。最終結果如下圖:
程式碼實現如下:
@Test public void testInterceptor() { SayService target = new SayServiceImpl(); SayService proxy1 = (SayService)JdkProxyExample.bind(target, new SayServiceInterceptor("proxy1")); SayService proxy2 = (SayService)JdkProxyExample.bind(proxy1, new SayServiceInterceptor("proxy2")); SayService proxy3 = (SayService)JdkProxyExample.bind(proxy2, new SayServiceInterceptor("proxy3")); proxy1.sayHello(); } /** proxy3:代理前執行before方法 proxy2:代理前執行before方法 proxy1:代理前執行before方法 Hello Friends proxy1:代理後執行after方法 proxy2:代理後執行after方法 proxy3:代理後執行after方法 */
before方法執行順序是從最後一個攔截器到第一個攔截器,而after方法是從第一個攔截器到最後一個。責任鏈模式的優點在於我們可以在傳遞鏈中加上新的攔截器,增加攔截邏輯,但缺點是每增加一層代理反射,程式效能越差。