代理模式(靜態代理和動態代理)
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();
這種代理模式也最為簡單,就是通過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();
於是你的代理模型會變成這樣:
毫無疑問,僅僅為了擴充套件同樣的功能,在方案一種,我們會重複建立多個邏輯相同,僅僅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方法。
對上面這個過程,做一張圖總結一下: