2023年再不會動態代理,就要被淘汰了

語言: CN / TW / HK
  • 👏作者簡介:大家好,我是愛敲程式碼的小黃,獨角獸企業的Java開發工程師,CSDN部落格專家,阿里雲專家博主
  • 📕系列專欄:Java設計模式、Kafka從成神到昇仙、Spring從成神到昇仙系列
  • 🔥如果感覺博主的文章還不錯的話,請👍三連支援👍一下博主哦
  • 🍂博主正在努力完成2023計劃中:以夢為馬,揚帆起航,2023追夢人
  • 📝聯絡方式:hls1793929520,和大家一起學習,一起進步👀

代理模式

一、引言

Spring 中,最重要的應該當屬 IOCAOP 了,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件事情:

  1. 通過一個類的全限定名來獲取定義此類的二進位制位元組流
  2. 將這個位元組流所代表的靜態儲存結構轉化為方法區的執行時資料結構
  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 的產品,網址:http://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}); }

我們通過原始碼可以看到,一共分為三步(下面為反射的內容,如不熟悉可提前學習下反射):

  • 拿到 $Proxy0Class
  • 根據 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.6JDK1.7JDK1.8 逐步對 JDK 動態代理優化之後,在呼叫次數較少的情況下,JDK 代理效率高於 CGLib 代理效率,只有當進行大量呼叫的時候,JDK1.6JDK1.7CGLib 代理效率低一點,但是到 JDK1.8 的時候,JDK 代理效率高於 CGLib 代理。所以如果有介面使用 JDK 動態代理,如果沒有介面使用 CGLIB 代理。

  • 動態代理和靜態代理

  • 動態代理與靜態代理相比較,最大的好處是介面中宣告的所有方法都被轉移到呼叫處理器一個集中的方法中處理(InvocationHandler.invoke)。這樣,在介面方法數量比較多的時候,我們可以進行靈活處理,而不需要像靜態代理那樣每一個方法進行中轉。
  • 如果介面增加一個方法,靜態代理模式除了所有實現類需要實現這個方法外,所有代理類也需要實現此方法。增加了程式碼維護的複雜度。而動態代理不會出現該問題

2、代理模式優缺點

優點:

  • 代理模式在客戶端與目標物件之間起到一箇中介作用和保護目標物件的作用;
  • 代理物件可以擴充套件目標物件的功能;
  • 代理模式能將客戶端與目標物件分離,在一定程度上降低了系統的耦合度;

缺點:

  • 增加了系統的複雜度;

3、代理模式使用場景

  • 功能增強
  • 當需要對一個物件的訪問提供一些額外操作時,可以使用代理模式

  • 遠端(Remote)代理

  • 實際上,RPC 框架也可以看作一種代理模式,GoF 的《設計模式》一書中把它稱作遠端代理。通過遠端代理,將網路通訊、資料編解碼等細節隱藏起來。客戶端在使用 RPC 服務的時候,就像使用本地函式一樣,無需瞭解跟伺服器互動的細節。除此之外,RPC 服務的開發者也只需要開發業務邏輯,就像開發本地使用的函式一樣,不需要關注跟客戶端的互動細節。

  • 防火牆(Firewall)代理

  • 當你將瀏覽器配置成使用代理功能時,防火牆就將你的瀏覽器的請求轉給網際網路;當網際網路返回響應時,代理伺服器再把它轉給你的瀏覽器。

  • 保護(Protect or Access)代理

    • 控制對一個物件的訪問,如果需要,可以給不同的使用者提供不同級別的使用許可權。

六、結尾

終於寫完了這篇文章,動態代理在我看 AOP 原始碼時,就感覺挺抽象的

我感覺最大的原因應該在於:代理類動態生成,無法檢視,導致對其模糊,從而陷入不理解

但通過這篇文章,我相信,99% 的人應該都可以理解了動態代理模式的來龍去脈

當然,好刀要用在刀刃上,在面試中,若面試官提及 設計模式動態代理SpringDubbo 都可以引出動態代理,基本這篇文章無差別秒殺

如果你能看到這,那博主必須要給你一個大大的鼓勵,謝謝你的支援!

喜歡的可以點個關注,後續會更新 Spring 原始碼系列文章

我是愛敲程式碼的小黃,獨角獸企業的Java開發工程師,CSDN部落格專家,Java領域新星創作者,喜歡後端架構和中介軟體原始碼。

我們下期再見。