Java基礎之靜態代理和動態代理

本篇介紹靜態代理和動態代理。

代理模式也是設計模式中的一種,主要解決的是在直接訪問對象時帶來的問題,其目的是爲其他對象提供一個代理來控制對目標對象的訪問。代理類爲委託類預處理消息,過濾消息並轉發消息,以及消息被委託類執行後的後續處理。

代理模式的類圖如下:

爲了保持行爲的一致性,委託類和代理類通常會實現相同的接口,客戶端調用Subject時不知道調用的是哪個,事實上它也不需要知道。

按照代理類的創建時機,可以將代理其分爲兩類,分別爲靜態代理和動態代理。

靜態代理

靜態代理在編譯時就已經將接口、代理類、被代理類等確定下來,在程序運行之前,代理類的.class文件就已經生成。

示例代碼如下:

Subject

public interface Subject {
    void saySomething();
}

RealSubject

public class RealSubject implements Subject{
    @Override
    public void saySomething() {
        System.out.println("***我是委託類***");
    }
}

Proxy

public class Proxy implements Subject{
    private Subject subject;

    public Proxy(Subject subject) {
        this.subject = subject;
    }

    @Override
    public void saySomething() {
        System.out.println("***我是代理類,我要調用委託類了哦***");
        subject.saySomething();
        System.out.println("***委託類調用完了,我還是代理類***");
    }
}

測試代碼

public class ProxyTest {
    public static void main(String[] args) {
        Subject subject = new RealSubject();
        Proxy proxy = new Proxy(subject);
        proxy.saySomething();
    }
}

輸出結果

***我是代理類,我要調用委託類了哦***
***我是委託類***
***委託類調用完了,我還是代理類***

由於委託類和代理類需要實現同一接口,不僅有大量代碼重複,並且在接口增加方法時,所有的委託類和代理類都需要新增此方法。除此之外,代理對象只能委託同一接口的對象,當有多個接口的對象需要代理時,就要爲每個接口都創建一個代理類,增加了代碼複雜度。

那麼能不能使用一個類就可以代理所有類呢?可以的,這就需要使用動態代理。

動態代理

動態代理的代理類是在程序運行時運用反射機制動態創建而成。動態代理又分爲JDK動態代理和CGLIB動態代理,其中JDK代理要求委託類必須實現接口,CGLIB動態代理可以直接代理類。

JDK動態代理

JDK動態代理涉及一個接口和一個類,一個接口爲InvocationHanlder接口,一個類爲Proxy類。在介紹JDK動態代理前,我們先來看一下InvocationHanlder接口和Proxy類的API。

InvocationHanlder

InvocationHandler接口只有一個invoke()方法。

public Object invoke(Object proxy, Method method, Object[] args)

invoke()方法的第一個參數是委託類實例,第二個接口是被代理的方法實例,第三個參數是被代理的方法的參數數組。我們需要定義一個類來實現InvocationHandler接口,此類實現的invoke()方法通過反射調用委託類的指定方法。

Proxy

Proxy提供了創建動態代理類和實例的靜態方法,它也是所有動態代理類的父類。在Proxy類內部維護了一個InvocationHanlder類型的成員變量,該成員變量的值在Proxy構造函數中初始化。Proxy構造函數如下:

protected Proxy(InvocationHandler h) {
    Objects.requireNonNull(h);
    this.h = h;
}

常用方法如下:

//獲取代理類的InvocationHandler實例
public static InvocationHandler getInvocationHandler(Object proxy);
//返回代理類的Class實例
public static Class<?> getProxyClass(ClassLoader loader,Class<?>... interfaces);
//判斷該類是不是代理類
public static boolean isProxyClass(Class<?> cl);
//返回指定接口的代理類的實例
public static Object newProxyInstance(ClassLoader loader,Class<?>[] interfaces,InvocationHandler h)

要使用動態代理爲委託類RealSubject創建代理類,首先創建一個SubjectInvocationHandler類實現InvocationHandler接口,在這個類中我們需要把委託類實例傳入但又不能與靜態代理相同(只能代理某一接口的實現類),因此傳入的類型應該是Object。

public class SubjectInvocationHandler implements InvocationHandler {
    private Object object;

    public SubjectInvocationHandler(Object object) {
        this.object = object;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("我是SubjectInvocationHandler的invoke()方法哦...");
        System.out.println("即將執行委託類的指定方法...");
        Object result = method.invoke(object,args);
        System.out.println("執行完委託類的指定方法了哦...");
        return result;
    }
}

測試方法

public class DynamicProxyTest {
    public static void main(String[] args) {
        //創建委託類實例
        RealSubject rs = new RealSubject();
        //創建調用處理類實例
        SubjectInvocationHandler sih = new SubjectInvocationHandler(rs);
        //獲取委託類的Class實例
        Class subClass = rs.getClass();
        //創建代理類
        Subject proxySubject = (Subject) Proxy.newProxyInstance(subClass.getClassLoader(),subClass.getInterfaces(),sih);
        //調用指定方法
        proxySubject.saySomething();
        //輸出代理類實現的接口
        Class[] interfaces = proxySubject.getClass().getInterfaces();
        for (Class i:interfaces){
            System.out.println(i.getName());
        }
    }
}

輸入結果

我是SubjectInvocationHandler的invoke()方法哦...
即將執行委託類的指定方法...
***我是委託類***
執行完委託類的指定方法了哦...
org.practice.Subject
java.io.Serializable

由newProxyInstance()方法生成的代理類繼承了Proxy類並實現了委託類實現的全部接口,在使用代理類實例調用接口方法時,通過反射調用了委託類的對應方法。

從上面可以看出,JDK動態代理要求委託類必須實現至少一個接口,這也是JDK動態代理的侷限性。Spring AOP就是通過動態代理實現的,在一個類沒有實現任何接口時Spring AOP還是正常工作的,原因在於在委託類沒有實現任何接口時Spring AOP會使用另一個叫做CGLIB的動態代理。

CGLIB動態代理

CGLIB動態代理不要求委託類實現接口,因爲它是通過使動態生成的代理類繼承委託類實現的,也因此,CGLIB無法代理final修飾的類。CGLIB與JDK動態代理類似,需要一個類來生成代理類以及另一個類來設定增強邏輯,分別爲Enhancer類以及MethodInterceptor接口。

MethodInterceptor接口只有一個方法。

//參數分別爲:委託類對象,要攔截的方法,被攔截方法的參數,代理方法
Object intercept(Object var1, Method var2, Object[] var3, MethodProxy var4) throws Throwable;

Enhancer類是一個字節碼增強器,功能與JDK動態代理中的Proxy類類似,可以用來爲未實現任何接口的類創建代理。以下使用這兩個類實現CGLIB動態代理的例子。

委託類RealSubject

public class RealSubject {
    public void saySomething() {
        System.out.println("我是委託類...");
    }
}

方法攔截器SubjectInterceptor

public class SubjectInterceptor implements MethodInterceptor {
    @Override
    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        System.out.println("進入攔截器...");
        Object object = methodProxy.invokeSuper(o,objects);
        System.out.println("被攔截方法執行完了...");
        return object;
    }
}

測試類

public class CGLIBProxyTest {
    public static void main(String[] args) {
    //生成動態代理類的Class文件
    System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY,"D:\\WorkSpace\\project");
        //創建Enhancer對象
        Enhancer enhancer = new Enhancer();
        //設置目標類的字節碼文件
        enhancer.setSuperclass(RealSubject.class);
        //設置回調函數
        enhancer.setCallback(new SubjectInterceptor());
        //創建代理類
        RealSubject rs = (RealSubject) enhancer.create();
        //使用代理對象調用目標方法
        rs.saySomething();
    }
}

輸出結果

CGLIB debugging enabled, writing to 'D:\WorkSpace\project'
進入攔截器...
我是委託類...
被攔截方法執行完了...

CGLIB動態代理並沒有使用反射,因爲反射的效率並不高,CGLIB底層是使用。CGLIB是使用Fastclass機制,即使動態生成的代理類繼承委託類,在代理類裏對委託類的非final方法建立索引,通過索引來調用相應的方法。

下表爲靜態代理,JDK動態代理和CGLIB動態代理的比較。

代理方式 實現方式 特點
靜態代理 委託類和代理類實現同一接口,通過代理類調用委託類的方法 只服務一種類型的對象,如果要服務多種類型則需要爲每種類型都創建代理類,增加了代碼的複雜性;由於委託類和代理類實現相同接口,勢必會有大量代碼重複;在編譯時就確定了代理對象
JDK動態代理 委託類和代理類實現同一接口,創建InvocationHandler的實現類並重寫invoke()方法來自定義增強邏輯,使用Proxy類創建代理類對象 通過反射機制實現;委託類必須實現至少一個接口;動態生成的代理類繼承了Proxy類
CGLIB動態代理 創建MethodInterceptor接口的實現類並重寫intercept()方法來自定義增強邏輯,使用Enhancer類創建代理類對象 使動態生成的代理類繼承委託類,底層使用asm字節碼生成框架來生成class文件
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章