Java基礎之代理模式


代理模式是常見的設計模式之一,意圖在為指定對象提供一種代理以控制對這個對象的訪問。Java中的代理分為動態代理和靜態代理,動態代理在Java中的應用比較廣泛,比如Spring的AOP實現、遠程RPC調用等。靜態代理和動態代理的最大區別就是代理類是JVM啟動之前還是之后生成。本文會介紹Java的靜態代理和動態代理,以及二者之間的對比,重點是介紹動態代理的原理及其實現。

代理模式

代理模式的定義:為其他對象提供一種代理以控制對這個對象的訪問。在某些情況下,一個對象不適合或者不能直接引用另一個對象,而代理對象可以在客戶端和目標對象之間起到中介的作用。比如說:要訪問的對象在遠程的機器上。在面向對象系統中,有些對象由於某些原因(比如對象創建開銷很大,或者某些操作需要安全控制,或者需要進程外的訪問),直接訪問會給使用者或者系統結構帶來很多麻煩,我們可以在訪問此對象時加上一個對此對象的訪問層。

代理示例圖

代理的組成

代理由以下三部分角色組成:

  • 抽象角色:通過接口或抽象類聲明真實角色實現的業務方法。
  • 代理角色:實現抽象角色,是真實角色的代理,通過真實角色的業務邏輯方法來實現抽象方法,並可以附加自己的操作。
  • 真實角色:實現抽象角色,定義真實角色所要實現的業務邏輯,供代理角色調用。

代理的優點

  1. 職責清晰:真實的角色就是實現實際業務的邏輯,不用關系非業務的邏輯(如事務管理)。
  2. 隔離作用:代理對象可以在客戶端和目標對象之間起到中介作用,目標對象不直接暴露給客戶端,從而實現隔離目標對象的作用
  3. 高可擴展性:代理對象可以對目標對象進行靈活的擴展。

代理的例子

我們用一個加載並顯示圖片的例子來解釋代理的工作原理,圖片存在磁盤上,每次IO會花費比較多的事件,如果我們需要頻繁的顯示圖片,每次都從磁盤讀取會花費比較長的時間。我們通過一個代理來緩存圖片,只有第一次讀取圖片的時候才從磁盤讀取,之后都從緩存中讀取,源碼示例如下:

import java.util.*;
 
interface Image {
    public void displayImage();
}

//on System A 
class RealImage implements Image {
    private String filename;
    public RealImage(String filename) { 
        this.filename = filename;
        loadImageFromDisk();
    }

    private void loadImageFromDisk() {
        System.out.println("Loading   " + filename);
    }

    public void displayImage() { 
        System.out.println("Displaying " + filename); 
    }
}

//on System B 
class ProxyImage implements Image {
    private String filename;
    private Image image;
 
    public ProxyImage(String filename) { 
        this.filename = filename; 
    }
    public void displayImage() {
        if(image == null)
              image = new RealImage(filename);
        image.displayImage();
    }
}
 
class ProxyExample {
    public static void main(String[] args) {
        Image image1 = new ProxyImage("HiRes_10MB_Photo1");
        Image image2 = new ProxyImage("HiRes_10MB_Photo2");     
        
        image1.displayImage(); // loading necessary
        image2.displayImage(); // loading necessary
    }
}

靜態代理

靜態代理需要在程序中定義兩個類:目標對象類和代理對象類,為了保證二者行為的一致性,目標對象和代理對象實現了相同的接口。代理類的信息在程序運行之前就已經確定,代理對象中會包含目標對象的引用。

舉例說明靜態代理的使用: 假設我們有一個接口方法用於計算員工工資,有一個實現類實現了具體的邏輯,如果我們需要給計算員工工資的邏輯添加日志應該怎么辦呢?直接在計算工資的實現邏輯里面添加會導致引入非業務邏輯,不符合規范。這個時候我們就可以引入一個日志代理,在計算工資前后輸出相關的日志信息。

  • 計算員工工資的接口定義如下:
public interface Employee {
    double calculateSalary(int id);
}
  • 計算員工工資的實現類如下:
public class EmployeeImpl {
    public double calculateSalary(int id){
        return 100;
    }
}
  • 帶有日志的代理類的實現如下:
public class EmployeeLogProxy implements Employee {

    //代理類需要包含一個目標類的對象引用
    private EmployeeImpl employee;

    //並提供一個帶參的構造方法用於指定代理哪個對象
    public EmployeeProxyImpl(EmployeeImpl employee){
        this.employee = employee;
    }

    public double calculateSalary(int id) {

        //在調用目標類的calculateSalary方法之前記錄日志
        System.out.println("當前正在計算員工: " + id + "的稅后工資");
        double salary = employee.calculateSalary(id);
        System.out.println("計算員工: " + id + "的稅后工資結束");
        // 在調用目標類方法之后記錄日志
        return salary;
    }
}

動態代理

動態代理的代理對象類在程序運行時被創建,而靜態代理對象類則是在程序編譯期就確定好的,這是二者最大的不同之處。動態代理的優勢再於不需要開發者手工寫很多代理類,比如上面的例子中,如果再來一個Manager類計算工資的邏輯需要日志,那么我們就需要新建一個ManagerLogProxy來代理對象,如果需要代理的對象很多,那么需要寫的代理類也會很多。

而使用動態代理則沒有這種問題,一種類型的代理只需要寫一次,就可以適用於所有的代理對象。比如上文中的EmployeeManager,二者只需要抽象一個計算薪資相關的接口,就可以使用同一套動態代理邏輯實現代理。

動態代理示例

下面我們使用上文中的EmployeeManager計算薪資的邏輯來展示動態代理的用法。

接口的抽象

我們知道EmployeeManager都有計算薪資的邏輯,而且需要對計算薪資的邏輯進行日志記錄,所以我們需要抽象一個計算薪資的接口:

public interface SalaryCalculator {
    double calculateSalary(int id);
}

接口的實現

public class EmployeeSalaryCalculator implements SalaryCalculator{
    public double calculateSalary(int id){
        return 100;
    }
}
public class ManagerSalaryCalculator implements SalaryCalculator{
    public double calculateSalary(int id){
        return 1000000;
    }
}

創建動態代理的InvocationHandler

public class SalaryLogProxy implements InvocationHandler {
    private SalaryCalculator calculator;

    public SalaryLogProxy(SalaryCalculator calculator) {
        this.calculator = calculator;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("--------------begin-------------");
        Object invoke = method.invoke(subject, args);
        System.out.println("--------------end-------------");
        return invoke;
    }
}

創建代理對象

public class Main {

    public static void main(String[] args) {
        SalaryCalculator calculator = new ManagerSalaryCalculator();
        InvocationHandler calculatorProxy = new SalaryLogProxy(subject);
        SalaryCalculator proxyInstance = (SalaryCalculator) Proxy.newProxyInstance(calculatorProxy.getClass().getClassLoader(), subject.getClass().getInterfaces(), calculatorProxy);
        proxyInstance.calculateSalary(1);
    }

}

動態代理源碼分析

動態代理的流程如下圖所示,可以看到動態代理中包含以下內容:

  • 目標對象:我們需要代理的對象,對應上文中的new ManagerSalaryCalculator()
  • 接口:目標對象和代理對象需要共同提供的方法,對應上文中的SalaryCalculator
  • Proxy代理:用於生成代理對象類。
  • 代理對象類:通過代理和對應的參數得到的代理對象。
  • 類加載器:用於加載代理對象類的類加載器,對應上文中的calculatorProxy.getClass().getClassLoader()

動態代理流程

Proxy.newProxyInstance

動態代理的關鍵代碼就是Proxy.newProxyInstance(classLoader, interfaces, handler).

  • 可以看到Proxy.newProxyInstance一共做了兩件事情:1.獲取代理對象類的構造函數,2:根據構造函數實例化代理對象。
@CallerSensitive
public static Object newProxyInstance(ClassLoader loader,
                Class<?>[] interfaces, InvocationHandler h) {
    Objects.requireNonNull(h);

    final Class<?> caller = System.getSecurityManager() == null
                                    ? null : Reflection.getCallerClass();

    /*
     * Look up or generate the designated proxy class and its constructor.
     */
    // 獲取代理對象類的構造函數,里面就包含了代理對象類的構建和加載
    Constructor<?> cons = getProxyConstructor(caller, loader, interfaces);
 
    // 根據構造函數生成代理實例.
    return newProxyInstance(caller, cons, h);
}

代理對象類

通過查看源碼,我們可以發現代理對象類都extend了Proxy類並實現了指定接口中的方法。由於java不能多繼承,這里已經繼承了Proxy類了,不能再繼承其他的類。所以JDK的動態代理不支持對實現類的代理,只支持接口的代理。

我是御狐神,歡迎大家關注我的微信公眾號

qrcode_for_gh_83670e17bbd7_344-2021-09-04-10-55-16

本文最先發布至微信公眾號,版權所有,禁止轉載!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM