天天看點

從源碼角度講述動态代理的實作

作者:小滿隻想睡覺

一、引言

在 Spring 中,最重要的應該當屬 IOC 和 AOP 了,IOC 的源碼流程還比較簡單,但 AOP 的流程就較為抽象了。

其中,AOP 中代理模式的重要性不言而喻,但對于沒了解過代理模式的人來說,痛苦至極

于是,我就去看了動态代理的實作,發現網上大多數文章講的都是不清不楚,甚至講了和沒講似的,讓我極其難受

本着咱們方向主打的就是源碼,直接從從源碼角度講述一下代理模式

兄弟們系好安全帶,準備發車!

注意:本文篇幅較長,請留出較長時間來閱讀

二、定義

代理模式的定義:由于某些原因需要給某對象提供一個代理以控制對該對象的通路。這時,通路對象不适合或者不能直接引用目标對象,代理對象作為通路對象和目标對象之間的中介。

舉個生活中常見的例子:客戶想買房,房東有很多房,提供賣房服務,但房東不會帶客戶看房,于是客戶通過中介買房。

從源碼角度講述動态代理的實作

這時候對于房東來說,不直接和客戶溝通,而是交于中介進行代理

對于中介來說,她也會在原有的基礎上收取一定的中介費

三、靜态代理

我們建立 Landlord 接口如下:

public interface Landlord {
    // 出租房子
    void apartmentToRent();
}
複制代碼           

建立其實作類 HangZhouLandlord 代表杭州房東出租房子

public class HangZhouLandlord implements Landlord {
    @Override
    public void apartmentToRent() {
        System.out.println("杭州房東出租房子");
    }
}
複制代碼           

建立代理類 LandlordProxy,代表中介服務

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("出租房後,簽訂合同");
    }
}
複制代碼           

建立最終測試:

public class JavaMain {
    public static void main(String[] args) {
        Landlord landlord = new HangZhouLandlord();

        LandlordProxy proxy = new LandlordProxy(landlord);
		  // 從中介進行租房
        proxy.apartmentToRent();
    }
}
複制代碼           

得出最終結果:

出租房前,收取中介費
杭州房東出租房子
出租房後,簽訂合同
複制代碼           

通過上述 demo 我們大概了解代理模式是怎麼一回事

  • 優點: 在不修改目标對象的功能前提下,能通過代理對象對目标功能擴充
  • 缺點: 代理對象需要與目标對象實作一樣的接口,是以會有很多代理類,一旦接口增加方法,目标對象與代理對象都要維護

四、動态代理

動态代理利用了JDK API,動态地在記憶體中建構代理對象,進而實作對目标對象的代理功能,動态代理又被稱為JDK代理或接口代理。

靜态代理與動态代理的差別:

  • 靜态代理在編譯時就已經實作了,編譯完成後代理類是一個實際的 class 檔案
  • 動态代理是在運作時動态生成的,即編譯完成後沒有實際的 class檔案,而是在運作時動态生成類位元組碼,并加載到 JVM 中

1、JDK代理

代碼如下:

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;
                    }
                }
        );
    }
}
複制代碼           

我們測試一下:

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){}
    }
}
複制代碼           

得出結果:

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 的産品,網址: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 的源碼了

從源碼角度講述動态代理的實作

我們将其複制到下面,并删減一些不必要的資訊。

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()是不是就是執行下面的代碼:

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,廢話不多說,直接來看代理的源碼。

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坐标
  • <dependency> <groupId>cglib</groupId> <artifactId>cglib</artifactId> <version>3.2.5</version> </dependency> 複制代碼

2.1 cglib動态代理實作

還是同樣的配方,我們要建立一個需要代理的類(UserServiceImpl),但不需要實作任何的接口,因為我們的 cglib 是根據類來進行建立的。

UserServiceImpl

public class UserServiceImpl {
    // 查詢功能
    List<String> findUserList() {
        return Collections.singletonList("小A");
    }
}
複制代碼           

實作 cglib 的工廠類:UserLogProxy

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

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) {

        }
    }
}
複制代碼           

結果:

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 取出,得到如下:

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 源碼系列文章

我們下期再見。

繼續閱讀