輕鬆理解 Java 靜態代理/動態代理

阿墩發表於2021-04-12

理解Java動態代理需要對Java的反射機制有一定了解

什麼是代理模式

在有些情況下,一個客戶不能或者不想直接訪問另一個物件,這時需要找一箇中介幫忙完成某項任務,這個中介就是代理物件。

例如,購買火車票不一定要去火車站買,可以通過 12306 網站或者去火車票代售點買。又如找女朋友、找保姆、找工作等都可以通過找中介完成。

定義

由於某些原因需要給某物件提供一個代理以控制對該物件的訪問。

訪問物件不適合或者不能直接引用目標物件,代理物件作為訪問物件目標物件之間的中介

代理模式的主要角色

  • 抽象角色(Subject):通過介面或抽象類宣告真實主題和代理物件實現的業務方法。

  • 真實角色(Real Subject):實現了抽象主題中的具體業務,是代理物件所代表的真實物件,是最終要引用的物件。

  • 代理(Proxy):提供了與真實主題相同的介面,其內部含有對真實主題的引用,它可以訪問、控制或擴充套件真實主題的功能。

  • 客戶 : 使用代理角色來進行一些操作 .

優點

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

缺點

  • 冗餘,由於代理物件要實現與目標物件一致的介面,會產生過多的代理類。
  • 系統設計中類的數量增加,變得難以維護。

使用動態代理方式,可以有效避免以上的缺點

靜態代理

靜態代理其實就是最基礎、最標準的代理模式實現方案。

舉例:

Rent . java 即抽象角色

//抽象角色:租房
public interface Rent {
   public void rent();
}

Landlord . java 即真實角色

//真實角色: 房東,房東要出租房子
public class Landlord implements Rent{
   public void rent() {
       System.out.println("房屋出租");
  }
}

Proxy . java 即代理

//代理角色:中介
public class Proxy implements Rent {

   private Landlord landlord;
   public Proxy() { }
   public Proxy(Landlord landlord) {
       this.landlord = landlord;
  }
   //租房
   public void rent(){
       seeHouse();
       landlord.rent();
       fare();
  }
   //看房
   public void seeHouse(){
       System.out.println("帶房客看房");
  }
   //收中介費
   public void fare(){
       System.out.println("收中介費");
  }
}

Client . java 即客戶

//客戶類,一般客戶都會去找代理!
public class Client {
   public static void main(String[] args) {
       //房東要租房
       Landlord landlord = new Landlord();
       //中介幫助房東
       Proxy proxy = new Proxy(landlord);
       //客戶找中介
       proxy.rent();
  }
}

結果:

帶房客看房
房屋出租
收中介費

Process finished with exit code 0

在這個過程中,客戶接觸的是中介,看不到房東,但是依舊租到了房東的房子。同時房東省了心,客戶省了事。

靜態代理享受代理模式的優點,同時也具有代理模式的缺點,那就是一旦實現的功能增加,將會變得異常冗餘和複雜,秒變光頭。

為了保護頭髮,就出現了動態代理模式!

動態代理

動態代理的出現就是為了解決傳統靜態代理模式的中的缺點。

具備代理模式的優點的同時,巧妙的解決了靜態代理程式碼冗餘,難以維護的缺點。

在Java中常用的有如下幾種方式:

  • JDK 原生動態代理
  • cglib 動態代理
  • javasist 動態代理

JDK原生動態代理

上例中靜態代理類中,中介作為房東的代理,實現了相同的租房介面。

例子

  1. 首先實現一個InvocationHandler,方法呼叫會被轉發到該類的invoke()方法。
  2. 然後在需要使用Rent的時候,通過JDK動態代理獲取Rent的代理物件。
class RentInvocationHandler implements InvocationHandler {

    private Rent rent;

    public RentInvocationHandler(Rent rent) {
        this.rent = rent;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        seeHouse();
        Object result = method.invoke(rent, args);
        fare();
        return result;
    }

    //看房
    public void seeHouse(){
        System.out.println("帶房客看房");
    }
    //收中介費
    public void fare(){
        System.out.println("收中介費");
    }
    //動態獲取代理
    public Object getProxy() {
        return Proxy.newProxyInstance(this.getClass().getClassLoader(),
                rent.getClass().getInterfaces(),this); //核心關鍵
    }
}

客戶使用動態代理呼叫

public class Client {
    public static void main(String[] args) {
        Landlord landlord = new Landlord();
        //代理例項的呼叫處理程式
        RentInvocationHandler pih = new RentInvocationHandler(landlord);
        Rent proxy = (Rent)pih.getProxy(); //動態生成對應的代理類!
        proxy.rent();
    }
}

執行結果和前例相同

分析

上述程式碼的核心關鍵是Proxy.newProxyInstance方法,該方法會根據指定的引數動態建立代理物件。

它三個引數的意義如下:

  1. loader,指定代理物件的類載入器
  2. interfaces,代理物件需要實現的介面,可以同時指定多個介面
  3. handler,方法呼叫的實際處理者,代理物件的方法呼叫都會轉發到這裡

Proxy.newProxyInstance會返回一個實現了指定介面的代理物件,對該物件的所有方法呼叫都會轉發給InvocationHandler.invoke()方法。

因此,在invoke()方法裡我們可以加入任何邏輯,比如修改方法引數,加入日誌功能、安全檢查功能等等等等……

小結

顯而易見,對於靜態代理而言,我們需要手動編寫程式碼代理實現抽象角色的介面。

而在動態代理中,我們可以讓程式在執行的時候自動在記憶體中建立一個實現抽象角色介面的代理,而不需要去單獨定義這個類,代理物件是在程式執行時產生的,而不是編譯期。

對於從Object中繼承的方法,JDK Proxy會把hashCode()equals()toString()這三個非介面方法轉發給InvocationHandler,其餘的Object方法則不會轉發。

CGLIB動態代理

JDK動態代理是基於介面的,如果物件沒有實現介面該如何代理呢?CGLIB代理登場

CGLIB(Code Generation Library)是一個基於ASM的位元組碼生成庫,它允許我們在執行時對位元組碼進行修改和動態生成。CGLIB通過繼承方式實現代理。

使用cglib需要引入cglib的jar包,如果你已經有spring-core的jar包,則無需引入,因為spring中包含了cglib。

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

例子

來看示例,假設我們有一個沒有實現任何介面的類Landlord

public class Landlord{
    public void rent() {
        System.out.println("房屋出租");
    }
}

因為沒有實現介面,所以使用通過CGLIB代理實現如下:

首先實現一個MethodInterceptor,方法呼叫會被轉發到該類的intercept()方法

public class RentMethodInterceptor implements MethodInterceptor {
    private Object target;//維護一個目標物件
    public RentMethodInterceptor(Object target) {
        this.target = target;
    }
    //為目標物件生成代理物件
    public Object getProxyInstance() {
        //工具類
        Enhancer en = new Enhancer();
        //設定父類
        en.setSuperclass(target.getClass());
        //設定回撥函式
        en.setCallback(this);
        //建立子類物件代理
        return en.create();
    }
    @Override
    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        System.out.println("看房");
        // 執行目標物件的方法
        Object returnValue = method.invoke(target, objects);
        System.out.println("中介費");
        return null;
    }
}

客戶通過CGLIB動態代理獲取代理物件

public class Client {
    public static void main(String[] args) {
        Landlord target = new Landlord();
        System.out.println(target.getClass());
        //代理物件
        Landlord proxy = (Landlord) new RentMethodInterceptor(target).getProxyInstance();
        System.out.println(proxy.getClass());
        //執行代理物件方法
        proxy.rent();
    }
}

執行輸出結果和前例相同

分析

對於從Object中繼承的方法,CGLIB代理也會進行代理,如hashCode()equals()toString()等,但是getClass()wait()等方法不會,因為它是final方法,CGLIB無法代理。

其實CGLIB和JDK代理的思路大致相同

上述程式碼中,通過CGLIB的Enhancer來指定要代理的目標物件、實際處理代理邏輯的物件。

最終通過呼叫create()方法得到代理物件,對這個物件所有非final方法的呼叫都會轉發給MethodInterceptor.intercept()方法

intercept()方法裡我們可以加入任何邏輯,同JDK代理中的invoke()方法

通過呼叫MethodProxy.invokeSuper()方法,我們將呼叫轉發給原始物件,具體到本例,就是Landlord的具體方法。CGLIG中MethodInterceptor的作用跟JDK代理中的InvocationHandler很類似,都是方法呼叫的中轉站。

final型別

CGLIB是通過繼承的方式來實現動態代理的,有繼承就不得不考慮final的問題。我們知道final型別不能有子類,所以CGLIB不能代理final型別,遇到這種情況會丟擲類似如下異常:

java.lang.IllegalArgumentException: Cannot subclass final class cglib.HelloConcrete

同樣的,final方法是不能過載的,所以也不能通過CGLIB代理,遇到這種情況不會拋異常,而是會跳過final方法只代理其他方法。

其他方案

  • 使用ASM在被代理類基礎上生成新的位元組碼形成代理類
  • 使用javassist在被代理類基礎上生成新的位元組碼形成代理類

javassist也是常用的一種動態代理方案,ASM速度非常快,這裡不在進行展開。

尾聲

動態代理是Spring AOP(Aspect Orient Programming, 面向切面程式設計)的實現方式,瞭解動態代理原理,對理解Spring AOP大有幫助。

  • 如spring等這樣的框架,要增強具體業務的邏輯方法,不可能在框架裡面去寫一個靜態代理類,太蠢了,只能按照使用者的註解或者xml配置來動態生成代理類。
  • 業務程式碼內,當需要增強的業務邏輯非常通用(如:新增log,重試,統一許可權判斷等)時,使用動態代理將會非常簡單,如果每個方法增強邏輯不同,那麼靜態代理更加適合。
  • 使用靜態代理時,如果代理類和被代理類同時實現了一個介面,當介面方法有變動時,代理類也必須同時修改,程式碼將變得臃腫且難以維護。

相關文章