1. 程式人生 > 其它 >動態代理與責任鏈模式

動態代理與責任鏈模式

  動態代理和責任鏈設計模式適用範圍廣,在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方法是從第一個攔截器到最後一個。責任鏈模式的優點在於我們可以在傳遞鏈中加上新的攔截器,增加攔截邏輯,但缺點是每增加一層代理反射,程式效能越差。