「原創」讓設計模式飛一會兒|⑥面試必問代理模式

2020-12-14 酷扯兒

本文轉載自【微信公眾號:java進階架構師,ID:java_jiagoushi】經微信公眾號授權轉載,如需轉載與原文作者聯繫

本文中所有的代碼和運行結果都是在amazon corretto openjdk 1.8環境中的,如果你不是使用該環境,可能會略有偏差。另外為了代碼看起來清晰整潔,將所有代碼中的異常處理邏輯全部拿去了

哈嘍,大家好,我是高冷就是範兒,好久不見!今天我們繼續來聊設計模式這個話題。前面已經講過幾個模式,如果沒有閱讀過的朋友可以回顧一下。

今天我要跟大家聊的是代理模式。相信只要是對GOF23設計模式有簡單了解過的,或者看過我github上面以前學習時記的筆記,或多或少是聽說過代理模式的。這一模式可以說是GOF23所有設計模式中應用最廣泛,但又最難以理解的一種模式,尤其是其中的動態代理模式,但是其功能之強大,應用場景之廣自然就體現出其重要性。有些場景要是沒有使用這一模式,就會變得很難實現。可以這麼說,我所了解過的或者閱讀過源碼的開源框架,底層幾乎沒有不用到代理模式的,尤其是接下去本文要說的重點-動態代理模式。因此,在文章的最後,我也會以一個在

Mybatis

底層使用動態代理模式解決的經典場景作為本文結束。

代理

首先,我們先來說說代理。何為代理?來看張圖。這就是我們日常租房的場景,客戶來一個陌生城市需要租一個房子,但是他人生地不熟,根本不知道行情,也不知道地段,更沒有房東的聯繫方式,所以,他會去找類似我愛我家之類的租房中介,而這些個中介手上會有大量房子的信息來源,自然會有個房東的聯繫方式,進而和房東取得聯繫,從而達到租房的目的。這個場景就是一個經典的代理模式的體現。

靜態代理

既然說到動態代理,自然聯想到肯定會有靜態代理。下面我們就先從簡單的開始,以上面租房的這個例子,用Java代碼實現靜態代理。

首先在代理模式(甭管靜態還是動態)結構中,肯定會有一個真實角色(Target),也是最後真正執行業務邏輯的那個對象,比如上圖中的房東(因為最後租的房子所有權是他的,也是和他去辦租房合同等手續),另外會有一個代理角色(Proxy),比如上圖中的房產中介(他沒有房產所有權),並且這個角色會必然實現一個與真實角色相同的抽象接口(Subject),為什麼呢?因為雖然這個出租的房子不是他的,但是是經他之手幫忙牽線搭橋出租出去的,也就是說,他和房東都會有出租房產的行為。另外代理角色會持有一個真實角色的引用,又是為什麼呢?因為他並不會(或者是不能)真正處理業務邏輯(因為房子不是他的唄),他會將真正的邏輯委託給真實角色處理。但是這個代理角色也不是一無是處,除了房子不是他的,但是他還可以給你幹點跑腿的工作嘛,比如幫你挑選最好的地段,挑選合適的價格等等,等你租房後出現漏水,或者電器啥的壞了可以幫你聯繫維修人員等等。如下代碼所示:

//公共抽象接口 - 出租的人public interface Person {void rent();}//真實角色 - 房東public class Landlord implements Person{ public void rent() { System.out.println("客官請進,我家的房子又大又便宜,來租我的吧..."); }}//代理角色 - 房產中介public class Agent implements Person{ Person landlord; public Agent(Person landlord) { this.landlord = landlord; } public void rent() { //前置處理 System.out.println("經過前期調研,西湖邊的房子環境挺好的..."); //委託真實角色處理 landlord.rent(); //後置處理 System.out.println("房子漏水,幫你聯繫維修人員..."); }}//客戶端public class Client { public static void main(String[] args) { Person landlord = new Landlord(); Person agent = new Agent(landlord); agent.rent(); }}//輸出結果:經過前期調研,西湖邊的房子環境挺好的...客官請進,我家的房子又大又便宜,來租我的吧...房子漏水,幫你聯繫維修人員...

靜態代理模式實現相對比較簡單,而且比較好理解,也確實實現了代理的效果。但是很遺憾,幾乎沒有一個開源框架的內部是採用靜態代理來實現代理模式的。那是為什麼呢?原因很簡單,從上面這個例子可以看出,靜態代理模式中的真實角色和代理角色緊耦合了。怎麼理解?

下面來舉個例子幫助理解靜態代理模式的缺點,深入理解靜態代理的缺點對於理解動態代理的應用場景是至關重要的。因為動態代理的誕生就是為了解決這一問題。

還是以上面的租房的場景,假設我現在需要你實現如下需求:有多個房東,並且每個房東都有多套房子出租,你怎麼用Java設計?按照上面的靜態代理模式的思路,你也許會有如下實現(偽代碼),

第一種方案:

public class Landlord01 implements Person{public void rent01() { ... } public void rent02() { ... } public void rent03() { ... }}public class Landlord02 implements Person{ public void rent01() { ... } public void rent02() { ... } public void rent03() { ... }}public class Landlord03 implements Person{ public void rent01() { ... } public void rent02() { ... } public void rent03() { ... }}... 可能還有很多房東,省略public class Agent01 implements Person{ Person landlord01; //省略構造器等信息 public void rent() {landlord01.rent();}}public class Agent02 implements Person{ Person landlord02; //省略構造器等信息 public void rent() {landlord02.rent();}}public class Agent03 implements Person{ Person landlord03; //省略構造器等信息 public void rent() {landlord03.rent();}}...

上面這種方案是為每個房東配一個對應的中介處理租房相關事宜。這種方案問題非常明顯,每一個真實角色都需要手動創建一個代理角色與之對應,而這些代理類的邏輯有可能都是很相似的,因此當真實角色數量非常多時,會造成代理類數量膨脹問題和代碼重複冗餘,方案不可取。

第二種方案:

public class Landlord01 implements Person{public void rent01() { ... } public void rent02() { ... } public void rent03() { ... }}public class Landlord02 implements Person{ public void rent01() { ... } public void rent02() { ... } public void rent03() { ... }}public class Landlord03 implements Person{ public void rent01() { ... } public void rent02() { ... } public void rent03() { ... }}public class Agent implements Person{ Person landlord01; Person landlord02; Person landlord03; //省略構造器等信息 public void rent01() { ... } public void rent02() { ... } public void rent03() { ... }}

第二種方案只創建一個代理角色,同時代理多個真實角色,這看上去貌似解決了第一種方案的弊病,但是同時引入了新的問題。那就是造成了代理類的膨脹。設計模式中有條重要原則——單一職責原則。這個代理類違反了該原則。當這個代理類為了代理其中某個真實角色時,需要將所有的真實角色的引用全部傳入,顯然太不靈活了。還是不可取。

而且有沒有發現靜態代理還有兩個很大的問題,第一,當抽象接口一旦修改,真實角色和代理角色必須全部做修改,這違反了設計模式的開閉原則。第二,每次創建一個代理角色,需要手動傳入一個已經存在的真實角色。但是在有些場景下,我們可能需要在並不知道真實角色的情況下創建出指定接口的代理。

動態代理

前面做了這麼多鋪墊,終於今天本文的主角——動態代理模式要登場了。此處應該有掌聲......而動態代理模式的產生就是為了解決上面提到的靜態代理所有弊病的。

JDK動態代理的實現關鍵在於

java.lang.reflect.Proxy

類,其

newProxyInstance(ClassLoader loader,Class<?>[] interfaces, InvocationHandler h)

方法是整個JDK動態代理的核心,用於生成指定接口的代理對象。這個方法有三個參數,分別表示加載動態生成的代理類的類加載器ClassLoader,代理類需要實現的接口interfaces以及調用處理器InvocationHandler,這三個參數一個比一個難以理解,說實話,我第一次學動態代理模式時,看到這三個參數也是一臉懵逼的狀態。動態代理模式之所以比較難理解關鍵也是這個原因。放心,後面會一一詳解。但在這之前,我們先做一下熱身,先用代碼簡單使用一下JDK的動態代理功能。代碼如下:

//公共抽象接口和真實角色和靜態代理的例子中代碼相同,省略//自定義調用處理器public class RentHandler implements InvocationHandler {Person landlord; public RentHandler(Person landlord) { this.landlord = landlord; } //客戶端對代理對象發起的所有請求都會被委託給該方法 public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { //前置處理 System.out.println("經過前期調研,西湖邊的房子環境挺好的..."); //委託給真實角色處理業務邏輯 method.invoke(landlord, args); //後置處理 System.out.println("房子漏水,幫你聯繫維修人員..."); return null; }}//客戶端public class Client2 { public static void main(String[] args) { Person landlord = new Landlord(); Person proxy = (Person) Proxy.newProxyInstance( ClassLoader.getSystemClassLoader(), //默認類加載器 new Class[]{Person.class}, //代理的接口 new RentHandler(landlord));//自定義調用處理器實現 proxy.rent(); }}//輸出結果:經過前期調研,西湖邊的房子環境挺好的...客官請進,我家的房子又大又便宜,來租我的吧...房子漏水,幫你聯繫維修人員...

可以看出,動態代理輕鬆的實現了代理模式,並且輸出了和靜態代理相同的結果,然而我們並沒有寫任何的代理類,是不是很神奇?下面我們就來深度剖析JDK實現的動態代理的原理。

Proxy.newProxyInstance()

在上面實現的JDK動態代理代碼中,核心的一行代碼就是調用

Proxy.newProxyInstance()

,傳入類加載器等參數,然後一頓神奇的操作後居然就直接返回了我們所需要的代理對象,因此我們就從這個神奇的方法開始說起......

進入這個方法的源碼中,以下是這個方法的核心代碼,邏輯非常清楚,使用

getProxyClass0

獲取一個Class對象,其實這個就是最終生成返回的代理代理類的Class對象,然後使用反射方式獲取有參構造器,並傳入我們的自定義InvocationHandler實例創建其對象。由此我們其實已經可以猜測,這個動態生成的代理類會有一個參數為InvocationHandler的構造器,這一點在之後會得到驗證。

public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h) throws IllegalArgumentException {... //省略一些非空校驗,權限校驗的邏輯 //返回一個代理類,這個是整個方法的核心,後續會做詳細剖析 Class<?> cl = getProxyClass0(loader, intfs); //使用反射獲取其有參構造器,constructorParams是定義在Proxy類中的欄位,值為{InvocationHandler.class} final Constructor<?> cons = cl.getConstructor(constructorParams); //使用返回創建代理對象 return cons.newInstance(new Object[]{h});}

那現在很明顯了,關鍵的核心就在於getProxyClass0()方法的邏輯了,於是我們繼續深入虎穴查看其源碼。

private static Class<?> getProxyClass0(ClassLoader loader, Class<?>... interfaces) {if (interfaces.length > 65535) { throw new IllegalArgumentException("interface limit exceeded"); } return proxyClassCache.get(loader, interfaces);}

最開始就是檢驗一下實現接口數量,然後執行

proxyClassCache.get()

。proxyClassCache是一個定義在Proxy中的欄位,你就將其當做一個代理類的緩存。這個也好理解,稍後大家會看到,動態代理類生成過程中會伴隨大量的IO操作,字節碼操作還有反射操作,還是比較消耗資源的。如果需要創建的代理類數量特別多,性能會比較差。所以Proxy提供了緩存機制,將已經生成的代理類緩存,當獲取時,會先從緩存獲取,如果獲取不到再執行生成邏輯。

我們繼續進入

proxyClassCache.get()

。這個方法看起來比較費勁,因為我使用的是JDK8,這邊用到了大量的Java8新增的函數式編程的語法和內容,因為這邊不是專門講Java8的,所以我就不展開函數式編程的內容了。以後有機會在其它專題詳述。另外,這邊會有很多對緩存的操作,這個不是我們的重點,所以也全部跳過,我們挑重點看,關注一下下面這部分代碼:

public V get(K key, P parameter){... //省略大量的緩存操作 while (true) { if (supplier != null) { V value = supplier.get(); if (value != null) { return value; ★ } } if (factory == null) { factory = new WeakCache.Factory(key, parameter, subKey, valuesMap); ▲ } if (supplier == null) { supplier = valuesMap.putIfAbsent(subKey, factory); if (supplier == null) { supplier = factory; } } else { if (valuesMap.replace(subKey, supplier, factory)) { supplier = factory; } else { supplier = valuesMap.get(subKey); } } }}

這個代碼非常有意思,是一個死循環。或許你和我一樣,完全看不懂這代碼是啥意思,沒關係,可以仔細觀察一下這代碼你就會發現柳暗花明。這個方法最後會需要返回一個從緩存或者新創建的代理類,而這整個死循環只有一個出口,沒錯就是帶★這一行,而value是通過

supplier.get()

獲得,Supplier是一個函數式接口,代表了一種數據的獲取操作。我們再觀察會發現,supplier是通過factory賦值而來的。而factory是通過▲行創建出來的。

WeakCache.Factory

恰好是Supplier的實現。所以我們進入

WeakCache.Factory

的get(),核心代碼如下,經觀察可以發現,返回的數據最終是通過valueFactory.apply()返回的。

public synchronized V get() {... //省略一些緩存操作 V value = null; value = Objects.requireNonNull(valueFactory.apply(key, parameter)); ... //省略一些緩存操作 return value;}

apply是BiFunction的一個抽象方法,BiFunction又是一個函數式接口。而valueFactory是通過WeakCache的構造器傳入,是一個ProxyClassFactory對象,而其剛好就是BiFunction的實現,顧名思義,這個類就是專門用來創建代理類的工廠類。

進入ProxyClassFactory的apply()方法,代碼如下:

Map<Class<?>, Boolean> interfaceSet = new IdentityHashMap<>(interfaces.length);//對每一個指定的Class校驗其是否能被指定的類加載器加載以及校驗是否是接口,動態代理只能對接口代理,至於原因,後面會說。 for (Class<?> intf : interfaces) { Class<?> interfaceClass = null; interfaceClass = Class.forName(intf.getName(), false, loader); if (interfaceClass != intf) { throw new IllegalArgumentException( intf + " is not visible from class loader"); } if (!interfaceClass.isInterface()) { throw new IllegalArgumentException( interfaceClass.getName() + " is not an interface"); } if (interfaceSet.put(interfaceClass, Boolean.TRUE) != null) { throw new IllegalArgumentException( "repeated interface: " + interfaceClass.getName()); } } //下面這一大段是用來指定生成的代理類的包信息 //如果全是public的,就是用默認的com.sun.proxy, //如果有非public的,所有的非public接口必須處於同一級別包下面,而該包路徑也會成為生成的代理類的包。 String proxyPkg = null; int accessFlags = Modifier.PUBLIC | Modifier.FINAL; for (Class<?> intf : interfaces) { int flags = intf.getModifiers(); if (!Modifier.isPublic(flags)) { accessFlags = Modifier.FINAL; String name = intf.getName(); int n = name.lastIndexOf('.'); String pkg = ((n == -1) ? "" : name.substring(0, n + 1)); if (proxyPkg == null) { proxyPkg = pkg; } else if (!pkg.equals(proxyPkg)) { throw new IllegalArgumentException( "non-public interfaces from different packages"); } } } if (proxyPkg == null) { proxyPkg = ReflectUtil.PROXY_PACKAGE + "."; } long num = nextUniqueNumber.getAndIncrement(); //代理類最後生成的名字是包名+$Proxy+一個數字 String proxyName = proxyPkg + proxyClassNamePrefix + num; //生成代理類的核心 byte[] proxyClassFile = ProxyGenerator.generateProxyClass( proxyName, interfaces, accessFlags);★ return defineClass0(loader, proxyName, proxyClassFile, 0, proxyClassFile.length); }

通過上面代碼不難發現,生成代理類的核心代碼在★這一行,會使用一個ProxyGenerator生成代理類(以byte[]形式存在)。然後將生成得到的字節數組轉換為一個Class對象。進入

ProxyGenerator.generateProxyClass()

。ProxyGenerator處於

sun.misc

包,不是開源的包,因為我這邊使用的是openjdk,所以可以直接查看其源碼,如果使用的是oracle jdk的話,這邊只能通過反編譯class文件查看。

public static byte[] generateProxyClass(final String name, Class<?>[] interfaces, int accessFlags) {ProxyGenerator gen = new ProxyGenerator(name, interfaces, accessFlags); final byte[] classFile = gen.generateClassFile(); if (saveGeneratedFiles) { //省略一堆IO操作 } return classFile; }

上述邏輯很簡單,就是使用一個生成器調用

generateClassFile()

方法返回代理類,後面有個if判斷我簡單提一下,這個作用主要是將內存中動態生成的代理類以class文件形式保存到硬碟。saveGeneratedFiles這個欄位是定義在ProxyGenerator中的欄位,

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

我簡單說一下,

AccessController.doPrivileged

這個玩意會去調用

java.security.PrivilegedAction

的run()方法,GetBooleanAction這個玩意就實現了java.security.PrivilegedAction,在其run()中會通過

Boolean.getBoolean()

從系統屬性中獲取

sun.misc.ProxyGenerator.saveGeneratedFiles

的值,默認是false,如果想要將動態生成的class文件持久化,可以往系統屬性中設置為true。

我們重點進入

ProxyGenerator.generateClassFile()

方法,代碼如下:

private byte[] generateClassFile() {addProxyMethod(hashCodeMethod, Object.class); addProxyMethod(equalsMethod, Object.class); addProxyMethod(toStringMethod, Object.class); for (Class<?> intf : interfaces) { for (Method m : intf.getMethods()) { addProxyMethod(m, intf); } } for (List<ProxyGenerator.ProxyMethod> sigmethods : proxyMethods.values()) { checkReturnTypes(sigmethods); } methods.add(generateConstructor()); for (List<ProxyGenerator.ProxyMethod> sigmethods : proxyMethods.values()) { for (ProxyGenerator.ProxyMethod pm : sigmethods) { fields.add(new ProxyGenerator.FieldInfo(pm.methodFieldName, "Ljava/lang/reflect/Method;", ACC_PRIVATE | ACC_STATIC)); methods.add(pm.generateMethod()); } } methods.add(generateStaticInitializer()); if (methods.size() > 65535) { throw new IllegalArgumentException("method limit exceeded"); } if (fields.size() > 65535) { throw new IllegalArgumentException("field limit exceeded"); } cp.getClass(dotToSlash(className)); cp.getClass(superclassName); for (Class<?> intf : interfaces) { cp.getClass(dotToSlash(intf.getName())); } cp.setReadOnly(); ByteArrayOutputStream bout = new ByteArrayOutputStream(); DataOutputStream dout = new DataOutputStream(bout); dout.writeInt(0xCAFEBABE); // u2 minor_version; dout.writeShort(CLASSFILE_MINOR_VERSION); // u2 major_version; dout.writeShort(CLASSFILE_MAJOR_VERSION); cp.write(dout); // (write constant pool) // u2 access_flags; dout.writeShort(accessFlags); // u2 this_class; dout.writeShort(cp.getClass(dotToSlash(className))); // u2 super_class; dout.writeShort(cp.getClass(superclassName)); // u2 interfaces_count; dout.writeShort(interfaces.length); // u2 interfaces[interfaces_count]; for (Class<?> intf : interfaces) { dout.writeShort(cp.getClass( dotToSlash(intf.getName()))); } // u2 fields_count; dout.writeShort(fields.size()); // field_info fields[fields_count]; for (ProxyGenerator.FieldInfo f : fields) { f.write(dout); } // u2 methods_count; dout.writeShort(methods.size()); // method_info methods[methods_count]; for (ProxyGenerator.MethodInfo m : methods) { m.write(dout); } // u2 attributes_count; dout.writeShort(0); return bout.toByteArray(); }

如果沒有學過Java虛擬機規範中關於字節碼文件結構的知識的話,上面這段代碼肯定是看得一頭霧水,因為本文主要是講解動態代理,加上個人對Java虛擬機的掌握也是菜鳥級別,所以下面就簡單闡述一下關於字節碼結構的內容以便大家理解上面這塊代碼,但是不展開詳說。

Class文件結構簡述

在Java虛擬機規範中,Class文件是一組二進位流,每個Class文件會對應一個類或者接口的定義信息,當然,Class文件並不是一定以文件形式存在於硬碟,也有可能直接由類加載器加載到內存。每一個Class文件加載到內存後,經過一系列的加載、連接、初始化過程,然後會在方法區中形成一個Class對象,作為外部訪問該類信息的的唯一入口。按照Java虛擬機規範,Class文件是具有非常嚴格嚴謹的結構規範,由一系列數據項組成,各個數組項之間沒有分隔符的結構緊湊排列。每個數據項會有相應的數據類型,如下表就是一個完整Class文件結構的表。

其中名稱一列就是組成Class文件的數據項,限於篇幅這邊就不展開詳細解釋每一項了,大家有興趣可以自己去查點資料了解一下,左邊是其類型,主要分兩類,像u2,u4這類是無符號數,分別表示2個字節和4個字節。以info結尾的是表結構,表結構又是一個複合類型,由其它的無符號數和其他的表結構組成。

我這邊以相對結構簡單的field_info結構舉個例子,field_info結構用來描述接口或者類中的變量。它的結構如下:

其它的表結構

method_info

,

attribute_info

也都是類似,都會有自己特有的一套結構規範。

好了,簡單了解一下

Class

文件結構後,現在再回到我們的主題來,我們再來研究

ProxyGenerator.generateClassFile()

方法內容就好理解了。其實這個方法就做了一件事情,就是根據我們傳入的這些個信息,再按照Java虛擬機規範的字節碼結構,用IO流的方式寫入到一個字節數組中,這個字節數組就是代理類的

Class

文件。默認情況這個Class文件直接存在內存中,為了更加深入理解動態代理原理,該是時候去看看這個文件到底是啥結構了。怎麼看?還記得前面提到過的

sun.misc.ProxyGenerator.saveGeneratedFiles

嗎?只要我們往系統屬性中加入該參數並將其值設為

true

,就會自動將該方法生成的byte[]形式的Class文件保存到硬碟上,如下代碼:

public class Client2 {public static void main(String[] args) { //加入該屬性並設置為true System.setProperty("sun.misc.ProxyGenerator.saveGeneratedFiles", "true"); Person landlord = new Landlord(); Person proxy = (Person) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), new Class[]{Person.class}, new RentHandler(landlord)); proxy.rent(); }}

再次運行,神奇的一幕發生了,工程中多了一個類,沒錯,這就是

JDK

動態代理生成的代理類,因為我們的接口是

public

修飾,所以採用默認包名

com.sun.proxy

,類名以

$Proxy

開頭,後面跟一個數字,和預期完全吻合。完美!

那麼就讓我們反編譯一下這個class文件看看它的內容來一探究竟......

下面是反編譯得到的代理類的內容,

public final class $Proxy0 extends Proxy implements Person { ★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 boolean equals(Object var1) throws { ④ return (Boolean) super.h.invoke(this, m1, new Object[]{var1}); } public final void rent() throws { ③ super.h.invoke(this, m3, (Object[]) null); } public final String toString() throws { ④ return (String) super.h.invoke(this, m2, (Object[]) null); } public final int hashCode() throws { ④ return (Integer) super.h.invoke(this, m0, (Object[]) null); } static { ① m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object")); m3 = Class.forName("com.dujc.mybatis.proxy.Person").getMethod("rent"); m2 = Class.forName("java.lang.Object").getMethod("toString"); m0 = Class.forName("java.lang.Object").getMethod("hashCode"); }}

有幾個關注點

標註①的是一個靜態代碼塊,當代理類一被加載,會立刻初始化,用反射方式獲取得到被代理的接口中方法和Objectequals(),toString(),hashCode()方法的Method對象,並將其保存在屬性中,為後續請求分派做準備。標註②的是帶有一個帶有InvocationHandler類型參數的構造器,這個也驗證了我們之前的猜測,沒錯,代理類會通過構造器接收一個InvocationHandler實例,再觀察標記★的地方,代理類繼承了Proxy類,其實代理類會通過調用父類構造器將其保存在Proxy的屬性h中,自然會繼承給當前這個代理類,這個InvocationHandler實例為後續請求分派做準備。同時由此我們也可以得出結論,Proxy是所有的代理類的父類。另外再延伸,因為Java是一門單繼承語言,所以意味著代理類不可能再通過繼承其他類的方式來擴展。所以,JDK動態代理沒法對不實現任何接口的類進行代理,原因就在於此。這或許也是動態代理模式不多的缺點之一。如果需要繼承形式的類代理,可以使用CGLIB等類庫。標註③的是我們指定接口Person中的方法,標註④的是代理類繼承自Object類中的equals(),toString(),hashCode()方法。再觀察這些方法內部實現,所有的方法請求全部委託給之前由構造器傳入的InvocationHandler實例的invoke()方法處理,將當前的代理類實例,各方法的Method對象和方法參數傳入,最後返回執行結果。由此得出結論,動態代理過程中,所指定接口的方法以及Objectequals(),toString(),hashCode()方法會被代理,而Object其他方法則並不會被代理,而且所有的方法請求全部都是委託給我們自己寫的自定義InvocationHandlerinvoke()方法統一處理,哇塞,O了,這樣的處理實在太優雅了!動態代理到底有什麼卵用

其實經過上面這一堆講解,動態代理模式中最核心的內容基本都分析完了,相信大家應該對其也有了一個本質的認知。學以致用,技術再牛逼如果沒法用在實際工作中也說實話也只能拿來裝逼了。那這個東西到底有什麼卵用呢?其實我以前學完動態代理模式後第一感覺是,嗯,這玩意確實挺牛逼的,但是到底有什麼用?沒有一點概念。在閱讀Spring或者Mybatis等經典開源框架中的代碼時,時不時也經常會發現動態代理模式的身影,但是還是沒有一個直接的感受。直到最近一段時間我在深入研究Mybatis源碼時,看到其日誌模塊的設計,內部就是使用了動態代理,忽然靈光一閃,大受啟發感覺一下子全想通了......這就是冥冥之中註定的吧?所以最後我就拿這個例子給大家講解一下動態代理模式的實際應用場景。

想必使用過

Mybatis

這一優秀持久層框架的人都注意到過,每當我們執行對資料庫操作,如果日誌級別是

DEBUG

,控制臺會列印出一些輔助信息,比如執行的

SQL

語句,綁定的參數和參數值,返回的結果等,你們有沒有想過這些信息到底是怎麼來的?

在Mybatis底層的日誌模塊中,有一塊專門用於列印JDBC相關信息日誌的功能。這塊功能是由一系列

xxxLogger

類構成。其中最頂層的是

BaseJdbcLogger

,他有4個子類,繼承關係如下圖:

看名字應該就能猜出來是幹啥了,以

ConnectionLogger

為例,下面是

ConnectionLogger

的關鍵代碼:

public final class ConnectionLogger extends BaseJdbcLogger implements InvocationHandler { private final Connection connection; private ConnectionLogger(Connection conn, Log statementLog, int queryStack) { super(statementLog, queryStack); this.connection = conn; } @Override public Object invoke(Object proxy, Method method, Object[] params) throws Throwable { if (Object.class.equals(method.getDeclaringClass())) { return method.invoke(this, params); } if ("prepareStatement".equals(method.getName())) { if (isDebugEnabled()) { debug(" Preparing: " + removeBreakingWhitespace((String) params[0]), true); } PreparedStatement stmt = (PreparedStatement) method.invoke(connection, params); stmt = PreparedStatementLogger.newInstance(stmt, statementLog, queryStack); return stmt; } else if ("prepareCall".equals(method.getName())) { if (isDebugEnabled()) { debug(" Preparing: " + removeBreakingWhitespace((String) params[0]), true); } PreparedStatement stmt = (PreparedStatement) method.invoke(connection, params); stmt = PreparedStatementLogger.newInstance(stmt, statementLog, queryStack); return stmt; } else if ("createStatement".equals(method.getName())) { Statement stmt = (Statement) method.invoke(connection, params); stmt = StatementLogger.newInstance(stmt, statementLog, queryStack); return stmt; } else { return method.invoke(connection, params); } } public static Connection newInstance(Connection conn, Log statementLog, int queryStack) { InvocationHandler handler = new ConnectionLogger(conn, statementLog, queryStack); ClassLoader cl = Connection.class.getClassLoader(); return (Connection) Proxy.newProxyInstance(cl, new Class[]{Connection.class}, handler); }}

怎麼樣?是不是有種熟悉的感覺?

觀察上面代碼,可以得出以下幾點結論:

ConnectionLogger實現了InvocationHandler,通過構造器傳入真實Connection對象,這是一個真實對象,並將其保存在屬性,後續請求會委託給它執行。其靜態方法newInstance()內部就是通過Proxy.newProxyInstance()並傳入類加載器等一系列參數返回一個Connection的代理對象給前端。該方法最終會在DEBUG日誌級別下被org.apache.ibatis.executor.BaseExecutor.getConnection()方法調用返回一個Connection代理對象。前面說過,JDK動態代理會將客戶端所有的請求全部派發給InvocationHandlerinvoke()方法,即上面ConnectionLogger中的invoke()方法。invoke()方法當中,不難發現,Mybatis對於Object中定義的方法,統一不做代理處理,直接調用返回。對於prepareStatement(),prepareCall(),createStatement()這三個核心方法會統一委託給真實的Connection對象處理,並且在執行之前會以DEBUG方式列印日誌信息。除了這三個方法,Connection其它方法也會被真實的Connection對象代理,但是並不會列印日誌信息。我們以prepareStatement()方法為例,當真實的Connection對象調用prepareStatement()方法會返回PreparedStatement對象,這又是一個真實對象,但是Mybatis並不會將該真實對象直接返回,而且通過調用PreparedStatementLogger.newInstance()再次包裝代理,看到這個方法名字,我相信聰明的您都能猜到這個方法的邏輯了。沒錯,PreparedStatementLogger類的套路和ConnectionLogger如出一轍。這邊我再貼回PreparedStatementLogger的代碼,public final class PreparedStatementLogger extends BaseJdbcLogger implements InvocationHandler { private final PreparedStatement statement; private PreparedStatementLogger(PreparedStatement stmt, Log statementLog, int queryStack) { super(statementLog, queryStack); this.statement = stmt; } @Override public Object invoke(Object proxy, Method method, Object[] params) throws Throwable { if (Object.class.equals(method.getDeclaringClass())) { return method.invoke(this, params); } if (EXECUTE_METHODS.contains(method.getName())) { if (isDebugEnabled()) { debug("Parameters: " + getParameterValueString(), true); } clearColumnInfo(); if ("executeQuery".equals(method.getName())) { ResultSet rs = (ResultSet) method.invoke(statement, params); return rs == null ? null : ResultSetLogger.newInstance(rs, statementLog, queryStack); } else { return method.invoke(statement, params); } } else if (SET_METHODS.contains(method.getName())) { if ("setNull".equals(method.getName())) { setColumn(params[0], null); } else { setColumn(params[0], params[1]); } return method.invoke(statement, params); } else if ("getResultSet".equals(method.getName())) { ResultSet rs = (ResultSet) method.invoke(statement, params); return rs == null ? null : ResultSetLogger.newInstance(rs, statementLog, queryStack); } else if ("getUpdateCount".equals(method.getName())) { int updateCount = (Integer) method.invoke(statement, params); if (updateCount != -1) { debug(" Updates: " + updateCount, false); } return updateCount; } else { return method.invoke(statement, params); } } public static PreparedStatement newInstance(PreparedStatement stmt, Log statementLog, int queryStack) { InvocationHandler handler = new PreparedStatementLogger(stmt, statementLog, queryStack); ClassLoader cl = PreparedStatement.class.getClassLoader(); return (PreparedStatement) Proxy.newProxyInstance(cl, new Class[]{PreparedStatement.class, CallableStatement.class}, handler); }}這個代碼的邏輯我就不講了,思路幾乎和ConnectionLogger完全一致。無非是攔截的方法不同,因為這次被代理對象是PreparedStatement,所以這次會去攔截都是PreparedStatement的方法,比如setXXX()系列,executeXX()系列等方法。然後在指定方法執行前後添加需要的DEBUG日誌信息,perfect!以getResultSet()方法為例,PreparedStatement對象調用getResultSet()後,會返回真實的ResultSet對象,但是一樣的套路,並不會直接將該真實對象返回,而是由調用ResultSetLogger.newInstance()再次將該ResultSet對象包裝,ResultSetLogger的代碼相信聰明的您不需要我再花篇幅講了。這個時候,再回過頭思考一下,這個場景下,如果是採用靜態代理是不是根本沒法完成了?因為,每一個資料庫連接都會產生一個新的Connection對象,而每一個Connection對象每次調用preparedStatement()方法都會產生一個新的PreparedStatement對象,而每一個PreparedStatement對象每次調用getResultSet()又都會產生一個新的ResultSet對象,跟上面的多個房東出租房子一個道理,就會產生不計其數處理邏輯極其相似的代理類,所以,這才是開源框架底層不採用靜態代理的本質原因!一切都豁然開朗了!結束

好了,關於JDK動態代理的核心原理部分到這裡算全部講解完畢了,其實我們聊了這麼多,都是圍繞著

java.lang.reflect.Proxy.newProxyInstance()

這個方法展開的。其實在Proxy類中,還有一個

getProxyClass()

方法,這個只需要傳入加載代理類的類加載器和指定接口就可以動態生成其代理類,我一開始說到靜態代理弊病的時候說過,靜態代理創建代理時,真實角色必須要存在,否則這個模式沒法進行下去,但是JDK動態代理可以做到在真實角色不存在的情況下就返回該接口的代理類。至於

Proxy

其它的方法都比較簡單了,此處不再贅述。

相關焦點

  • 「原創」讓設計模式飛一會兒|④原型模式
    今天我們繼續設計模式的探索之路。前幾篇的內容有小夥伴還沒有閱讀過的,可以閱讀一下。今天我們接下來要聊的是原型模式。何為原型?維基百科上給出的概念:原型是首創的模型,代表同一類型的人物、物件、或觀念。為什麼需要原型模式?我還是堅持前面幾篇一貫的風格,在深入了解該模式之前,先來思考一下,這個模式它出現的原因以及存在的意義是什麼?首先,這個模式也是屬於創建型模式,也是用來創建對象。
  • 讓設計模式飛一會兒|①開篇獲獎感言
    從今天開始我將正式開啟有關設計模式的系列文章的寫作,和大家一同來聊聊設計模式這個老生常談的玩意。關於設計模式的文章,書籍,多如牛毛,隨便百度、Google一下都能給你搜出不計其數關於講解設計模式的文章來。那為什麼我還要花費如此大的精力和時間,來寫這麼個耳熟能詳的東西呢?
  • 面試官:Mybatis 使用了哪些設計模式?
    本文轉載自【微信公眾號:java進階架構師,ID:java_jiagoushi】經微信公眾號授權轉載,如需轉載與原文作者聯繫雖然我們都知道有20多個設計模式,但是大多停留在概念層面,真實開發中很少遇到,Mybatis源碼中使用了大量的設計模式,閱讀源碼並觀察設計模式在其中的應用,能夠更深入的理解設計模式。
  • 詳解微商代理模式 | 三種核心代理模式揭秘
    所有的代理制度都是根據產品來設計的,不同的產品有不同的制度,不能完全照搬。層級微商代理制度比較容易理解,就是層級批發模式,跟傳統的渠道代理模式一模一樣;簡單點的理解就是傳統渠道代理模式+網際網路,就變為了層級代理微商。代表企業有思埠、魔能、蒙牛、伊利、光明、娃哈哈、王老吉的新零售項目,還有眾多的品牌微商項目。
  • 微商模式設計公司分享:微商頂層代理模式設計5大因素
    微商的模式是什麼?是下層通往上層的通道,是企業盈利的方法,是一套完整的體系。設計好這個模式對於微商來說意義重大,讓下層代理看到上升的空間,促進他們積極的引流拓客,讓上層代理能夠有效地打響個人旗幟,吸引更多人加入。
  • 重學Java 設計模式:實戰命令模式「模擬高檔餐廳八大菜系,小二點單...
    二、開發環境JDK 1.8Idea + Maven涉及工程三個,可以通過關注「公眾號」:bugstack蟲洞棧,回復源碼下載獲取(打開獲取的連結,找到序號18)三、命令模式介紹命令模式,圖片來自 refactoringguru.cn
  • 用「男頻爽文」模式打開《追光吧!哥哥》
    看到這裡,筆者恍然大悟,原來,它要走的是這種路線……男頻爽文的「開掛綜藝」國內沒有真正意義上的周播劇,所以,綜藝是唯一可以做到一邊播出、一邊跟進輿論、一邊優化改進的「真周播模式」。看不破這個點,所以,很多普通觀眾甚至業內專家,會認為「姐姐」可以做,但是「哥哥」不可行,然而,優酷、東方衛視與燦星之所以選擇聯合製作,必然是看得超前一步——當大部分的人看到的是,「哥哥們又『油膩』又懈怠,能有什麼好看?」之時;他們看到的是,這個既真實又不好的現狀,恰恰就是「男頻爽文模式」最好不過的起點。
  • 詳解微商代理模式
    那下面我重點講解下代理制度有哪些,是如何制定的?所有的代理制度都是根據產品來設計的,不同的產品有不同的制度,不能完全照搬。01 層級微商制度傳統渠道代理+網際網路=層級代理微商,2013年——至今。層級微商代理制度比較容易理解,就是層級批發模式,跟傳統的渠道代理模式一模一樣;簡單點的理解就是傳統渠道代理模式+網際網路,就變為了層級代理微商。
  • 「Thrill of the Fight 2」開發多人模式
    據外媒VRFocus報導,由美國獨立遊戲開發者Ian Fitz開發的VR拳擊遊戲「Thrill of the Fight」的正版續作「Thrill of the Fight 2」正在開發多人模式,近日該作開發商Sealost Interactive確認該多人模式將是「Thrill of the
  • 微信「深色模式」辣眼睛?這6大新功能才是真的香!
    還有一些同學,發朋友圈會別出心裁地配圖,在「深色模式」下則變成了「冒著傻氣」的九宮格。接下來我們來看看,深色模式下的排版都有哪些雷區,以及如何避免踩雷。1) 分割線首先,大部分公眾號的精心設計的分割線,在「深色模式」下都會有扎眼的白邊,影響讀者的閱讀節奏不說,還會顯得排版不專業。
  • 黑暗模式背後,有這些你不知道的「黑歷史」
    什麼時候開始,「黑暗模式」這樣一個 app 皮膚級的功能開始變成了系統級支持?黑暗模式究竟有什麼用,在黑暗模式流行前,我們不是也用的好好的?黑底才是計算機最早的「名門正統」實際上,在計算機誕生的早期有一段時間一直都是只有「黑暗模式」,或者說是深色界面。
  • 微信上線「深色模式」,QQ HD卻被蘋果下架
    文 | 考拉科技館 排版 | 考拉科技館原創文章,禁止轉載,違者必究!今天(3月22日),微信正式版迎來更新,而在更新結束後,蘋果用戶夢寐以求的「深色模式」也是正式上線。不少人應該知道,這次微信「深色模式」的推出還曾在前段時間引發熱議。在今年早些時候,國內有蘋果用戶曾向微信提議適配「深色模式」,可不曾想卻遭到了微信方面的拒絕。而在幾周前,伴隨著一封來自蘋果公司的「警告信」,微信方面卻選擇了妥協。
  • 代理模式的社交電商,為什麼難做?
    從傳統微商興起,到雲集成功上市,國內各種社交代理模式的電商平臺不斷湧現。但除了雲集和個別淘客代理APP以外,至今就再也沒看到其他真正有影響力的品牌了。那到底代理模式的社交電商,為什麼難做?以上這兩個項目一個有供應鏈優勢,一個有微商團隊長資源,作為社交代理模式的電商平臺起盤其實是極具競爭力的,但都在成立不足一年的時間裡就宣告失敗了。當然,在2019年—2020年嚴格意義上來說,名存實亡的社交代理模式的電商項目有很多。比如:360金融旗下的喜上街,對!就是周鴻禕的那個360。
  • 《碧藍幻想 Versus》RPG 模式評測:「RPG」化的格鬥動作體驗
    本作的遊戲類型之所以還有「RPG」的標籤,就是因為遊戲中包含了混合原作的養成要素和動作戰鬥要素的「RPG 模式」。本文就將會為大家帶來 RPG 模式的評測內容。平行世界的劇情RPG 模式的劇情描繪的是與《GBF》正傳不同的世界,也就是所謂的平行世界。
  • Beebees代理模式的優勢是什麼?
    Beebees代理商城系統都有哪些功能? 代理模式如何實現? 部落管家系統開發團隊beebees品牌在網際網路上開設了官方旗艦店beebees旗艦店,讓廣大網民在網上也能買到與beebees實體店同款的商品。
  • 「原創壁紙」挑戰全網最美壁紙,原創專屬設計,不要錯過
    挑戰全網最美頭像,原創專屬設計,不要錯過哈嘍!七七來給大家送頭像來啦!如何設計:請看到最後圖!本期為上期關注、留言忠實粉絲製作的姓名:「高展」「胡登楊」「莊大智」「楊海濤」「劉勇」「洪巖」「鄭」「王」「馮」「陳」「褚」「衛」「蔣」「沈」「韓」「楊」「朱」「秦」「尤」「江哲」「傅建輝」「杜學濤」「吳麗麗」「康剛軍」「白小白」「鄭偉」「郭子東」「戴浩然」「管德林」「康」「丁希峰」「唐」「嶽」…………
  • 《漫威復仇者》IGN 戰役模式評測:「單人」反被「多人」誤
    進入《漫威復仇者》主菜單,首個選項將啟動單人戰役「再度集結」,其次則是開啟後期內容的多人模式「復仇者計劃」—— 但後者會在開始前提醒玩家,多人模式有不少單人戰役的劇透,而且只有通關戰役才能完全解鎖本體附帶的 6 個可選英雄。若是你完全對服務型遊戲無感,也可以將《漫威復仇者》視為一個單人遊戲,玩玩戰役,忽略多人模式。
  • JAVA知識點:設計模式在Spring中的應用
    設計模式(design pattern)是對面向對象設計中反覆出現的問題的解決方案。這個術語是在1990年代由Erich Gamma等人從建築設計領域引入到計算機科學中來的。這個術語的含義還存有爭議。算法不是設計模式,因為算法致力於解決問題而非設計問題。設計模式通常描述了一組相互緊密作用的對象。
  • 佳明Forerunner 745「操場跑步」模式實際測試
    ,745有個比較大的亮點新功能,就是「操場跑步」模式。顧名思義,針對400米標準田徑跑道的運動模式。儘管此前佳明手錶跑步也可以自定義設置單圈長度、自動計圈,畢竟還是要繁瑣很多,「操場跑步」相當於精簡了操作,也特別優化了運動軌跡的準確性。 我們比較好奇的幾點:「操場跑步」的實際精準度如何、有無誤差?精準度的優化是否真實,還是僅僅讓軌跡顯得準確?
  • 面試被問spring的aop的機制,這樣答
    參加過面試的或者是準備要參加面試在刷面試題的小夥伴,肯定遇到過「spring的aop的機制是什麼?」這個問題。刷過面試題的小夥伴肯定要說了,aop的機制是代理啊。那代理又是怎麼回事呢?下面我來給大家說說這個代理是怎麼回事,怎麼就能實現aop了。先來說說代理這個詞,你都聽過哪些代理相關的詞呢?房產代理?保險代理?代理商?