1. 程式人生 > >代理模式(靜態代理和動態代理)

代理模式(靜態代理和動態代理)

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

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

一個典型的代理模式通常有三個角色,這裡稱之為**代理三要素**

共同介面

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的引用,並進行一層封裝。

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

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

假如有這樣一個需求,有十個不同的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 實現類裡:

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("proxy do");
 
        return method.invoke(realObject, args);
    }
}


這個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。只不過這個Proxy不是我們自己寫的,而是java幫我們生成的,有沒有一點動態的味道。

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

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

綜上,動態生成+代理模式,也就是動態代理。
網上搜了不少文章,到了這裡,接下來就是和cglib等動態代理實現方法做一下橫向比較。本文不做橫向比較,為了不偏離主題,接下來做縱向挖掘。

看一下原始碼
道理清楚了,但是這篇文章題目是搞懂,所以來看一下這個Proxy是如何自動被生成的。入口就在newProxyInstance方法,核心程式碼如下:

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物件。

proxyPkg = ReflectUtil.PROXY_PACKAGE + ".";
long num = nextUniqueNumber.getAndIncrement();
String proxyName = proxyPkg + proxyClassNamePrefix + num;
byte[] proxyClassFile = ProxyGenerator.generateProxyClass(
                proxyName, interfaces, accessFlags);
return defineClass0(loader, proxyName,
                                    proxyClassFile, 0, proxyClassFile.length);

看一下第四步生成.class檔案位元組碼的過程,主要分為兩個階段:

addProxyMethod(hashCodeMethod, Object.class);
addProxyMethod(equalsMethod, Object.class);
addProxyMethod(toStringMethod, Object.class);
 
for (int i = 0; i < interfaces.length; i++) {
    Method[] methods = interfaces[i].getMethods();
    for (int j = 0; j < methods.length; j++) {
         addProxyMethod(methods[j], interfaces[i]);
    }
}
methods.add(this.generateConstructor());
 
 for (List<ProxyMethod> sigmethods : proxyMethods.values()) {
    for (ProxyMethod pm : sigmethods) { 
        fields.add(new FieldInfo(pm.methodFieldName,
                                   "Ljava/lang/reflect/Method;", ACC_PRIVATE | ACC_STATIC));
        methods.add(pm.generateMethod());
    }
 }
methods.add(generateStaticInitializer());

第一個階段的程式碼比較清晰,主要就是新增各種Method,比如toString()、equals,以及傳入的代理介面中的方法。再新增一下構造方法以及靜態初始化方法。這要構成了一個物件,儲存生成Proxy的Class的一些資訊。

到了這裡,已經把要構造的Proxy的方法基本定義完成了,接下來就要生成這個.class檔案了。

ByteArrayOutputStream bout = new ByteArrayOutputStream();
 DataOutputStream dout = new DataOutputStream(bout);
 dout.writeInt(0xCAFEBABE);
 ...
 dout.writeShort(ACC_PUBLIC | ACC_FINAL | ACC_SUPER);
 ...
 return bout.toByteArray();

看到這個CAFEBABE,就清楚第二階段的內容了。CAFEBABE是Class檔案的魔數,關於Class檔案這個咖啡寶貝的魔數,相信做Java的人都知道。沒錯,第二階段就是生成位元組碼。按JVM規範,寫入Class檔案中包括許可權控制、方法表、欄位表等內容,生成符合規範的Class檔案。最後返回對應的位元組碼。

位元組碼生成以後,通過呼叫native方法defineClass解析位元組碼,就生成了Proxy的Class物件。

Proxy構造方法
看一下Proxy的構造方法位元組碼生成部分:

MethodInfo minfo = new MethodInfo("<init>", "(Ljava/lang/reflect/InvocationHandler;)V",ACC_PUBLIC);
DataOutputStream out = new DataOutputStream(minfo.code);
code_aload(0, out);
code_aload(1, out);
out.writeByte(opc_invokespecial);
out.writeShort(cp.getMethodRef(superclassName,"<init>", "(Ljava/lang/reflect/InvocationHandler;)V"));

關鍵在於,生成了一個引數為InvocationHandler的構造方法,code載入的是jvm方法區中的程式碼,然後通過invokespecial指令呼叫了父類構造方法。

檢視生成的Class檔案
上面利用位元組碼生成技術產生Class檔案的過程,看起來可能比較晦澀,其實我們可以檢視這個產生的Proxy到底是個什麼樣子。

注意ProxyGenerator中有這樣一個邏輯:

if(saveGeneratedFiles) {
    ...
    FileOutputStream file = new FileOutputStream(dotToSlash(name) + ".class");
    file.write(classFile);
    ...
 }

再看一下saveGeneratedFiles這個變數:

private final static boolean saveGeneratedFiles =
    java.security.AccessController.doPrivileged( 
    new GetBooleanAction("sun.misc.ProxyGenerator.saveGeneratedFiles"))
    .booleanValue();

這是一個final型別的變數,通過GetBooleanAction方法讀取系統變數,獲取系統設定。預設這個值是false,稍微看一下System這個類的原始碼,發現有可以設定系統變數的Api,然後在程式的main 函式設定一下這個變數:

System.getProperties().setProperty("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");
這個時候,再跑一遍程式,就可以看到生成的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