1. 程式人生 > >從原始碼角度學習Java動態代理

從原始碼角度學習Java動態代理

[公眾號文章連結:https://mp.weixin.qq.com/s/jaLvb21yVHg2R_gJ-JSeVA](https://mp.weixin.qq.com/s/jaLvb21yVHg2R_gJ-JSeVA) # 前言 最近,看了一下關於RMI(Remote Method Invocation)相關的知識,遇到了一個動態代理的問題,然後就決定探究一下動態代理。 這裡先科普一下RMI。 # RMI 像我們平時寫的程式,物件之間互相呼叫方法都是在同一個JVM中進行,而RMI可以實現一個JVM上的物件呼叫另一個JVM上物件的方法,即遠端呼叫。 ## 介面定義 定義一個遠端物件介面,實現Remote介面來進行標記。 ```java public interface UserInterface extends Remote { void sayHello() throws RemoteException; } ``` ## 遠端物件定義 定義一個遠端物件類,繼承UnicastRemoteObject來實現Serializable和Remote介面,並實現介面方法。 ```java public class User extends UnicastRemoteObject implements UserInterface { public User() throws RemoteException {} @Override public void sayHello() { System.out.println("Hello World"); } } ``` ## 服務端 啟動服務端,將user物件在登錄檔上進行註冊。 ```java public class RmiServer { public static void main(String[] args) throws RemoteException, AlreadyBoundException, MalformedURLException { User user = new User(); LocateRegistry.createRegistry(8888); Naming.bind("rmi://127.0.0.1:8888/user", user); System.out.println("rmi server is starting..."); } } ``` 啟動服務端: ![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20201129174603889.png) ## 客戶端 從服務端登錄檔獲取遠端物件,在服務端呼叫sayHello()方法。 ```java public class RmiClient { public static void main(String[] args) throws RemoteException, NotBoundException, MalformedURLException { UserInterface user = (UserInterface) Naming.lookup("rmi://127.0.0.1:8888/user"); user.sayHello(); } } ``` 服務端執行結果: ![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20201129175133393.png) 至此,一個簡單的RMI demo完成。 # 動態代理 ## 提出問題 看了看RMI程式碼,覺得UserInterface這個介面有點多餘,如果客戶端使用Naming.lookup()獲取的物件不強轉成UserInterface,直接強轉成User是不是也可以,於是試了一下,就報了以下錯誤: ![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20201129182736225.png) 似曾相識又有點陌生的$Proxy0,翻了翻塵封的筆記找到了是動態代理的知識點,寥寥幾筆帶過,所以決定梳理一下動態代理,重新整理一份筆記。 ## 動態代理Demo ### 介面定義 ```java public interface UserInterface { void sayHello(); } ``` ### 真實角色定義 ```java public class User implements UserInterface { @Override public void sayHello() { System.out.println("Hello World"); } } ``` ### 呼叫處理類定義 代理類呼叫真實角色的方法時,其實是呼叫與真實角色繫結的處理類物件的invoke()方法,而invoke()呼叫的是真實角色的方法。 這裡需要實現 *InvocationHandler* 介面以及invoke()方法。 ```java public class UserHandler implements InvocationHandler { private User user; public UserProxy(User user) { this.user = user; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("invoking start...."); method.invoke(user); System.out.println("invoking stop...."); return user; } } ``` ### 執行類 ```java public class Main { public static void main(String[] args) { User user = new User(); // 處理類和真實角色繫結 UserHandler userHandler = new UserHandler(user); // 開啟將代理類class檔案儲存到本地模式,平時可以省略 System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true"); // 動態代理生成代理物件$Proxy0 Object o = Proxy.newProxyInstance(Main.class.getClassLoader(), new Class[]{UserInterface.class}, userHandler); // 呼叫的其實是invoke() ((UserInterface)o).sayHello(); } ``` 執行結果: ![動態代理執行結果](https://img-blog.csdnimg.cn/20201203170810771.png) 這樣動態代理的基本用法就學完了,可是還有好多問題不明白。 1. 動態代理是怎麼呼叫的invoke()方法? 2. 處理類UserHandler有什麼作用? 3. 為什麼要將類載入器和介面類陣列當作引數傳入newProxyInstance? *假如讓你去實現動態代理,你有什麼設計思路?* ## 猜想 動態代理,是不是和靜態代理,即設計模式的代理模式有相同之處呢? 簡單捋一捋代理模式實現原理:真實角色和代理角色共同實現一個介面並實現抽象方法A,代理類持有真實角色物件,代理類在A方法中呼叫真實角色物件的A方法。在Main中例項化代理物件,呼叫其A方法,間接呼叫了真實角色的A方法。 **實現程式碼** ```java // 介面和真實角色物件就用上面程式碼 // 代理類,實現UserInterface介面 public class UserProxy implements UserInterface { // 持有真實角色物件 private User user = new User(); @Override public void sayHello() { System.out.println("invoking start...."); // 在代理物件的sayHello()裡呼叫真實角色的sayHello() user.sayHello(); System.out.println("invoking stop...."); } } // 執行類 public class Main { public static void main(String[] args) { // 例項化代理角色物件 UserInterface userProxy = new UserProxy(); // 呼叫了代理物件的sayHello(),其實是呼叫了真實角色的sayHello() userProxy.sayHello(); } ``` 拿開始的動態代理程式碼和靜態代理比較,介面、真實角色都有了,區別就是多了一個UserHandler處理類,少了一個UserProxy代理類。 接著對比一下兩者的處理類和代理類,發現UserHandler的invoke()和UserProxy的sayHello()這兩個方法的程式碼都是一樣的。那麼,是不是新建一個UserProxy類,然後實現UserInterface介面並持有UserHandler的物件,在sayHello()方法中呼叫UserHandler的invoke()方法,就可以動態代理了。 **程式碼大概就是這樣的** ```java // 猜想的代理類結構,動態代理生成的代理是com.sun.proxy.$Proxy0 public class UserProxy implements UserInterface{ // 持有處理類的物件 private InvocationHandler handler; public UserProxy(InvocationHandler handler) { this.handler = handler; } // 實現sayHello()方法,並呼叫invoke() @Override public void sayHello() { try { handler.invoke(this, UserInterface.class.getMethod("sayHello"), null); } catch (Throwable throwable) { throwable.printStackTrace(); } } } // 執行類 public static void main(String[] args) { User user = new User(); UserHandler userHandler = new UserHandler(user); UserProxy proxy = new UserProxy(userHandler); proxy.sayHello(); } ``` 輸出結果: ![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20201206220326655.png) 上面的代理類程式碼是寫死的,而動態代理是當你呼叫Proxy.newProxyInstance()時,會根據你傳入的引數來動態生成這個代理類程式碼,如果讓我實現,會是以下這個流程。 1. 根據你傳入的Class[]介面陣列,代理類會來實現這些介面及其方法(這裡就是sayHello()),並且持有你傳入的userHandler物件,使用檔案流將預先設定的包名、類名、方法名等一行行程式碼寫到本地磁碟,生成\$Proxy0.java檔案 2. 使用編譯器將$Proxy0.java編譯成\$Proxy0.class 3. 根據你傳入的ClassLoader將\$Proxy0.class載入到JMV中 4. 呼叫Proxy.newProxyInstance()就會返回一個$Proxy0的物件,然後呼叫sayHello(),就執行了裡面userHandler的invoke() 以上就是對動態代理的一個猜想過程,下面就通過debug看看原始碼是怎麼實現的。
# 在困惑的日子裡學會擁抱原始碼 ![擁抱原始碼](https://img-blog.csdnimg.cn/20201203214119167.jpg) ## 呼叫流程圖 這裡先用PPT畫一個流程圖,可以跟著流程圖來看後面的原始碼。 ![流程圖](https://img-blog.csdnimg.cn/20201207144730337.jpg) **從newProxyInstance()設定斷點** ![main](https://img-blog.csdnimg.cn/20201206222446406.png) ## newProxyInstance() newProxyInstance()程式碼分為上下兩部分,上部分是獲取類$Proxy0.class,下部分是通過反射構建\$Proxy0物件。 **上部分程式碼** ![newProxyInstance()](https://img-blog.csdnimg.cn/20201206223542698.png) 從名字看就知道getProxyClass0()是核心方法,step into ### getProxyClass0() ![getProxyClass()](https://img-blog.csdnimg.cn/20201206223854355.png) 裡面呼叫了WeakCache物件的get()方法,這裡暫停一下debug,先講講WeakCache類。 # WeakCache 顧名思義,它是一個弱引用快取。那什麼是是弱引用呢,是不是還有強引用呢? ## 弱引用 WeakReference就是弱引用類,作為包裝類來包裝其他物件,在進行GC時,其中的包裝物件會被回收,而WeakReference物件會被放到引用佇列中。 舉個栗子: ```java // 這就是強引用,只要不寫str1 = null,str1指向的這個字串不就會被垃圾回收 String str1 = new String("hello"); ReferenceQueue referenceQueue = new ReferenceQueue(); // 只要垃圾回收,這個str2裡面包裝的物件就會被回收,但是這個弱引用物件不會被回收,即word會被回收,但是str2指向的弱引用物件不會 // 每個弱引用關聯一個ReferenceQueue,當包裝的物件被回收,這個弱引用物件會被放入引用佇列中 WeakR