2023年再不會動態代理,就要被淘汰了
- 👏作者簡介:大家好,我是愛敲程式碼的小黃,獨角獸企業的Java開發工程師,CSDN部落格專家,阿里雲專家博主
- 📕系列專欄:Java設計模式、Kafka從成神到昇仙、Spring從成神到昇仙系列
- 🔥如果感覺博主的文章還不錯的話,請👍三連支援👍一下博主哦
- 🍂博主正在努力完成2023計劃中:以夢為馬,揚帆起航,2023追夢人
- 📝聯絡方式:hls1793929520,和大家一起學習,一起進步👀
代理模式
一、引言
在 Spring
中,最重要的應該當屬 IOC
和 AOP
了,IOC
的原始碼流程還比較簡單,但 AOP
的流程就較為抽象了。
其中,AOP
中代理模式的重要性不言而喻,但對於沒了解過代理模式的人來說,痛苦至極
於是,我就去看了動態代理的實現,發現網上大多數文章講的都是不清不楚,甚至講了和沒講似的,讓我極其難受
本著咱們方向主打的就是原始碼,直接從從原始碼角度講述一下 代理模式
兄弟們繫好安全帶,準備發車!
注意:本文篇幅較長,請留出較長時間來閱讀
二、定義
代理模式的定義:由於某些原因需要給某物件提供一個代理以控制對該物件的訪問。這時,訪問物件不適合或者不能直接引用目標物件,代理物件作為訪問物件和目標物件之間的中介。
舉個生活中常見的例子:客戶想買房,房東有很多房,提供賣房服務,但房東不會帶客戶看房,於是客戶通過中介買房。
這時候對於房東來說,不直接和客戶溝通,而是交於中介進行代理
對於中介來說,她也會在原有的基礎上收取一定的中介費
三、靜態代理
我們建立 Landlord
介面如下:
java
public interface Landlord {
// 出租房子
void apartmentToRent();
}
建立其實現類 HangZhouLandlord
代表杭州房東出租房子
java
public class HangZhouLandlord implements Landlord {
@Override
public void apartmentToRent() {
System.out.println("杭州房東出租房子");
}
}
建立代理類 LandlordProxy
,代表中介服務
```java public class LandlordProxy {
public Landlord landlord;
public LandlordProxy(Landlord landlord) {
this.landlord = landlord;
}
public void apartmentToRent() {
apartmentToRentBefore();
landlord.apartmentToRent();
apartmentToRentAfter();
}
public void apartmentToRentBefore() {
System.out.println("出租房前,收取中介費");
}
public void apartmentToRentAfter() {
System.out.println("出租房後,簽訂合同");
}
} ```
建立最終測試:
```java public class JavaMain { public static void main(String[] args) { Landlord landlord = new HangZhouLandlord();
LandlordProxy proxy = new LandlordProxy(landlord);
// 從中介進行租房
proxy.apartmentToRent();
}
} ```
得出最終結果:
json
出租房前,收取中介費
杭州房東出租房子
出租房後,簽訂合同
通過上述 demo
我們大概瞭解代理模式是怎麼一回事
- 優點:
- 在不修改目標物件的功能前提下,能通過代理物件對目標功能擴充套件
- 缺點:
- 代理物件需要與目標物件實現一樣的介面,所以會有很多代理類,一旦介面增加方法,目標物件與代理物件都要維護
四、動態代理
動態代理利用了JDK API,動態地在記憶體中構建代理物件,從而實現對目標物件的代理功能,動態代理又被稱為JDK代理或介面代理。
靜態代理與動態代理的區別:
- 靜態代理在編譯時就已經實現了,編譯完成後代理類是一個實際的
class
檔案 - 動態代理是在執行時動態生成的,即編譯完成後沒有實際的
class
檔案,而是在執行時動態生成類位元組碼,並載入到JVM
中
1、JDK代理
程式碼如下:
```java public class ProxyFactory { // 目標方法 public Object target; public ProxyFactory(Object target) { this.target = target; }
public Object getProxyInstance() {
return Proxy.newProxyInstance(
// 目標物件的類載入器
target.getClass().getClassLoader(),
// 目標物件的介面型別
target.getClass().getInterfaces(),
// 事件處理器
new InvocationHandler() {
/**
*
* @param proxy 代理物件
* @param method 代理物件呼叫的方法
* @param args 代理物件呼叫方法時實際的引數
* @return
* @throws Throwable
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("我是前置增強");
method.invoke(target, args);
System.out.println("我是後置增強");
return null;
}
}
);
}
} ```
我們測試一下:
```java public class JavaMain { public static void main(String[] args) { Landlord landlord = new HangZhouLandlord();
System.out.println(landlord.getClass());
Landlord proxy = (Landlord) new ProxyFactory(landlord).getProxyInstance();
proxy.apartmentToRent();
System.out.println(proxy.getClass());
while (true){}
}
} ```
得出結果:
json
class com.company.proxy.HangZhouLandlord
我是前置增強
杭州房東出租房子
我是後置增強
class com.sun.proxy.$Proxy0
這裡可能有小夥伴已經懵了,接著往後看
1.1 JDK類的動態生成
Java虛擬機器類載入過程主要分為五個階段:載入、驗證、準備、解析、初始化。其中載入階段需要完成以下3件事情:
- 通過一個類的全限定名來獲取定義此類的二進位制位元組流
- 將這個位元組流所代表的靜態儲存結構轉化為方法區的執行時資料結構
- 在記憶體中生成一個代表這個類的
java.lang.Class
物件,作為方法區這個類的各種資料訪問入口
由於虛擬機器規範對這3點要求並不具體,所以實際的實現是非常靈活的,關於第1點,獲取類的二進位制位元組流(class位元組碼)就有很多途徑:
- 從本地獲取
-
從網路中獲取
-
執行時計算生成,這種場景使用最多的是動態代理技術,在
java.lang.reflect.Proxy
類中,就是用了ProxyGenerator.generateProxyClass
來為特定介面生成形式為*$Proxy
的代理類的二進位制位元組流所以,動態代理就是想辦法,根據介面或目標物件,計算出代理類的位元組碼,然後再載入到
JVM
中使用
1.2 JDK動態代理流程
所以,我們可以得出一個結論:我們上面的 $Proxy0
實際上是 JVM
在編譯時期加載出來的類,由於這個類是編譯時期載入的,所以我們沒辦法在 IDEA
裡面看到。
可能一般的文章,到這裡基本就結束了,讓大家知道 $Proxy0
是由 JVM
編譯時期加載出來的類
但大家都知道,小黃的文章主打的就是一個硬核、原始碼級。所以,我們直接去看 $Proxy0
的原始碼
首先,我們需要下載一個 arthas
的產品,網址:https://arthas.aliyun.com/doc/,跟隨流程解壓即可。
Arthas 是一款線上監控診斷產品,通過全域性視角實時檢視應用 load、記憶體、gc、執行緒的狀態資訊,並能在不修改應用程式碼的情況下,對業務問題進行診斷,包括檢視方法呼叫的出入參、異常,監測方法執行耗時,類載入資訊等,大大提升線上問題排查效率。
當我們一切準備完成後,啟動我們上面動態代理的測試 JavaMain
類
啟動完成後,進入我們的 arthas
頁面,執行命令:java -jar arthas-boot.jar
我們可以看到,我們的目標類 com.company.proxy.JavaMain
就出現了,隨後我們按下 4
,進入到我們的監控頁面。
隨後使用 jad com.sun.proxy.$Proxy0
之後,可以看到我們已經解析出來 $Proxy0
的原始碼了
我們將其複製到下面,並刪減一些不必要的資訊。
```java public final class $Proxy0 extends Proxy implements Landlord { private static Method m3;
// $Proxy0 類的構造方法
// 引數為 invocationHandler
public $Proxy0(InvocationHandler invocationHandler) {
super(invocationHandler);
}
static {
m3 = Class.forName("com.company.proxy.Landlord").getMethod("apartmentToRent", new Class[0]);
}
public final void apartmentToRent() {
this.h.invoke(this, m3, null);
return;
}
}
``
我們先看其有參構造方法,可以看到
$Proxy0的構造方法入參為
InvocationHandler`,有沒有感覺似曾相識。
如果你這裡忘掉了,不妨去看一下動態代理的 ProxyFactory
的程式碼,可以發現,我們 Proxy.newProxyInstance()
的第三個自定義的引數,也正是我們的 InvocationHandler
。
我們猜測一下,如果這裡的傳的 InvocationHandler
是我們之前自定義的 InvocationHandler
那麼,如果我呼叫 $Proxy0.apartmentToRent()
是不是就是執行下面的程式碼:
```java public final void apartmentToRent() { this.h.invoke(this, m3, null); return; }
// 這裡的h.invoke執行的是我們這裡自定義的方法,然後進行的前後增強 public Object getProxyInstance() { return Proxy.newProxyInstance( target.getClass().getClassLoader(), target.getClass().getInterfaces(), new InvocationHandler() { @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("我是前置增強"); method.invoke(target, args); System.out.println("我是後置增強"); return null; } } ); ```
如果說我們這個猜測是正確的話,那麼會得出這樣的幾個結論:
- 我們的代理類實際上是實現了
Landlord
的介面,然後重寫了Landlord
介面中的apartmentToRent
方法 - 當外界呼叫代理類的
apartmentToRent()
方法時,實際上是呼叫的我們自定義的new InvocationHandler()
類裡面的invoke
方法
還有我們的最後一步,也就是證明 $Proxy0
的構造入參 InvocationHandler
就是我們自定義的 InvocationHandler
,廢話不多說,直接來看代理的原始碼。
java
return Proxy.newProxyInstance(ClassLoader,Interfaces,new InvocationHandler() {});
public static Object newProxyInstance(ClassLoader loader,Class<?>[] interfaces,InvocationHandler h){
// cl = class com.sun.proxy.$Proxy0
Class<?> cl = getProxyClass0(loader, intfs);
// cons = public com.sun.proxy.$Proxy0(java.lang.reflect.InvocationHandler)
final Constructor<?> cons = cl.getConstructor(constructorParams);
// 根據構造引數例項化物件
return cons.newInstance(new Object[]{h});
}
我們通過原始碼可以看到,一共分為三步(下面為反射的內容,如不熟悉可提前學習下反射):
- 拿到
$Proxy0
的Class
- 根據
Class
拿到其構造方法 - 根據構造方法傳入引數進行例項化
這就確定了我們上述的猜想是正確的。
2、Cglib代理
cglib (Code Generation Library ) 是一個第三方程式碼生成類庫,執行時在記憶體中動態生成一個子類物件從而實現對目標物件功能的擴充套件。cglib
為沒有實現介面的類提供代理,為 JDK
的動態代理提供了很好的補充。
- 最底層是位元組碼
ASM
是操作位元組碼的工具cglib
基於ASM
位元組碼工具操作位元組碼(即動態生成代理,對方法進行增強)SpringAOP
基於cglib
進行封裝,實現cglib
方式的動態代理
使用 cglib
需要引入 cglib
的jar包,如果你已經有 spring-core
的jar包,則無需引入,因為 spring
中包含了cglib
。
cglib
的Maven座標
xml
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>3.2.5</version>
</dependency>
2.1 cglib動態代理實現
還是同樣的配方,我們要建立一個需要代理的類(UserServiceImpl),但不需要實現任何的介面,因為我們的 cglib
是根據類來進行建立的。
UserServiceImpl
java
public class UserServiceImpl {
// 查詢功能
List<String> findUserList() {
return Collections.singletonList("小A");
}
}
實現 cglib
的工廠類:UserLogProxy
```java public class UserLogProxy implements MethodInterceptor { /* * 生成 CGLIB 動態代理類方法 * * @param target * @return / public Object getLogProxy(Object target) { // 增強器類,用來建立動態代理類 Enhancer enhancer = new Enhancer();
// 設定代理類的父類位元組碼物件
enhancer.setSuperclass(target.getClass());
// 設定回撥
enhancer.setCallback(this);
// 建立動態代理物件並返回
return enhancer.create();
}
/**
* @param o 代理物件
* @param method 目標物件中的方法的Method例項
* @param objects 實際引數
* @param methodProxy 代理類物件中的方法的Method例項
* @return
* @throws Throwable
*/
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
System.out.println("前置輸出");
Object result = methodProxy.invokeSuper(o, objects);
return result;
}
} ```
測試程式:JavaMainTest
```java public class JavaMainTest { public static void main(String[] args) {
// 目標物件
UserServiceImpl userService = new UserServiceImpl();
System.out.println(userService.getClass());
// 代理物件
UserServiceImpl proxy = (UserServiceImpl) new UserLogProxy().getLogProxy(userService);
System.out.println(proxy.getClass());
List<String> list = proxy.findUserList();
System.out.println("使用者資訊:" + list);
while (true) {
}
}
} ```
結果:
java
class com.study.spring.proxy.UserServiceImpl
class com.study.spring.proxy.UserServiceImpl$$EnhancerByCGLIB$$cd9788d
前置輸出
使用者資訊:[小A]
2.2 cglib代理流程
按照上述我們分析 $Proxy0
的方法,將 com.study.spring.proxy.UserServiceImpl$$EnhancerByCGLIB$$cd9788d
取出,得到如下:
java
public class UserServiceImpl$$EnhancerByCGLIB$$cd9788d extends UserServiceImpl implements Factory {
final List findUserList() {
// 是否設定了回撥
MethodInterceptor methodInterceptor = this.CGLIB$CALLBACK_0;
if (methodInterceptor == null) {
UserServiceImpl$$EnhancerByCGLIB$$cd9788d.CGLIB$BIND_CALLBACKS(this);
methodInterceptor = this.CGLIB$CALLBACK_0;
}
// 設定回撥,需要呼叫 intercept 方法
if (methodInterceptor != null) {
return (List) methodInterceptor.intercept(this, CGLIB$findUserList$0$Method, CGLIB$emptyArgs, CGLIB$findUserList$0$Proxy);
}
// 無回撥,呼叫父類的 findUserList 即可
return super.findUserList();
}
final List CGLIB$findUserList$0() {
return super.findUserList();
}
}
博主先把整個流程圖放到下面,然後結合流程圖來進行講解:
- 在
JVM
編譯期間,我們的 Enhancer
會根據目標類的資訊去動態的生成 動態代理類
並設定 回撥
- 當用戶在通過上述的動態代理類執行 findUserList()
方法時,有兩個執行選項
- 若設定了回撥介面,則直接呼叫UserLogProxy
中的 intercept
,然後通過 FastClass
類呼叫動態代理類,執行CGLIB$findUserList$0
方法,呼叫父類的 findUserList()
方法
- 若沒有設定回撥介面,則直接呼叫父類的 findUserList()
方法
五、代理模式總結
1、三種代理模式實現方式的對比
jdk
代理和CGLIB
代理-
使用
CGLib
實現動態代理,CGLib
底層採用ASM
位元組碼生成框架,使用位元組碼技術生成代理類,在JDK1.6
之前比使用Java
反射效率要高。唯一需要注意的是,CGLib
不能對宣告為final
的類或者方法進行代理,因為CGLib
原理是動態生成被代理類的子類。 -
在
JDK1.6
、JDK1.7
、JDK1.8
逐步對JDK
動態代理優化之後,在呼叫次數較少的情況下,JDK
代理效率高於CGLib
代理效率,只有當進行大量呼叫的時候,JDK1.6
和JDK1.7
比CGLib
代理效率低一點,但是到JDK1.8
的時候,JDK
代理效率高於CGLib
代理。所以如果有介面使用JDK
動態代理,如果沒有介面使用CGLIB
代理。 -
動態代理和靜態代理
- 動態代理與靜態代理相比較,最大的好處是介面中宣告的所有方法都被轉移到呼叫處理器一個集中的方法中處理(InvocationHandler.invoke)。這樣,在介面方法數量比較多的時候,我們可以進行靈活處理,而不需要像靜態代理那樣每一個方法進行中轉。
- 如果介面增加一個方法,靜態代理模式除了所有實現類需要實現這個方法外,所有代理類也需要實現此方法。增加了程式碼維護的複雜度。而動態代理不會出現該問題
2、代理模式優缺點
優點:
- 代理模式在客戶端與目標物件之間起到一箇中介作用和保護目標物件的作用;
- 代理物件可以擴充套件目標物件的功能;
- 代理模式能將客戶端與目標物件分離,在一定程度上降低了系統的耦合度;
缺點:
- 增加了系統的複雜度;
3、代理模式使用場景
- 功能增強
-
當需要對一個物件的訪問提供一些額外操作時,可以使用代理模式
-
遠端(Remote)代理
-
實際上,RPC 框架也可以看作一種代理模式,GoF 的《設計模式》一書中把它稱作遠端代理。通過遠端代理,將網路通訊、資料編解碼等細節隱藏起來。客戶端在使用 RPC 服務的時候,就像使用本地函式一樣,無需瞭解跟伺服器互動的細節。除此之外,RPC 服務的開發者也只需要開發業務邏輯,就像開發本地使用的函式一樣,不需要關注跟客戶端的互動細節。
-
防火牆(Firewall)代理
-
當你將瀏覽器配置成使用代理功能時,防火牆就將你的瀏覽器的請求轉給網際網路;當網際網路返回響應時,代理伺服器再把它轉給你的瀏覽器。
-
保護(Protect or Access)代理
- 控制對一個物件的訪問,如果需要,可以給不同的使用者提供不同級別的使用許可權。
六、結尾
終於寫完了這篇文章,動態代理在我看 AOP
原始碼時,就感覺挺抽象的
我感覺最大的原因應該在於:代理類動態生成,無法檢視,導致對其模糊,從而陷入不理解
但通過這篇文章,我相信,99%
的人應該都可以理解了動態代理模式的來龍去脈
當然,好刀要用在刀刃上,在面試中,若面試官提及 設計模式
、動態代理
、Spring
、Dubbo
都可以引出動態代理,基本這篇文章無差別秒殺
如果你能看到這,那博主必須要給你一個大大的鼓勵,謝謝你的支援!
喜歡的可以點個關注,後續會更新 Spring
原始碼系列文章
我是愛敲程式碼的小黃,獨角獸企業的Java開發工程師,CSDN部落格專家,Java領域新星創作者,喜歡後端架構和中介軟體原始碼。
我們下期再見。