首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 相關知識

Java 設計模式深度解析:Proxy 代理模式原理與靜態/動態代理完整實作

     編輯:Java 相關知識

前言:什麼是代理模式?

代理模式為某物件提供一個替身(代理),控制對原物件的存取。生活中無處不在:火車票代售點(代理火車站賣票)、VPN 翻牆(代理存取受限網站)、資料庫連線池(代理 Connection.close 歸還而非關閉)。

一、代理模式四大角色

  • Subject(抽象主題):定義真實主題與代理的共同介面
  • RealSubject(真實主題):實際執行業務邏輯的物件
  • Proxy(代理):持有 RealSubject 參考,控制存取、可增強行為
  • Client(客戶端):透過 Proxy 間接操作 RealSubject

二、靜態代理:編譯期決定

手寫代理類,適用於介面固定、方法較少的場景。

範例:延遲載入圖片

// 1. 抽象主題
public interface Image {
    void display();
}

// 2. 真實主題:載入大圖耗時
public class RealImage implements Image {
    private String filename;
    public RealImage(String filename) {
        this.filename = filename;
        loadFromDisk(); // 耗時操作
    }
    private void loadFromDisk() { System.out.println("載入圖片:" + filename); }
    @Override public void display() { System.out.println("顯示:" + filename); }
}

// 3. 靜態代理:首次 display 才載入
public class ProxyImage implements Image {
    private RealImage realImage;
    private String filename;
    public ProxyImage(String filename) { this.filename = filename; }
    @Override
    public void display() {
        if (realImage == null) realImage = new RealImage(filename); // 惰性載入
        realImage.display();
    }
}

// 4. 客戶端
public class Client {
    public static void main(String[] args) {
        Image img = new ProxyImage("huge_photo.jpg");
        System.out.println("代理建立完成,尚未載入圖片");
        img.display(); // 此時才真正載入
        img.display(); // 第二次直接使用快取
    }
}

靜態代理缺點

  • 真實主題每增加一個方法,代理就要同步增加 → 維護成本高
  • 介面變更需修改兩邊代碼
  • 無法在運行期動態決定代理邏輯

三、JDK 動態代理:運行期生成位元碼

核心類:java.lang.reflect.Proxy、InvocationHandler。前提:被代理類必須實作介面。

核心 API

public static Object newProxyInstance(
    ClassLoader loader,          // 類載入器
    Class[] interfaces,       // 代理要實作的介面陣列
    InvocationHandler h          // 方法攔截處理器
)

完整實作:記錄方法執行時間

// 1. 定義業務介面
public interface UserService {
    void addUser(String name);
    String getUser(int id);
    void deleteUser(int id);
}

// 2. 真實實作
public class UserServiceImpl implements UserService {
    @Override public void addUser(String name) { 
        System.out.println("新增用戶:" + name); 
    }
    @Override public String getUser(int id) { 
        return "User_" + id; 
    }
    @Override public void deleteUser(int id) { 
        System.out.println("刪除用戶:" + id); 
    }
}

// 3. 通用攔截器:橫切關注點
public class LoggingHandler implements InvocationHandler {
    private final Object target;
    public LoggingHandler(Object target) { this.target = target; }
    
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        long start = System.nanoTime();
        System.out.println("▶ 開始執行:" + method.getName());
        Object result = method.invoke(target, args); // 反射呼叫真實方法
        long cost = System.nanoTime() - start;
        System.out.println("■ 結束執行:" + method.getName() + " 耗時 " + cost/1_000_000 + " ms");
        return result;
    }
}

// 4. 客戶端使用
public class DynamicProxyDemo {
    public static void main(String[] args) {
        UserService real = new UserServiceImpl();
        UserService proxy = (UserService) Proxy.newProxyInstance(
            UserService.class.getClassLoader(),
            new Class[]{UserService.class},
            new LoggingHandler(real)
        );
        
        proxy.addUser("Alice");
        System.out.println("查詢結果:" + proxy.getUser(101));
        proxy.deleteUser(101);
    }
}

JDK 動態代理源碼核心流程

  1. Proxy.newProxyInstance → getProxyClass0 獲取/生成代理 Class
  2. ProxyGenerator.generateProxyClass 依介面動態產生 class $Proxy0 extends Proxy implements YourInterface
  3. 代理類每個方法皆委派給 InvocationHandler.invoke()
  4. 靜態初始化區塊 (<clinit>) 緩存 Method 物件供 invoke 使用

四、CGLIB 代理:無介面也能代理

原理:繼承目標類並覆寫非 final 方法。適用於沒有實作介面的類或需要代理具體類的場景(Spring AOP 預設策略)。

Maven 依賴

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

實作範例

public class UserServiceCglib {  // 無介面的具體類
    public void addUser(String name) { System.out.println("CGLIB 新增:" + name); }
    public String getUser(int id) { return "CGLIB_User_" + id; }
}

public class CglibProxyDemo {
    public static void main(String[] args) {
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(UserServiceCglib.class);
        enhancer.setCallback((MethodInterceptor) (obj, method, args1, proxy) -> {
            System.out.println("【CGLIB】前置增強:" + method.getName());
            Object result = proxy.invokeSuper(obj, args1); // 呼叫父類方法
            System.out.println("【CGLIB】後置增強:" + method.getName());
            return result;
        });
        
        UserServiceCglib proxy = (UserServiceCglib) enhancer.create();
        proxy.addUser("Bob");
        System.out.println(proxy.getUser(202));
    }
}

五、三種代理對比選型指南

維度靜態代理JDK 動態代理CGLIB 代理
需實作介面是強制否(繼承類)
代理 final 方法可控制不可(介面無 final)不可(無法覆寫)
生成時機編譯期運行期運行期
效能最佳(直接呼叫)反射呼叫有開銷FastClass 索引較快
適用場景固定介面、少量方法介面導向、AOP 切面無介面類、第三方庫擴充
Spring 預設-有介面時無介面時

六、實戰應用場景

  • Spring AOP:@Transactional、@Cacheable、@Async 底層皆為動態代理
  • MyBatis Mapper:介面無實作類,JDK 代理自動生成 SQL 執行邏輯
  • RPC 框架:Dubbo/gRPC 客戶端 Stub 為代理,網路通訊透明化
  • 連線池:代理 Connection.close() 實為歸還池中而非真正關閉
  • 延遲載入:Hibernate Proxy、圖片載入、大物件建立
  • 權限控制:方法前檢查角色、記錄審計日誌

七、注意事項與坑點

  1. this 參考問題:目標物件內部呼叫 this.method() 繞過代理 → 解決:注入 Self 參考或使用 AopContext.currentProxy()
  2. 序列化:JDK Proxy 實作 Serializable 需額外處理
  3. 效能敏感路徑:高頻呼叫考慮靜態代理或手寫字節碼 (ASM/ByteBuddy)
  4. CGLIB 限制:無法代理 final 類/方法、構造函數會被呼叫兩次

結語

代理模式是「關注點分離」的利器,將核心業務與橫切邏輯(日誌、事務、權限、快取)解耦。掌握 JDK 動態代理與 CGLIB 的原理差異,能讓你在框架選型、效能調優、疑難排查時遊刃有餘。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved