設計模式

2022-10-19 06:00:56

設計模式 - 動態代理

什麼是代理

  • 代購、中介、商家

舉個栗子:

比如有一家美國大學,面向全世界招生,而我們國內的同學,需要去到某個大學。因為我們所處國內,並不知道這個大學的基本情況。那我們又想去了解,並且進入這個大學。這就衍生處理一個行業,中介(代理)由代理招收學生到給到大學。也就是我們入學的事情交給了代理去完成。

特點:

  • 中介和代理,是基於不同角度來看待的,從學校的角度來看,我們入學,需要中介這個橋樑。從我們的角度來看,入學需要通過中介與學校進行溝通(代理)。、
  • 中介幫我們入學,收取一定費用。(功能增強)

代理模式

代理模式:給某一個物件提供一個代理,並由代理物件來控制對真實物件的存取。代理模式是一種結構型設計模式。(逐字理解)

三種角色

  • Subject(抽象主題角色):定義代理類和真實主題的公共對外方法,也是代理類代理真實主題的方法;(介面)
  • RealSubject(真實主題角色):真正實現業務邏輯的類;(類)
  • Proxy(代理主題角色):用來代理和封裝真實主題;(類)

代理模式的結構比較簡單,其核心是代理類,為了讓使用者端能夠一致性地對待真實物件和代理物件,在代理模式中引入了抽象層。

可能這裡還是看的雲裡霧裡的,通過一個demo,來加深我們對於靜態代理的理解

靜態代理

目錄結構

// 等同於 Subject
public interface UserService {
    // 定義的業務邏輯
    void select();
    // 定義的業務邏輯
    void update();
}

// 等同於 RealSubject
public class UserServiceImpl implements UserService {
    public void select() {
        System.out.println("查詢 selectById");
    }

    public void update() {
        System.out.println("更新 update");
    }
}

此時,我們的業務邏輯已經實現,但是我們的代理還未定義。我們都知道,代理簡單來說,在不侵入原有業務程式碼的條件下,對其功能增強。

public class UserServiceProxy implements UserService {
    private UserService target; // 被代理的物件

    public UserServiceProxy(UserService target) {
        this.target = target;
    }

    public void select() {
        before();           // 增強操作
        target.select();    // 這裡才實際呼叫真實主題角色的方法
        after();            // 增強操作
    }

    public void update() {
        before();           // 增強操作
        target.update();    // 這裡才實際呼叫真實主題角色的方法
        after();            // 增強操作
    }

    /**
     * 在執行方法之前執行
     */
    private void before() {
        System.out.println(String.format("log start time [%s] ", new Date()));
    }

    /**
     * 在執行方法之後執行
     */
    private void after() {
        System.out.println(String.format("log end time [%s] ", new Date()));
    }
}

執行使用者端測試:

public class Client {
    public static void main(String[] args) {
        // 建立業務處理類
        UserService userService = new UserServiceImpl();
        // 通過構造方法進行傳入業務處理類到代理物件中 進行功能增強
        UserServiceProxy proxy = new UserServiceProxy(userService);
        // 代理執行目標方法
        proxy.select();
    }
}

執行結果:

可以看到通過靜態代理,我們達到了功能增強的目的,而且沒有侵入原始碼,這是靜態代理的一個優點。

缺點:

雖然靜態代理實現簡單,且不侵入原始碼,但是,當場景稍微複雜一些的時候,靜態代理的缺點也會暴露出來。

如:當需要代理多個類的時候,由於代理物件要實現與目標物件一致的介面,有兩種方式:

  • 只維護一個代理類,由這個代理類實現多個介面,但是這樣就導致代理類過於龐大
  • 新建多個代理類,每個目標物件對應一個代理類,但是這樣會產生過多的代理類
  • 當介面需要增加、刪除、修改方法的時候,目標物件與代理類都要同時修改,不易維護。

如何改進?

  • 當然是我們的動態代理啦。

動態代理

為什麼類可以動態的生成?

這就涉及到Java虛擬機器器的類載入機制了,推薦翻看《深入理解Java虛擬機器器》7.3節 類載入的過程。

Java虛擬機器器類載入過程主要分為五個階段:載入、驗證、準備、解析、初始化。其中載入階段需要完成以下3件事情:

  • 通過一個類的全限定名來獲取定義此類的二進位制位元組流
  • 將這個位元組流所代表的靜態儲存結構轉化為方法區的執行時資料結構
  • 在記憶體中生成一個代表這個類的 java.lang.Class 物件,作為方法區這個類的各種資料存取入口

由於虛擬機器器規範對這3點要求並不具體,所以實際的實現是非常靈活的,關於第1點,獲取類的二進位制位元組流(class位元組碼)就有很多途徑:

  • 從ZIP包獲取,這是JAR、EAR、WAR等格式的基礎
  • 從網路中獲取,典型的應用是 Applet
  • 執行時計算生成,這種場景使用最多的是動態代理技術,在 java.lang.reflect.Proxy 類中,就是用了 ProxyGenerator.generateProxyClass 來為特定介面生成形式為 *$Proxy 的代理類的二進位制位元組流
  • 由其它檔案生成,典型應用是JSP,即由JSP檔案生成對應的Class類
  • 從資料庫中獲取等等

所以,動態代理就是想辦法,根據介面或目標物件,計算出代理類的位元組碼,然後再載入到JVM中使用。但是如何計算?如何生成?情況也許比想象的複雜得多,我們需要藉助現有的方案。


常見的位元組碼操作類庫

  • Apache BCEL (Byte Code Engineering Library):是Java classworking廣泛使用的一種框架,它可以深入到JVM組合語言進行類操作的細節。
  • ObjectWeb ASM:是一個Java位元組碼操作框架。它可以用於直接以二進位制形式動態生成stub根類或其他代理類,或者在載入時動態修改類。
  • CGLIB(Code Generation Library):是一個功能強大,高效能和高質量的程式碼生成庫,用於擴充套件JAVA類並在執行時實現介面。
  • Javassist:Java的載入時反射系統,它是一個用於在Java中編輯位元組碼的類庫; 它使Java程式能夠在執行時定義新類,並在JVM載入之前修改類檔案。

實現動態代理的思考方向

為了讓生成的代理類與目標物件(真實主題角色)保持一致性,從現在開始將介紹以下兩種最常見的方式:

  • 通過實現介面的方式 -> JDK動態代理
  • 通過繼承類的方式 -> CGLIB動態代理

注:使用ASM對使用者要求比較高,使用Javassist會比較麻煩。


JDK動態代理

JDK動態代理主要涉及兩個類:java.lang.reflect.Proxyjava.lang.reflect.InvocationHandler,我們仍然通過案例來學習編寫一個呼叫邏輯處理器 LogHandler 類,提供紀錄檔增強功能,並實現 InvocationHandler 介面;在 LogHandler 中維護一個目標物件,這個物件是被代理的物件(真實主題角色);在 invoke 方法中編寫方法呼叫的邏輯處理。

public class LogHandler implements InvocationHandler {
    private Object target;

    //傳入目標物件
    public LogHandler(Object target) {
        this.target = target;
    }


    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        before();
        Object result = method.invoke(target, args);// 呼叫 target 的 method 方法
        after();
        return result; // 返回方法的執行結果
    }

    // 呼叫invoke方法之前執行
    private void before() {
        System.out.println(String.format("log start time [%s] ", new Date()));
    }

    // 呼叫invoke方法之後執行
    private void after() {
        System.out.println(String.format("log end time [%s] ", new Date()));
    }
}

使用者端測試:

public class Client {
    public static void main(String[] args) {
        // 1. 建立被代理的物件,UserService介面的實現類
        UserService userService = new UserServiceImpl();
        // 2. 獲取對應的 ClassLoader
        ClassLoader classLoader = UserServiceImpl.class.getClassLoader();
        // 3. 獲取所有介面的Class,這裡的UserServiceImpl只實現了一個介面UserService
        Class<?>[] interfaces = UserServiceImpl.class.getInterfaces();
        // 4. 建立一個將傳給代理類的呼叫請求處理器,處理所有的代理物件上的方法呼叫
        //    這裡建立的是一個自定義的紀錄檔處理器,須傳入實際的執行物件 userServiceImpl
        InvocationHandler handler = new LogHandler(userService);
        // 5. newProxyInstance 建立代理物件
        //  引數1:需要傳入一個類載入器 也就是需要代理的類
        //  引數2:需要傳入一個介面的Class 也就是代理的類需要實現的介面
        //  引數3:需要傳入一個呼叫處理類 也就是呼叫過程程中,對目標方法的增強
        UserService  proxy = (UserService) Proxy.newProxyInstance(classLoader, interfaces, handler);
        // 通過代理類 呼叫目標方法
        proxy.select();
    }
}

執行紀錄檔:

JDK動態代理執行方法呼叫的過程簡圖如下:

  • UserServiceProxy 繼承了 Proxy 類,並且實現了被代理的所有介面,以及equals、hashCode、toString等方法
  • 由於 UserServiceProxy 繼承了 Proxy 類,所以每個代理類都會關聯一個 InvocationHandler 方法呼叫處理器
  • 類和所有方法都被 public final 修飾,所以代理類只可被使用,不可以再被繼承
  • 每個方法都有一個 Method 物件來描述,Method 物件在static靜態程式碼塊中建立,以 m + 數位 的格式命名
  • 呼叫方法的時候通過 super.h.invoke(this, m1, (Object[])null); 呼叫,其中的 super.h.invoke 實際上是在建立代理的時候傳遞給 Proxy.newProxyInstance 的 LogHandler 物件,它繼承 InvocationHandler 類,負責實際的呼叫處理邏輯

CGLIB動態代理

這裡就不重複寫文章了,參照大佬的文章。

https://www.cnblogs.com/wyq1995/p/10945034.html