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

代理模式

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

代理的組成

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

  • 抽象角色:通過介面或抽象類宣告真實角色實現的業務方法。
  • 代理角色:實現抽象角色,是真實角色的代理,通過真實角色的業務邏輯方法來實現抽象方法,並可以附加自己的操作。
  • 真實角色:實現抽象角色,定義真實角色所要實現的業務邏輯,供代理角色呼叫。

代理的優點

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

代理的例子

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

  1. import java.util.*;
  2. interface Image {
  3. public void displayImage();
  4. }
  5. //on System A
  6. class RealImage implements Image {
  7. private String filename;
  8. public RealImage(String filename) {
  9. this.filename = filename;
  10. loadImageFromDisk();
  11. }
  12. private void loadImageFromDisk() {
  13. System.out.println("Loading " + filename);
  14. }
  15. public void displayImage() {
  16. System.out.println("Displaying " + filename);
  17. }
  18. }
  19. //on System B
  20. class ProxyImage implements Image {
  21. private String filename;
  22. private Image image;
  23. public ProxyImage(String filename) {
  24. this.filename = filename;
  25. }
  26. public void displayImage() {
  27. if(image == null)
  28. image = new RealImage(filename);
  29. image.displayImage();
  30. }
  31. }
  32. class ProxyExample {
  33. public static void main(String[] args) {
  34. Image image1 = new ProxyImage("HiRes_10MB_Photo1");
  35. Image image2 = new ProxyImage("HiRes_10MB_Photo2");
  36. image1.displayImage(); // loading necessary
  37. image2.displayImage(); // loading necessary
  38. }
  39. }

靜態代理

靜態代理需要在程式中定義兩個類:目標物件類和代理物件類,為了保證二者行為的一致性,目標物件和代理物件實現了相同的介面。代理類的資訊在程式執行之前就已經確定,代理物件中會包含目標物件的引用。

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

  • 計算員工工資的介面定義如下:
  1. public interface Employee {
  2. double calculateSalary(int id);
  3. }
  • 計算員工工資的實現類如下:
  1. public class EmployeeImpl {
  2. public double calculateSalary(int id){
  3. return 100;
  4. }
  5. }
  • 帶有日誌的代理類的實現如下:
  1. public class EmployeeLogProxy implements Employee {
  2. //代理類需要包含一個目標類的物件引用
  3. private EmployeeImpl employee;
  4. //並提供一個帶參的構造方法用於指定代理哪個物件
  5. public EmployeeProxyImpl(EmployeeImpl employee){
  6. this.employee = employee;
  7. }
  8. public double calculateSalary(int id) {
  9. //在呼叫目標類的calculateSalary方法之前記錄日誌
  10. System.out.println("當前正在計算員工: " + id + "的稅後工資");
  11. double salary = employee.calculateSalary(id);
  12. System.out.println("計算員工: " + id + "的稅後工資結束");
  13. // 在呼叫目標類方法之後記錄日誌
  14. return salary;
  15. }
  16. }

動態代理

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

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

動態代理示例

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

介面的抽象

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

  1. public interface SalaryCalculator {
  2. double calculateSalary(int id);
  3. }

介面的實現

  1. public class EmployeeSalaryCalculator implements SalaryCalculator{
  2. public double calculateSalary(int id){
  3. return 100;
  4. }
  5. }
  1. public class ManagerSalaryCalculator implements SalaryCalculator{
  2. public double calculateSalary(int id){
  3. return 1000000;
  4. }
  5. }

建立動態代理的InvocationHandler

  1. public class SalaryLogProxy implements InvocationHandler {
  2. private SalaryCalculator calculator;
  3. public SalaryLogProxy(SalaryCalculator calculator) {
  4. this.calculator = calculator;
  5. }
  6. @Override
  7. public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
  8. System.out.println("--------------begin-------------");
  9. Object invoke = method.invoke(subject, args);
  10. System.out.println("--------------end-------------");
  11. return invoke;
  12. }
  13. }

建立代理物件

  1. public class Main {
  2. public static void main(String[] args) {
  3. SalaryCalculator calculator = new ManagerSalaryCalculator();
  4. InvocationHandler calculatorProxy = new SalaryLogProxy(subject);
  5. SalaryCalculator proxyInstance = (SalaryCalculator) Proxy.newProxyInstance(calculatorProxy.getClass().getClassLoader(), subject.getClass().getInterfaces(), calculatorProxy);
  6. proxyInstance.calculateSalary(1);
  7. }
  8. }

動態代理原始碼分析

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

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

Proxy.newProxyInstance

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

  • 可以看到Proxy.newProxyInstance一共做了兩件事情:1.獲取代理物件類的建構函式,2:根據建構函式例項化代理物件。
  1. @CallerSensitive
  2. public static Object newProxyInstance(ClassLoader loader,
  3. Class<?>[] interfaces, InvocationHandler h) {
  4. Objects.requireNonNull(h);
  5. final Class<?> caller = System.getSecurityManager() == null
  6. ? null : Reflection.getCallerClass();
  7. /*
  8. * Look up or generate the designated proxy class and its constructor.
  9. */
  10. // 獲取代理物件類的建構函式,裡面就包含了代理物件類的構建和載入
  11. Constructor<?> cons = getProxyConstructor(caller, loader, interfaces);
  12. // 根據建構函式生成代理例項.
  13. return newProxyInstance(caller, cons, h);
  14. }

代理物件類

通過檢視原始碼,我們可以發現代理物件類都extend了Proxy類並實現了指定介面中的方法。由於java不能多繼承,這裡已經繼承了Proxy類了,不能再繼承其他的類。所以JDK的動態代理不支援對實現類的代理,只支援介面的代理。

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

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