從頭捋了一遍 Java 代理機制,收穫頗豐

飛天小牛肉發表於2021-03-01

? 盡人事,聽天命。博主東南大學碩士在讀,熱愛健身和籃球,樂於分享技術相關的所見所得,關注公眾號 @ 飛天小牛肉,第一時間獲取文章更新,成長的路上我們一起進步

? 本文已收錄於 「CS-Wiki」Gitee 官方推薦專案,現已累計 1.5k+ star,致力打造完善的後端知識體系,在技術的路上少走彎路,歡迎各位小夥伴前來交流學習

? 如果各位小夥伴春招秋招沒有拿得出手的專案的話,可以參考我寫的一個專案「開源社群系統 Echo」Gitee 官方推薦專案,目前已累計 330+ star,基於 SpringBoot + MyBatis + MySQL + Redis + Kafka + Elasticsearch + Spring Security + ... 並提供詳細的開發文件和配套教程。公眾號後臺回覆 Echo 可以獲取配套教程,目前尚在更新中

 

前文提到,動態代理機制使用了反射,Spring 中的 AOP 由於使用了動態代理,所以也相當於使用了反射機制。那麼,代理是什麼?動態代理又是什麼?動態代理中是如何使用反射的?全文脈絡思維導圖如下:

從頭捋了一遍 Java 代理機制,收穫頗豐

1. 常規編碼方式

在學習代理之前,先回顧以下我們的常規編碼方式:所有 interface 型別的變數總是通過向上轉型並指向某個例項的。

1)首先,定義一個介面:

1 public interface SmsService {
2     String send(String message);
3 }

 

2)然後編寫其實現類:

1 public class SmsServicseImpl implements SmsService {
2     public String send(String message) {
3         System.out.println("send message:" + message);
4         return message;
5     }
6 }

 

3)最後建立該實現類的例項,轉型為介面並呼叫:

SmsService s = new SmsServicseImpl();
s.send("Java");

 

上述這種方式就是我們通常編寫程式碼的方式。而代理模式和這種方式有很大的區別,且看下文。

2. 代理模式概述

簡單來說,代理模式就是 使用代理物件來代替對真實物件的訪問,這樣就可以在不修改原目標物件的前提下,提供額外的功能操作,擴充套件目標物件的功能。

代理模式大致有三種角色:

  • Real Subject:真實類,也就是被代理類、委託類。用來真正完成業務服務功能;

  • Proxy:代理類。將自身的請求用 Real Subject 對應的功能來實現,代理類物件並不真正的去實現其業務功能;

  • Subject:定義 RealSubject 和 Proxy 角色都應該實現的介面。

從頭捋了一遍 Java 代理機制,收穫頗豐

通俗來說,代理模式的主要作用是擴充套件目標物件的功能,比如說在目標物件的某個方法執行前後你可以增加一些額外的操作,並且不用修改這個方法的原有程式碼。如果大家學過 Spring 的 AOP,一定能夠很好的理解這句話。

舉個例子:你找了小紅來幫你向小綠問話,小紅就看作是代理我的代理類 Proxy,而你是 Real Subject,因為小紅要傳達的話其實是你說的。那麼你和小紅都需要實現的介面(Subject)就是說話,由於你倆都能說話,在外界看來你倆就是一樣的(滑稽,大家理解就好,不用較真)

從頭捋了一遍 Java 代理機制,收穫頗豐

看到這裡,不知道大家能不能理解了為什麼委託類和代理類都需要實現相同的介面?

那是為了保持行為的一致性,在訪問者看來兩者之間就沒有區別。這樣,通過代理類這個中間層,很好地隱藏和保護了委託類物件,能有效遮蔽外界對委託類物件的直接訪問。同時,也可以在代理類上加上額外的操作,比如小紅在說話之前會跳一段舞,外界就會覺得你在說話前會跳一段舞,所以,這就實現了委託類的功能增強

代理模式有靜態代理和動態代理兩種實現方式。

3. 靜態代理

什麼是靜態代理

先來看靜態代理的實現步驟:

1)定義一個介面(Subject)

2)建立一個委託類(Real Subject)實現這個介面

3)建立一個代理類(Proxy)同樣實現這個介面

4)將委託類 Real Subject 注入進代理類 Proxy,在代理類的方法中呼叫 Real Subject 中的對應方法。這樣的話,我們就可以通過代理類遮蔽對目標物件的訪問,並且可以在目標方法執行前後做一些自己想做的事情。

從實現和應用角度來說,靜態代理中,我們對目標物件的每個方法的增強都是手動完成的,非常不靈活(比如介面一旦新增加方法,目標物件和代理物件都要進行修改)且麻煩(需要對每個目標類都單獨寫一個代理類)。 實際應用場景非常非常少,日常開發幾乎看不到使用靜態代理的場景。

從 JVM 層面來說, 靜態代理在編譯時就將介面、委託類、代理類這些都變成了一個個實際的 .class 檔案。

程式碼示例

1)定義傳送簡訊的介面

public interface SmsService {
    String send(String message);
}

 

2)建立一個委託類(Real Subject)實現這個介面

public class SmsServiceImpl implements SmsService {
    public String send(String message) {
        System.out.println("send message:" + message);
        return message;
    }
}

 

3)建立一個代理類(Proxy)同樣實現這個介面

4)將委託類 Real Subject 注入進代理類 Proxy,在代理類的方法中呼叫 Real Subject 中的對應方法。這樣的話,我們就可以通過代理類遮蔽對目標物件的訪問,並且可以在目標方法執行前後做一些自己想做的事情。

public class SmsProxy implements SmsService {
    
    // 將委託類注入進代理類
    private final SmsService smsService;
​
    public SmsProxy(SmsService smsService) {
        this.smsService = smsService;
    }
​
    @Override
    public String send(String message) {
        // 呼叫委託類方法之前,我們可以新增自己的操作
        System.out.println("before method send()");
        // 呼叫委託類方法
        smsService.send(message); 
        // 呼叫委託類方法之後,我們同樣可以新增自己的操作
        System.out.println("after method send()");
        return null;
    }
}

 

那麼,如何使用這個被增強的 send 方法呢?

public class Main {
    public static void main(String[] args) {
        SmsService smsService = new SmsServiceImpl();
        SmsProxy smsProxy = new SmsProxy(smsService);
        smsProxy.send("Java");
    }
}

 

執行上述程式碼之後,控制檯列印出:

before method send()
send message:java
after method send()

 

從輸出結果可以看出,我們已經增強了委託類 SmsServiceImplsend() 方法。

當然,從上述程式碼我們也能看出來,靜態代理存在一定的弊端。假如說我們現在新增了一個委託類實現了 SmsService 介面,如果我們想要對這個委託類進行增強,就需要重新寫一個代理類,然後注入這個新的委託類,非常不靈活。也就是說靜態代理是一個委託了對應一個代理類,能不能將代理類做成一個通用的呢?為此,動態代理應用而生。

4. Java 位元組碼生成框架

在講解動態之前,我們有必要詳細說一下 .class 位元組碼檔案這個東西。動態代理機制和 Java 位元組碼生成框架息息相關。

在上文反射中我們提到,一個 Class 類對應一個 .class 位元組碼檔案,也就說位元組碼檔案中儲存了一個類的全部資訊。位元組碼其實是二進位制檔案,內容是隻有 JVM 能夠識別的機器碼。

解析過程這樣的:JVM 讀取 .class 位元組碼檔案,取出二進位制資料,載入到記憶體中,解析位元組碼檔案內的資訊,生成對應的 Class 類物件:

從頭捋了一遍 Java 代理機制,收穫頗豐

顯然,上述這個過程是在編譯期就發生的。

那麼,由於JVM 是通過 .class 位元組碼檔案(也就是二進位制資訊)載入類的,如果我們在執行期遵循 Java 編譯系統組織 .class 位元組碼檔案的格式和結構,生成相應的二進位制資料,然後再把這個二進位制資料載入轉換成對應的類。這樣,我們不就完成了在執行時動態的建立一個類。這個思想其實也就是動態代理的思想。

從頭捋了一遍 Java 代理機制,收穫頗豐

在執行時期按照 JVM 規範對 .class 位元組碼檔案的組織規則,生成對應的二進位制資料。當前有很多開源框架可以完成這個功能,如

  • ASM

  • CGLIB

  • Javassist

  • ......

需要注意的是,CGLIB 是基於 ASM 的。 這裡簡單對比一下 ASM 和 Javassist:

  • Javassist 原始碼級 API 比 ASM 中實際的位元組碼操作更容易使用

  • Javassist 在複雜的位元組碼級操作上提供了更高階別的抽象層。Javassist 原始碼級 API 只需要很少的位元組碼知識,甚至不需要任何實際位元組碼知識,因此實現起來更容易、更快。

  • Javassist 使用反射機制,這使得它比 ASM 慢。

總的來說 ASM 比 Javassist 快得多,並且提供了更好的效能,但是 Javassist 相對來說更容易使用,兩者各有千秋。

以 Javassist 為例,我們來看看這些框架在執行時生成 .class 位元組碼檔案的強大能力。

正常來說,我們建立一個類的程式碼是這樣的:

package com.samples;
​
public class Programmer {
    public void code(){
        System.out.println("I'm a Programmer,Just Coding.....");
    }
}

 

下面通過 Javassist 建立和上面一模一樣的 Programmer 類的位元組碼:

import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import javassist.CtNewMethod;
​
public class MyGenerator {
    public static void main(String[] args) throws Exception {
        ClassPool pool = ClassPool.getDefault();
        // 建立 Programmer 類      
        CtClass cc= pool.makeClass("com.samples.Programmer");
        // 定義方法
        CtMethod method = CtNewMethod.make("public void code(){}", cc);
        // 插入方法程式碼
        method.insertBefore("System.out.println(\"I'm a Programmer,Just Coding.....\");");
        cc.addMethod(method);
        // 儲存生成的位元組碼
        cc.writeFile("d://temp");
    }
}

 

通過反編譯工具開啟 Programmer.class 可以看到以下程式碼:

從頭捋了一遍 Java 代理機制,收穫頗豐

恐怖如斯!

5. 什麼是動態代理

OK,瞭解了 Java 位元組碼生成框架,可以開始學習動態代理(Dynamic Proxy)了。

回顧一下靜態代理,我們把靜態代理的執行過程抽象為下圖:

從頭捋了一遍 Java 代理機制,收穫頗豐

可以看見,代理類無非是在呼叫委託類方法的前後增加了一些操作。委託類的不同,也就導致代理類的不同。

那麼為了做一個通用性的代理類出來,我們把呼叫委託類方法的這個動作抽取出來,把它封裝成一個通用性的處理類,於是就有了動態代理中的 InvocationHandler 角色(處理類)。

於是,在代理類和委託類之間就多了一個處理類的角色,這個角色主要是對代理類呼叫委託類方法的這個動作進行統一的呼叫,也就是由 InvocationHandler 來統一處理代理類呼叫委託類方法這個操作。看下圖:

從頭捋了一遍 Java 代理機制,收穫頗豐

從 JVM 角度來說,動態代理是在執行時動態生成 .class 位元組碼檔案 ,並載入到 JVM 中的。這個我們在 Java 位元組碼生成框架中已經提到過。

雖然動態代理在我們日常開發中使用的相對較少,但是在框架中的幾乎是必用的一門技術。學會了動態代理之後,對於我們理解和學習各種框架的原理也非常有幫助,Spring AOP、RPC 等框架的實現都依賴了動態代理

就 Java 來說,動態代理的實現方式有很多種,比如:

  • JDK 動態代理

  • CGLIB 動態代理

  • Javassit 動態代理

  • ......

下面詳細講解這三種動態代理機制。

6. JDK 動態代理機制

使用步驟

先來看下 JDK 動態代理機制的使用步驟:

1)定義一個介面(Subject)

2)建立一個委託類(Real Subject)實現這個介面

3)建立一個處理類並實現 InvocationHandler 介面,重寫其 invoke 方法(在 invoke 方法中利用反射機制呼叫委託類的方法,並自定義一些處理邏輯),並將委託類注入處理類

從頭捋了一遍 Java 代理機制,收穫頗豐

該方法有下面三個引數:

  • proxy:代理類物件(見下一步)

  • method:還記得我們在上篇文章反射中講到的 Method.invoke 嗎?就是這個,我們可以通過它來呼叫委託類的方法(反射)

    從頭捋了一遍 Java 代理機制,收穫頗豐

  • args:傳給委託類方法的引數列表

4)建立代理物件(Proxy):通過 Proxy.newProxyInstance() 建立委託類物件的代理物件

從頭捋了一遍 Java 代理機制,收穫頗豐

這個方法需要 3 個引數:

  • 類載入器 ClassLoader

  • 委託類實現的介面陣列,至少需要傳入一個介面進去

  • 呼叫的 InvocationHandler 例項處理介面方法(也就是第 3 步我們建立的類的例項)

也就是說:我們在通過 Proxy 類的 newProxyInstance() 建立的代理物件在呼叫方法的時候,實際會呼叫到實現了 InvocationHandler 介面的處理類的 invoke()方法,可以在 invoke() 方法中自定義處理邏輯,比如在方法執行前後做什麼事情。

程式碼示例

1)定義一個介面(Subject)

public interface SmsService {
    String send(String message);
}

 

2)建立一個委託類(Real Subject)實現這個介面

public class SmsServiceImpl implements SmsService {
    public String send(String message) {
        System.out.println("send message:" + message);
        return message;
    }
}

 

3)建立一個處理類並實現 InvocationHandler 介面,重寫其 invoke 方法(在 invoke 方法中利用反射機制呼叫委託類的方法,並自定義一些處理邏輯),並將委託類注入處理類

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
​
public class DebugInvocationHandler implements InvocationHandler {
    
    // 將委託類注入處理類(這裡我們用 Object 代替,方便擴充套件)
    private final Object target;
​
    public DebugInvocationHandler(Object target) {
        this.target = target;
    }
    
    // 重寫 invoke 方法
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws InvocationTargetException, IllegalAccessException {
        //呼叫方法之前,我們可以新增自己的操作
        System.out.println("before method " + method.getName());
        Object result = method.invoke(target, args);
        //呼叫方法之後,我們同樣可以新增自己的操作
        System.out.println("after method " + method.getName());
        return result;
    }
}

 

4)定義一個建立代理物件(Proxy)的工廠類:通過 Proxy.newProxyInstance() 建立委託類物件的代理物件

public class JdkProxyFactory {
    public static Object getProxy(Object target) {
        return Proxy.newProxyInstance(
                target.getClass().getClassLoader(),
                target.getClass().getInterfaces(),
                new DebugInvocationHandler(target)
        );
    }
}

 

5)實際使用

SmsService smsService = (SmsService) JdkProxyFactory.getProxy(new SmsServiceImpl());
smsService.send("Java");

 

執行上述程式碼之後,控制檯列印出:

before method send
send message:Java
after method send

 

7. CGLIB 動態代理機制

使用步驟

JDK 動態代理有一個最致命的問題是它只能代理實現了某個介面的實現類,並且代理類也只能代理介面中實現的方法,要是實現類中有自己私有的方法,而介面中沒有的話,該方法不能進行代理呼叫

為了解決這個問題,我們可以用 CGLIB 動態代理機制。

上文也提到過,CGLIB(Code Generation Library)是一個基於 ASM 的 Java 位元組碼生成框架,它允許我們在執行時對位元組碼進行修改和動態生成。原理就是通過位元組碼技術生成一個子類,並在子類中攔截父類方法的呼叫,織入額外的業務邏輯。關鍵詞大家注意到沒有,攔截!CGLIB 引入一個新的角色就是方法攔截器 MethodInterceptor。和 JDK 中的處理類 InvocationHandler 差不多,也是用來實現方法的統一呼叫的。看下圖:

從頭捋了一遍 Java 代理機制,收穫頗豐

另外由於 CGLIB 採用繼承的方式,所以被代理的類不能被 final 修飾。

很多知名的開源框架都使用到了 CGLIB, 例如 Spring 中的 AOP 模組中:如果目標物件實現了介面,則預設採用 JDK 動態代理,否則採用 CGLIB 動態代理

來看 CGLIB 動態代理的使用步驟:

1)首先建立一個委託類(Real Subject)

2)建立一個方法攔截器實現介面 MethodInterceptor,並重寫 intercept 方法。intercept 用於攔截並增強委託類的方法(和 JDK 動態代理 InvocationHandler 中的 invoke 方法類似)

從頭捋了一遍 Java 代理機制,收穫頗豐

該方法擁有四個引數:

  • Object var1:委託類物件

  • Method var2:被攔截的方法(委託類中需要增強的方法)

  • Object[] var3:方法入參

  • MethodProxy var4:用於呼叫委託類的原始方法(底層也是通過反射機制,不過不是 Method.invoke 了,而是使用 MethodProxy.invokeSuper 方法)

    從頭捋了一遍 Java 代理機制,收穫頗豐

3)建立代理物件(Proxy):通過 Enhancer.create() 建立委託類物件的代理物件

從頭捋了一遍 Java 代理機制,收穫頗豐

也就是說:我們在通過 Enhancer 類的 create() 建立的代理物件在呼叫方法的時候,實際會呼叫到實現了 MethodInterceptor 介面的處理類的 intercept()方法,可以在 intercept() 方法中自定義處理邏輯,比如在方法執行前後做什麼事情。

可以發現,CGLIB 動態代理機制和 JDK 動態代理機制的步驟差不多,CGLIB 動態代理的核心是方法攔截器 MethodInterceptorEnhancer,而 JDK 動態代理的核心是處理類 InvocationHandlerProxy

程式碼示例

不同於 JDK 動態代理不需要額外的依賴。CGLIB 是一個開源專案,如果你要使用它的話,需要手動新增相關依賴。

<dependency>
  <groupId>cglib</groupId>
  <artifactId>cglib</artifactId>
  <version>3.3.0</version>
</dependency>

 

1)首先建立一個委託類(Real Subject)

public class AliSmsService {
    public String send(String message) {
        System.out.println("send message:" + message);
        return message;
    }
}

 

2)建立一個方法攔截器實現介面 MethodInterceptor,並重寫 intercept 方法

import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;

public class DebugMethodInterceptor implements MethodInterceptor {

    @Override
    public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
        // 呼叫方法之前,我們可以新增自己的操作
        System.out.println("before method " + method.getName());
        // 通過反射呼叫委託類的方法
        Object object = methodProxy.invokeSuper(o, args);
        // 呼叫方法之後,我們同樣可以新增自己的操作
        System.out.println("after method " + method.getName());
        return object;
    }

}

 

3)建立代理物件(Proxy):通過 Enhancer.create() 建立委託類物件的代理物件

import net.sf.cglib.proxy.Enhancer;

public class CglibProxyFactory {
    public static Object getProxy(Class<?> clazz) {
        // 建立動態代理增強類
        Enhancer enhancer = new Enhancer();
        // 設定類載入器
        enhancer.setClassLoader(clazz.getClassLoader());
        // 設定委託類(設定父類)
        enhancer.setSuperclass(clazz);
        // 設定方法攔截器
        enhancer.setCallback(new DebugMethodInterceptor());
        // 建立代理類
        return enhancer.create();
    }
}

 

setSuperclass 我們就能看出,為什麼說 CGLIB 是基於繼承的。

4)實際使用

AliSmsService aliSmsService = 
    (AliSmsService) CglibProxyFactory.getProxy(AliSmsService.class);
aliSmsService.send("Java");

 

執行上述程式碼之後,控制檯列印出:

before method send
send message:Java
after method send

 

JDK 動態代理和 CGLIB 動態代理對比

1)JDK 動態代理是基於實現了介面的委託類,通過介面實現代理;而 CGLIB 動態代理是基於繼承了委託類的子類,通過子類實現代理。

2)JDK 動態代理只能代理實現了介面的類,且只能增強介面中現有的方法;而 CGLIB 可以代理未實現任何介面的類。

3)就二者的效率來說,大部分情況都是 JDK 動態代理的效率更高,隨著 JDK 版本的升級,這個優勢更加明顯。

提一嘴,常見的還有 Javassist 動態代理機制。和 CGLIB 一樣,作為一個 Java 位元組碼生成框架,Javassist 天生就擁有在執行時動態建立一個類的能力,實現動態代理自然不在話下。 Dubbo 就是預設使用 Javassit 來進行動態代理的。

8. 什麼情況下使用動態代理

1)設計模式中有一個設計原則是開閉原則,即對修改關閉,對擴充套件開放,我們在工作中有時會接手很多前人的程式碼,裡面程式碼邏輯讓人摸不著頭腦,就很難去下手修改程式碼,那麼這時我們就可以通過代理對類進行增強。

2)我們在使用 RPC 框架的時候,框架本身並不能提前知道各個業務方要呼叫哪些介面的哪些方法 。那麼這個時候,就可用通過動態代理的方式來建立一箇中間人給客戶端使用,也方便框架進行搭建邏輯,某種程度上也是客戶端程式碼和框架鬆耦合的一種表現。

3)Spring 的 AOP 機制同樣也是採用了動態代理,此處不做詳細討論。

9. 靜態代理和動態代理對比

1)靈活性 :動態代理更加靈活,不需要必須實現介面,可以直接代理實現類,並且可以不需要針對每個目標類都建立一個代理類。另外,靜態代理中,介面一旦新增加方法,目標物件和代理物件都要進行修改,這是非常麻煩的

2)JVM 層面 :靜態代理在編譯時就將介面、實現類、代理類這些都變成了一個個實際的 .class 位元組碼檔案。而動態代理是在執行時動態生成類位元組碼,並載入到 JVM 中的。

10. 總結

全部捋一遍下來還是收穫蠻多的,我感覺只要理解了位元組碼在編譯期生成還是在執行期生成,就差不多能夠把握住靜態代理和動態代理了。總結一下靜態代理和動態代理中的角色:

靜態代理:

  • Subject:公共介面

  • Real Subject:委託類

  • Proxy:代理類

JDK 動態代理:

  • Subject:公共介面

  • Real Subject:委託類

  • Proxy:代理類

  • InvocationHandler:處理類,統一呼叫方法

CGLIB 動態代理:

  • Subject:公共介面

  • Real Subject:委託類

  • Proxy:代理類

  • MethodInterceptor:方法攔截器,統一呼叫方法

參考資料

? 關注公眾號 | 飛天小牛肉,即時獲取更新

  • 博主東南大學碩士在讀,利用課餘時間運營一個公眾號『 飛天小牛肉 』,2020/12/29 日開通,專注分享計算機基礎(資料結構 + 演算法 + 計算機網路 + 資料庫 + 作業系統 + Linux)、Java 基礎和麵試指南的相關原創技術好文。本公眾號的目的就是讓大家可以快速掌握重點知識,有的放矢。希望大家多多支援哦,和小牛肉一起成長 ?

  • 並推薦個人維護的開源教程類專案: CS-Wiki(Gitee 推薦專案,現已累計 1.5k+ star), 致力打造完善的後端知識體系,在技術的路上少走彎路,歡迎各位小夥伴前來交流學習 ~ ?

  • 如果各位小夥伴春招秋招沒有拿得出手的專案的話,可以參考我寫的一個專案「開源社群系統 Echo」Gitee 官方推薦專案,目前已累計 330+ star,基於 SpringBoot + MyBatis + MySQL + Redis + Kafka + Elasticsearch + Spring Security + ... 並提供詳細的開發文件和配套教程。公眾號後臺回覆 Echo 可以獲取配套教程,目前尚在更新中。

相關文章