1. 程式人生 > >Java動態代理和靜態代理

Java動態代理和靜態代理

一、概念

  代理模式是常用的Java設計模式,它的特徵是代理類與委託類有同樣的介面,代理類主要負責為委託類預處理訊息、過濾訊息、把訊息轉發給委託類,以及事後處理訊息等。代理類與委託類之間通常會存在關聯關係,一個代理類的物件與一個委託類的物件關聯,代理類的物件本身並不真正實現服務,而是通過呼叫委託類的物件的相關方法,來提供特定的服務。按照代理類的建立時期,代理類可分為兩種。

靜態代理類:在編譯的時候就確定了代理類具體型別,再對其編譯。在程式執行前,代理類的.class檔案就已經存在了。
動態代理類:在程式執行時,運用反射機制動態的創建出代理類及其物件

區別:
--靜態代理通常只代理一個類,動態代理是代理一個介面下的多個實現類。
--靜態代理事先知道要代理的是什麼,而動態代理不知道要代理什麼東西,只有在執行時才知道。
--動態代理是實現JDK裡的InvocationHandler介面的invoke方法,但注意的是代理的是介面,也就是你的業務類必須要實現介面,通過Proxy裡的newProxyInstance得到代理物件。
--還有一種動態代理CGLIB,代理的是類,不需要業務類繼承介面,通過派生的子類來實現代理。通過在執行時,動態修改位元組碼達到修改類的目的。
 

二、Java 靜態代理

靜態代理通常用於對原有業務邏輯的擴充。比如持有二方包的某個類,並呼叫了其中的某些方法。然後出於某種原因,比如記錄日誌、列印方法執行時間,但是又不好將這些邏輯寫入二方包的方法裡。所以可以建立一個代理類實現和二方方法相同的方法,通過讓代理類持有真實物件,然後在原始碼中呼叫代理類方法,來達到新增我們需要業務邏輯的目的。

這其實也就是代理模式的一種實現,通過對真實物件的封裝,來實現擴充套件性。

一個典型的代理模式通常有三個角色,這裡稱之為**代理三要素**{真實角色與代理角色的抽象介面,真實角色,代理角色}

1. 代理模式的作用是:

     為其他物件提供一種代理以控制對這個物件的訪問。在某些情況下,一個客戶不想或者不能直接引用另一個物件,而代理物件可以在客戶端和目標物件之間起到中介的作用

2. 代理模式一般涉及到的角色

(1)抽象角色:宣告真實物件和代理物件的共同介面

(2)代理角色:代理物件角色內部含有對真實物件的引用,從而可以操作真實物件,同時代理物件提供與真實物件相同的介面以便在任何時刻都能代替真實物件。同時,代理物件可以在執行真實物件操作時,附加其他的操作,相當於對真實物件進行封裝

(3)真實角色:代理角色所代表的真實物件,是我們最終要引用的物件

抽象角色

public interface Action {
    public void doSomething();
}

真實角色

public class RealObject implements Action{
 
    public void doSomething() {
        System.out.println("do something");
    }
}

代理角色

public class Proxy implements Action {
    private Action realObject;
 
    public Proxy(Action realObject) {
        this.realObject = realObject;
    }
    public void doSomething() {
        System.out.println("proxy do");
        realObject.doSomething();
    }
}

執行程式碼

    Proxy proxy = new Proxy(new RealObject());
    proxy.doSomething();

simple_proxy.png

由此可見:代理類可以為委託類預處理訊息、把訊息轉發給委託類和事後處理訊息等

這種代理模式也最為簡單,就是通過proxy持有realObject的引用,並進行一層封裝。

3.靜態代理的優點和缺點

先看看代理模式的優點: 擴充套件原功能,不侵入原始碼。

再看看這種代理模式的缺點:

假如有這樣一個需求,有十個不同的RealObject,同時我們要去代理的方法是不同的,比要代理方法:doSomething、doAnotherThing、doTwoAnotherThing,新增代理前,原始碼可能是這樣的:

realObject.doSomething();
realObject1.doAnotherThing();
realObject2.doTwoAnother();

為了解決這個問題,我們有方案一:

為這些方法建立不同的代理類,代理後的程式碼是這樣的:

proxy.doSomething();
proxy1.doAnotherThing();
proxy2.doTwoAnother();

當然,也有方案二:

通過建立一個proxy,持有不同的realObject,實現Action1、Action2、Action3介面,來讓程式碼變成這樣:

proxy.doSomething();
proxy.doAnotherThing();
proxy.doTwoAnother();

於是你的代理模型會變成這樣:

dynamic_proxy.png

毫無疑問,僅僅為了擴充套件同樣的功能,在方案一種,我們會重複建立多個邏輯相同,僅僅RealObject引用不同的Proxy。

而在方案二中,會導致proxy的膨脹,而且這種膨脹往往是無意義的。此外,假如方法簽名是相同的,更需要在呼叫的時候引入額外的判斷邏輯。

java 動態代理

搞清楚靜態代理的缺點十分重要,因為動態代理的目的就是為了解決靜態代理的缺點。通過使用動態代理,我們可以通過在執行時,動態生成一個持有RealObject、並實現代理介面的Proxy,同時注入我們相同的擴充套件邏輯。哪怕你要代理的RealObject是不同的物件,甚至代理不同的方法,都可以動過動態代理,來擴充套件功能。

簡單理解,動態代理就是我們上面提到的方案一,只不過這些proxy的建立都是自動的並且是在執行期生成的。

動態代理的實現過程

具體有如下四步驟:

--建立一個實現介面InvocationHandler的呼叫處理器(類),它必須實現invoke方法;

--通通過Proxy的靜態方法newProxyInstance(ClassLoader loader, Class[] interfaces, InvocationHandler h) 建立一個動態代理類;

--通過反射機制獲得動態代理類的建構函式,其唯一引數型別是呼叫處理器介面型別;

--通過建構函式建立動態代理類例項,構造時呼叫處理器物件作為引數被傳入,通過代理呼叫方法。

動態代理基本用法

使用動態代理,需要將要擴充套件的功能寫在一個InvocationHandler 實現類裡,用來響應代理的任何呼叫:

public class DynamicProxyHandler implements InvocationHandler {
    private Object realObject;
 
    public DynamicProxyHandler(Object realObject) {
        this.realObject = realObject;
    }
 
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        //代理擴充套件邏輯
        System.out.println("準備工作之前:");

        //轉調具體目標物件的方法
        Object object= method.invoke(realObject, args);

        System.out.println("工作已經做完了!");

        return object;
    }
}

這個Handler中的invoke方法中實現了代理類要擴充套件的公共功能。

到這裡,需要先看一下這個handler的用法:

public static void main(String[] args) {
        RealObject realObject = new RealObject();
        Action proxy = (Action) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), new Class[]{Action.class}, new DynamicProxyHandler(realObject));
        proxy.doSomething();
}

Proxy.newProxyInstance 傳入的是一個ClassLoader, 一個代理介面,和我們定義的handler,返回的是一個Proxy的例項。

仔細體會這個過程,其實有點類似我們在靜態代理中提到的方案一,生成了一個包含我們擴充套件功能,持有RealObject引用,實現Action介面的代理例項Proxy。

讓我們再回顧一下代理三要素:真實物件:RealObject,代理介面:Action,代理例項:Proxy

上面的程式碼實含義也就是,輸入 RealObject、Action,返回一個Proxy。妥妥的代理模式。
 

看一下原始碼

private static final Class<?>[] constructorParams =
        { InvocationHandler.class };
 
public static Object newProxyInstance(ClassLoader loader,
                                          Class<?>[] interfaces,
                                          InvocationHandler h)
        throws IllegalArgumentException
{
    Class<?> cl = getProxyClass0(loader, intfs);
    ...
    final Constructor<?> cons = cl.getConstructor(constructorParams);
 
    if (!Modifier.isPublic(cl.getModifiers())) {
        AccessController.doPrivileged(new PrivilegedAction<Void>() {
            public Void run() {
            cons.setAccessible(true);
            return null;
        }
        });
    }
return cons.newInstance(new Object[]{h});
}

整體流程就是:

1、生成代理類Proxy的Class物件。

2、如果Class作用域為私有,通過 setAccessible 支援訪問

3、獲取Proxy Class建構函式,建立Proxy代理例項。

生成Proxy的Class檔案

生成Class物件的方法中,先是通過傳進來的ClassLoader引數和Class[] 陣列作為組成鍵,維護了一個對於Proxy的Class物件的快取。這樣需要相同Proxy的Class物件時,只需要建立一次。

第一次建立該Class檔案時,為了執行緒安全,方法進行了大量的處理,最後會來到ProxyClassFactory的apply方法中,經過以下流程:

1、校驗傳入的介面是否由傳入的ClassLoader載入的。

2、校驗傳入是否是介面的Class物件。

3、校驗是否傳入重複的介面。

4、拼裝代理類包名和類名,生成.class 檔案的位元組碼。

5、呼叫native方法,傳入位元組碼,生成Class物件。

這個時候,再跑一遍程式,就可以看到生成的Proxy的Class檔案了,直接雙擊利用 ide 反編譯。

 
package com.sun.proxy;
 
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.UndeclaredThrowableException;
 
public final class $Proxy0 extends Proxy implements Action {
    private static Method m1;
    private static Method m3;
    private static Method m2;
    private static Method m0;
 
    public $Proxy0(InvocationHandler var1) throws  {
        super(var1);
    }
 
 
    public final void doSomething() throws  {
        try {
            super.h.invoke(this, m3, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }
 
    ...
 
    static {
        try {
           ...
            m3 = Class.forName("Action").getMethod("doSomething", new Class[0]);
        } catch (NoSuchMethodException var2) {
            throw new NoSuchMethodError(var2.getMessage());
        } catch (ClassNotFoundException var3) {
            throw new NoClassDefFoundError(var3.getMessage());
        }
    }
}
 

省略一些無關程式碼,可以看到兩個重要的方法。

一個就是我們的代理方法doSomething、另一個就是構造方法。

這個$Proxy0 繼承 Proxy並呼叫了父類的構造方法,回憶一下上文提到的invokeSpecial,怎麼樣,對上了吧。

看一下Proxy中這個構造方法:

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

在看一下$Proxy0 的代理方法:

super.h.invoke(this, m3, (Object[])null);

再來回顧一下生成Proxy例項的過程:

private static final Class<?>[] constructorParams =
        { InvocationHandler.class };
...
final Constructor<?> cons = cl.getConstructor(constructorParams);
...
return cons.newInstance(new Object[]{h});   

實newInstance生成Proxy例項時,通過$Proxy0的Class物件,選擇了這個InvocationHandler為引數的構造方法,傳入我們定義的InvocationHandler並生成了一個 Proxy0的例項!InvocationHandler 裡有realObject的邏輯以及我們的擴充套件邏輯,當我們呼叫Proxy0的doSomething方法時,就會呼叫到我們InvocationHandler 裡 實現的invoke方法。

對上面這個過程,做一張圖總結一下:

flow.png

總結:

動態代理的做法:在執行時刻,可以動態創建出一個實現了多個介面的代理類。每個代理類的物件都會關聯一個表示內部處理邏輯的InvocationHandler接 口的實現。當使用者呼叫了代理物件所代理的介面中的方法的時候,這個呼叫的資訊會被傳遞給InvocationHandler的invoke方法。在 invoke方法的引數中可以獲取到代理物件、方法對應的Method物件和呼叫的實際引數。invoke方法的返回值被返回給使用者。這種做法實際上相 當於對方法呼叫進行了攔截。

類圖如下所示:

上面類圖中使用的JDK中的Proxy類,所以是需要要辦法來告訴Proxy類需要做什麼,不能像靜態代理一樣,將程式碼放到Proxy類中,因為現在Proxy不是直接實現的。既然這樣的程式碼不能放在Proxy類中,那麼就需要一個InvocationHandler,InvocationHandler的工作就是響應代理的任何呼叫。