在 Spring AOP、RPC 框架、事務管理、日誌攔截等無數經典場景中,動態代理都是核心基石。相較於靜態代理(手寫代理類),動態代理能在運行時動態生成代理類字節碼,實現「寫一次攔截邏輯,處處復用」的極致解耦。本文將從角色劃分、核心 API、字節碼生成機制、實戰陷阱四個維度徹底講透 JDK 動態代理。
| 角色 | 靜態代理 | JDK 動態代理 | 本質 |
|---|---|---|---|
| 抽象角色 | 介面或抽象類 | 只能是介面 | 定義代理與真實對象的公共契約 |
| 真實角色 | 實現介面的具體類 | 實現介面的具體類 | 核心業務邏輯載體 |
| 代理角色 | 手寫固定類 | 運行時生成的 $ProxyN 類 | 委派給 InvocationHandler 處理 |
關鍵限制:JDK 動態代理強制要求目標類實現至少一個介面。若目標類未實現介面,必須改用 CGLIB(繼承子類代理)或 ByteBuddy 等字節碼操作庫。
@FunctionalInterface
public interface InvocationHandler {
/**
* @param proxy 代理對象自身引用(極少直接使用)
* @param method 被調用的介面方法(含聲明異常、泛型等元數據)
* @param args 實際參數值陣列
* @return 方法返回值
* @throws Throwable 任意異常,將拋回調用方
*/
Object invoke(Object proxy, Method method, Object[] args) throws Throwable;
}
每個代理對象持有一個 InvocationHandler 實例。當調用代理方法時,JVM 會將調用重定向到 invoke(),開發者在其中實現:前置增強、參數校驗、事務開啟、調用真實對象、後置增強、異常處理、結果包裝等邏輯。
public static Object newProxyInstance(
ClassLoader loader, // 代理類加載器,通常用目標類的 getClassLoader()
Class>[] interfaces, // 代理需實現的介面陣列(決定代理類類型)
InvocationHandler h // 方法調用處理器
) throws IllegalArgumentException
該方法內部流程:
com.sun.proxy.$ProxyN,實現所有指定介面)InvocationHandlerpublic interface UserService {
void register(String username, String password);
User findById(Long id);
}
public class UserServiceImpl implements UserService {
@Override
public void register(String username, String password) {
System.out.printf("[業務] 註冊用戶: %s%n", username);
// 模擬 DB 操作
}
@Override
public User findById(Long id) {
System.out.printf("[業務] 查詢用戶 ID: %d%n", id);
return new User(id, "user_" + id);
}
}
public class LoggingInterceptor implements InvocationHandler {
private final Object target;
public LoggingInterceptor(Object target) {
this.target = Objects.requireNonNull(target);
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
String methodName = method.getName();
long start = System.nanoTime();
try {
System.out.printf("[前置] %s 開始執行,參數: %s%n", methodName, Arrays.toString(args));
Object result = method.invoke(target, args); // 反射調用真實對象
System.out.printf("[後置] %s 執行成功,耗時: %.2f ms%n", methodName, (System.nanoTime() - start) / 1e6);
return result;
} catch (InvocationTargetException e) {
System.err.printf("[異常] %s 拋出異常: %s%n", methodName, e.getCause());
throw e.getCause(); // 拋出真實業務異常
}
}
}
public class Client {
public static void main(String[] args) {
UserService real = new UserServiceImpl();
UserService proxy = (UserService) Proxy.newProxyInstance(
real.getClass().getClassLoader(),
real.getClass().getInterfaces(), // 關鍵:代理類實現了相同介面
new LoggingInterceptor(real)
);
proxy.register("alice", "secret123");
User u = proxy.findById(1001L);
System.out.println("返回對象: " + u);
}
}
為何能強轉為 UserService? 生成的 $Proxy0 類聲明實現了 UserService 介面(由第二個參數決定),因此滿足「是一個」關係,編譯與運行期均合法。
可通過以下代碼將生成的代理類字節碼輸出到磁碟反編譯:
System.setProperty("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");
// 或 JVM 參數 -Dsun.misc.ProxyGenerator.saveGeneratedFiles=true
反編譯後可見 $Proxy0 大致結構:
public final class $Proxy0 extends Proxy implements UserService {
protected $Proxy0(InvocationHandler h) { super(h); }
public final void register(String username, String password) throws Throwable {
super.h.invoke(this, m1, new Object[]{username, password}); // m1 為 Method 對象
}
public final User findById(Long id) throws Throwable {
return (User) super.h.invoke(this, m2, new Object[]{id});
}
// 繼承自 Proxy 的 equals/hashCode/toString 也會被攔截
static { /* 初始化 Method 對象 m1, m2... */ }
}
關鍵發現:
InvocationHandler.invoke()Object 的 equals/hashCode/toString 也會被代理(需在攔截器中判斷處理)
if (method.getDeclaringClass() == Object.class) {
return method.invoke(this, args); // 直接走默認邏輯
}
catch (InvocationTargetException e) {
throw e.getCause(); // 而非 throw e,否則外層捕獲的是 InvocationTargetException
}
參數 proxy 即為代理對象自身,若在 invoke 內再次調用 proxy.someMethod() 將無限遞歸。
動態代理類實現 Serializable,但反序列化後 InvocationHandler 可能失效。分布式場景建議改用 Spring AOP(基於 CGLIB/AspectJ)或自定義序列化邏輯。
| 維度 | JDK 動態代理 | CGLIB |
|---|---|---|
| 前提條件 | 目標類實現介面 | 目標類非 final、方法非 final |
| 原理 | 生成實現介面的類 | 生成繼承目標類的子類 |
| final 方法 | 不受影響 | 無法代理(直接調用父類) |
| 效能(JDK 8+) | 極優(invokevirtual + JIT 內聯) | 略慢(FastClass 索引調用) |
| 適用場景 | Spring AOP 默認、RPC 框架 | 第三方庫無介面類、Spring Boot 2.x+ 默認 |
JDK 動態代理以「介面導向 + 運行時字節碼生成 + 委派模式」三大支柱,構建了 Java 生態中最輕量、最標準的 AOP 基礎設施。掌握其生成機制、類型轉換本質、異常傳播規則、序列化陷阱,是從「會用」進階到「精通」的必經之路。實際架構中,建議封裝統一的 ProxyFactory 工廠,屏蔽 JDK/CGLIB 差異,提供聲明式切面配置能力。