代理模式是軟件開發中一種經典的設計模式,它通過引入 "代理對象" 間接訪問目標對象,從而在不修改目標對象代碼的前提下,實現功能增強(如日志記錄、事務管理)、權限控制等橫切需求。從簡單的靜態代理到靈活的動態代理,再到 Spring AOP 的工業化實現,代理模式的演進極大地提升了代碼的可擴展性和可維護性。本文將從原理到實踐,全面解析代理模式的各種實現方式及其在 Spring AOP 中的應用。
一、代理模式的核心思想
代理模式的本質是 "控制訪問":通過代理對象作為目標對象的 "中間人",所有對目標對象的訪問都必須經過代理,從而在代理中嵌入額外邏輯(如前置檢查、后置處理)。其核心價值在于:
- 解耦:將核心業務邏輯與橫切關注點(如日志、事務)分離,符合 "單一職責原則";
- 增強:在不修改目標對象代碼的前提下,動態擴展功能;
- 隔離:通過代理隔離客戶端與目標對象,保護目標對象的直接訪問(如遠程代理中隱藏網絡通信細節)。
代理模式的通用結構包含三個角色:
- 抽象接口(Subject):定義目標對象和代理對象的共同行為,是代理模式的 "契約";
- 目標對象(Target):實現抽象接口,包含核心業務邏輯,是被代理的對象;
- 代理對象(Proxy):實現抽象接口,持有目標對象的引用,在調用目標方法前后嵌入增強邏輯。
二、靜態代理:編譯時確定的代理關系
靜態代理是代理模式最基礎的實現方式,其代理類在編譯期就已確定,與目標對象的關系是 "硬編碼" 的。
1. 靜態代理的實現原理
靜態代理要求代理類與目標對象實現相同的抽象接口,代理類內部持有目標對象的實例,在重寫的接口方法中調用目標對象的對應方法,并在調用前后添加增強邏輯。
實現步驟:
- 定義抽象接口(Subject):規范目標對象和代理對象的行為;
- 實現目標對象(Target):完成核心業務邏輯;
- 實現代理對象(Proxy):持有目標對象引用,在方法中嵌入增強邏輯;
- 客戶端通過代理對象訪問目標功能。
2. 靜態代理實戰案例:日志增強
以 "給用戶服務添加操作日志" 為例,演示靜態代理的實現。
(1)抽象接口:定義用戶服務行為
// 抽象接口(Subject)
public interface UserService {void login(String username); // 登錄方法void logout(); // 登出方法
}
(2)目標對象:實現核心業務邏輯
// 目標對象(Target)
public class UserServiceImpl implements UserService {@Overridepublic void login(String username) {System.out.println("用戶[" + username + "]登錄成功");}@Overridepublic void logout() {System.out.println("用戶登出成功");}
}
(3)代理對象:嵌入日志增強邏輯
// 代理對象(Proxy)
public class UserServiceProxy implements UserService {// 持有目標對象引用private UserService target;// 通過構造器注入目標對象public UserServiceProxy(UserService target) {this.target = target;}@Overridepublic void login(String username) {// 前置增強:記錄開始時間long start = System.currentTimeMillis();System.out.println("【日志】登錄方法開始執行,參數:" + username);// 調用目標方法target.login(username);// 后置增強:記錄結束時間和耗時long end = System.currentTimeMillis();System.out.println("【日志】登錄方法執行結束,耗時:" + (end - start) + "ms");}@Overridepublic void logout() {// 前置增強System.out.println("【日志】登出方法開始執行");// 調用目標方法target.logout();// 后置增強System.out.println("【日志】登出方法執行結束");}
}
(4)客戶端調用
public class Client {public static void main(String[] args) {// 創建目標對象UserService target = new UserServiceImpl();// 創建代理對象(傳入目標對象)UserService proxy = new UserServiceProxy(target);// 通過代理對象調用方法proxy.login("zhangsan");System.out.println("-----");proxy.logout();}
}
執行結果:
【日志】登錄方法開始執行,參數:zhangsan
用戶[zhangsan]登錄成功
【日志】登錄方法執行結束,耗時:1ms
-----
【日志】登出方法開始執行
用戶登出成功
【日志】登出方法執行結束
3. 靜態代理的優缺點
優點
- 實現簡單:邏輯直觀,易于理解和調試;
- 性能較好:編譯期確定代理關系,運行時無額外開銷。
缺點:
- 代碼冗余:每一個目標類都需要對應一個代理類,類數量爆炸;
- 維護成本高:目標類新增 / 修改方法時,代理類必須同步修改,違反 "開閉原則";
- 靈活性差:代理邏輯固定,無法動態切換(如不同場景需要不同增強邏輯時,需創建多個代理類)。
靜態代理僅適用于目標類少、方法固定的簡單場景(如固定第三方接口的適配),在復雜系統中難以應用。
三、動態代理:運行時生成的靈活代理
動態代理解決了靜態代理的局限性,其核心是在運行時動態生成代理類,代理關系在程序運行時才確定,無需手動編寫代理類代碼。Java 中動態代理的主流實現有兩種:JDK Proxy(基于接口)和 CGLib(基于繼承)。
1. JDK Proxy:基于接口的動態代理
JDK Proxy 是 Java 原生支持的動態代理方式,通過java.lang.reflect.Proxy
類和InvocationHandler
接口實現,僅能代理實現了接口的目標類。
(1)JDK Proxy 的核心原理
JDK Proxy 的工作流程可概括為:
- 客戶端通過
Proxy.newProxyInstance()
方法請求生成代理對象; - JVM 在運行時動態生成一個代理類的字節碼(繼承
Proxy
類,實現目標接口); - 代理類的所有方法都會委托給
InvocationHandler
的invoke()
方法; - 在
invoke()
方法中,開發者可嵌入增強邏輯,再通過反射調用目標對象的方法。
關鍵機制:
- 動態生成的代理類名稱格式為
$ProxyN
(N 為數字),由sun.misc.ProxyGenerator
生成字節碼; - 代理類持有
InvocationHandler
實例,通過它轉發所有方法調用; - 利用反射(
Method.invoke()
)調用目標方法,這是 JDK Proxy 性能開銷的主要來源。
(2)JDK Proxy 實戰:通用日志代理
基于 JDK Proxy 實現一個通用的日志代理,可對任意接口的目標對象添加日志增強。
步驟 1:定義接口和目標類(復用靜態代理中的UserService
和UserServiceImpl
)
步驟 2:實現InvocationHandler
:封裝增強邏輯
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;// 日志增強處理器
public class LogInvocationHandler implements InvocationHandler {// 目標對象(被代理的對象)private Object target;public LogInvocationHandler(Object target) {this.target = target;}/*** 代理對象的所有方法調用都會轉發到這里* @param proxy 代理對象本身* @param method 目標方法* @param args 目標方法參數* @return 目標方法返回值*/@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {// 前置增強:記錄方法開始日志System.out.println("【JDK Proxy日志】方法[" + method.getName() + "]開始執行,參數:" + Arrays.toString(args));// 反射調用目標方法Object result = method.invoke(target, args);// 后置增強:記錄方法結束日志System.out.println("【JDK Proxy日志】方法[" + method.getName() + "]執行結束,返回值:" + result);return result;}
}
步驟 3:生成代理對象并調用
import java.lang.reflect.Proxy;public class JdkProxyClient {public static void main(String[] args) {// 1. 創建目標對象UserService target = new UserServiceImpl();// 2. 創建InvocationHandler(傳入目標對象)LogInvocationHandler handler = new LogInvocationHandler(target);// 3. 動態生成代理對象// 參數:類加載器、目標接口數組、InvocationHandlerUserService proxy = (UserService) Proxy.newProxyInstance(target.getClass().getClassLoader(),target.getClass().getInterfaces(),handler);// 4. 通過代理對象調用方法proxy.login("lisi");System.out.println("-----");proxy.logout();}
}
執行結果:
【JDK Proxy日志】方法[login]開始執行,參數:[lisi]
用戶[lisi]登錄成功
【JDK Proxy日志】方法[login]執行結束,返回值:null
-----
【JDK Proxy日志】方法[logout]開始執行,參數:null
用戶登出成功
【JDK Proxy日志】方法[logout]執行結束,返回值:null
(3)JDK Proxy 的特點
- 接口依賴:必須代理實現了接口的類,無法代理純類(無接口);
- 動態生成:代理類在運行時生成,無需手動編寫;
- 反射調用:通過
Method.invoke()
調用目標方法,性能略低于直接調用; - 原生支持:無需額外依賴,Java 核心庫自帶。
2. CGLib:基于繼承的動態代理
CGLib(Code Generation Library)是一個第三方字節碼操作庫,通過生成目標類的子類實現代理,無需目標類實現接口,彌補了 JDK Proxy 的局限性。
(1)CGLib 的核心原理
CGLib 的工作流程:
- 通過
Enhancer
類指定目標類作為父類; - 實現
MethodInterceptor
接口,定義方法攔截邏輯; - CGLib 使用 ASM 框架動態生成目標類的子類(代理類),重寫父類的非 final 方法;
- 代理類的方法被調用時,會觸發
MethodInterceptor
的intercept()
方法,在此嵌入增強邏輯。
關鍵機制:
- FastClass 機制:為目標類和代理類生成一個 "FastClass",通過方法索引直接調用目標方法,避免反射開銷,性能優于 JDK Proxy;
- 字節碼操作:通過 ASM 框架直接操作字節碼生成代理類,無需源碼;
- 繼承限制:無法代理 final 類(無法繼承)和 final 方法(無法重寫)。
(2)CGLib 實戰:代理無接口的類
以一個無接口的OrderService
為例,演示 CGLib 代理。
步驟 1:引入 CGLib 依賴(Maven)
<dependency><groupId>cglib</groupId><artifactId>cglib</artifactId><version>3.3.0</version>
</dependency>
步驟 2:定義無接口的目標類
// 無接口的目標類
public class OrderService {public void createOrder(String goods) {System.out.println("訂單創建成功,商品:" + goods);}public void cancelOrder(Long orderId) {System.out.println("訂單[" + orderId + "]取消成功");}
}
步驟 3:實現MethodInterceptor
:定義攔截邏輯
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;// 訂單服務攔截器(增強邏輯)
public class OrderServiceInterceptor implements MethodInterceptor {/*** 代理類方法被調用時觸發* @param obj 代理對象(子類實例)* @param method 目標方法(父類方法)* @param args 方法參數* @param proxy 方法代理對象(用于調用父類方法)* @return 目標方法返回值*/@Overridepublic Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {// 前置增強:記錄開始日志System.out.println("【CGLib日志】方法[" + method.getName() + "]開始執行,參數:" + Arrays.toString(args));// 調用目標方法(通過MethodProxy調用父類方法,比反射高效)Object result = proxy.invokeSuper(obj, args);// Object invoke = method.invoke(t, args);// 作用等同與上面。// 后置增強:記錄結束日志System.out.println("【CGLib日志】方法[" + method.getName() + "]執行結束");return result;}
}
步驟 4:生成代理對象并調用
import net.sf.cglib.proxy.Enhancer;public class CglibProxyClient {public static void main(String[] args) {// 1. 創建增強器(用于生成代理類)Enhancer enhancer = new Enhancer();// 2. 設置父類(目標類)enhancer.setSuperclass(OrderService.class);// 3. 設置攔截器(增強邏輯)enhancer.setCallback(new OrderServiceInterceptor());// 4. 生成代理對象(目標類的子類)OrderService proxy = (OrderService) enhancer.create();// 5. 調用代理對象方法proxy.createOrder("手機");System.out.println("-----");proxy.cancelOrder(1001L);}
}
執行結果:
【CGLib日志】方法[createOrder]開始執行,參數:[手機]
訂單創建成功,商品:手機
【CGLib日志】方法[createOrder]執行結束
-----
【CGLib日志】方法[cancelOrder]開始執行,參數:[1001]
訂單[1001]取消成功
【CGLib日志】方法[cancelOrder]執行結束
(3)CGLib 的特點
- 無接口依賴:可代理任意非 final 類,無需實現接口;
- 性能優勢:通過 FastClass 機制避免反射,調用效率高于 JDK Proxy;
- 字節碼操作:依賴 ASM 框架生成字節碼,實現復雜;
- 繼承限制:無法代理 final 類或方法(因無法生成子類或重寫方法)。
3. JDK Proxy vs CGLib:核心差異對比
維度 | JDK Proxy | CGLib |
---|---|---|
底層原理 | 實現目標接口(接口代理) | 繼承目標類(子類代理) |
目標類要求 | 必須實現接口 | 不能是 final 類,方法不能是 final |
性能 | 反射調用,性能中等 | FastClass 機制,性能更高(尤其是多次調用) |
依賴 | Java 原生支持,無額外依賴 | 需引入 CGLib 和 ASM 依賴 |
生成代理類時間 | 較快(僅生成接口實現類) | 較慢(需生成子類字節碼) |
適用場景 | 目標類已實現接口 | 目標類無接口或為純類 |
四、靜態代理與動態代理:如何選擇?
靜態代理和動態代理的核心差異在于代理類的生成時機(編譯期 vs 運行時),選擇時需結合場景:
1. 靜態代理的適用場景
- 目標類數量少且固定(如固定的第三方接口適配);
- 增強邏輯簡單且不常變更(如簡單的參數校驗);
- 對性能要求極高(無運行時生成代理類的開銷)。
2. 動態代理的適用場景
- 目標類數量多或不確定(如框架中通用增強,如 Spring 事務);
- 增強邏輯需要動態切換(如不同環境下的日志級別切換);
- 需代理無接口的類(如遺留系統中的純類)。
總結:日常開發中,動態代理(尤其是結合框架的實現)應用更廣泛,靜態代理僅在簡單場景下使用。
五、Spring AOP:動態代理的工業化實現
Spring AOP(面向切面編程)是代理模式的工業化應用,它基于動態代理(JDK Proxy 或 CGLib)實現橫切關注點的模塊化,是 Spring 框架的核心特性之一。
1. AOP 核心概念
AOP 通過以下概念描述橫切邏輯的設計與織入:
- 切面(Aspect):封裝橫切關注點的模塊(如日志切面、事務切面),由切點和通知組成;
- 連接點(Join Point):程序執行過程中的可插入點(如方法調用、異常拋出),Spring AOP 僅支持方法級連接點;
- 切點(Pointcut):篩選連接點的條件(如 "所有被 @Transactional 注解的方法");
- 通知(Advice):切面在連接點執行的邏輯,包括前置通知(@Before)、后置通知(@After)、環繞通知(@Around)等;
- 織入(Weaving):將切面邏輯嵌入目標對象的過程(Spring AOP 在運行時織入)。
2. Spring AOP 的代理選擇策略
Spring AOP 默認根據目標類是否實現接口選擇代理方式:
- 若目標類實現了接口:使用 JDK Proxy 生成代理;
- 若目標類未實現接口:使用 CGLib 生成代理;
- 可通過
proxy-target-class="true"
強制使用 CGLib(如@EnableAspectJAutoProxy(proxyTargetClass = true)
)。
3. Spring AOP 實戰:基于注解的切面
以 "用戶服務的操作日志記錄" 為例,演示 Spring AOP 的實現。
步驟 1:引入 Spring AOP 依賴(Maven)
<dependency><groupId>org.springframework</groupId><artifactId>spring-context</artifactId><version>5.3.20</version>
</dependency>
<dependency><groupId>org.springframework</groupId><artifactId>spring-aop</artifactId><version>5.3.20</version>
</dependency>
<dependency><groupId>org.aspectj</groupId><artifactId>aspectjweaver</artifactId><version>1.9.9.1</version>
</dependency>
步驟 2:定義目標服務(UserService)
import org.springframework.stereotype.Service;@Service // 交由Spring管理
public class UserService {public void login(String username) {System.out.println("用戶[" + username + "]登錄成功");}public void logout() {System.out.println("用戶登出成功");}
}
步驟 3:定義切面類(日志切面)
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
import java.util.Arrays;@Aspect // 標識為切面類
@Component // 交由Spring管理
public class LogAspect {// 定義切點:匹配UserService中的所有方法@Pointcut("execution(* com.example.aop.UserService.*(..))")public void userServicePointcut() {}// 前置通知:方法執行前觸發@Before("userServicePointcut()")public void beforeAdvice(JoinPoint joinPoint) {String methodName = joinPoint.getSignature().getName();Object[] args = joinPoint.getArgs();System.out.println("【AOP前置通知】方法[" + methodName + "]參數:" + Arrays.toString(args));}// 后置通知:方法執行后觸發(無論是否異常)@After("userServicePointcut()")public void afterAdvice(JoinPoint joinPoint) {String methodName = joinPoint.getSignature().getName();System.out.println("【AOP后置通知】方法[" + methodName + "]執行結束");}// 環繞通知:包圍方法執行,可控制是否執行目標方法@Around("userServicePointcut()")public Object aroundAdvice(ProceedingJoinPoint joinPoint) throws Throwable {String methodName = joinPoint.getSignature().getName();long start = System.currentTimeMillis();// 執行目標方法(必須調用,否則目標方法不執行)Object result = joinPoint.proceed();long end = System.currentTimeMillis();System.out.println("【AOP環繞通知】方法[" + methodName + "]耗時:" + (end - start) + "ms");return result;}
}
步驟 4:配置 Spring 并測試
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.EnableAspectJAutoProxy;@EnableAspectJAutoProxy // 開啟AOP注解支持
@ComponentScan("com.example.aop") // 掃描組件
public class SpringAopClient {public static void main(String[] args) {// 初始化Spring容器AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(SpringAopClient.class);// 獲取代理對象(注意:此時獲取的是代理對象,而非原始UserService)UserService userService = context.getBean(UserService.class);// 調用方法userService.login("wangwu");System.out.println("-----");userService.logout();}
}
執行結果:
【AOP前置通知】方法[login]參數:[wangwu]
【AOP環繞通知】方法[login]開始執行
用戶[wangwu]登錄成功
【AOP環繞通知】方法[login]耗時:2ms
【AOP后置通知】方法[login]執行結束
-----
【AOP前置通知】方法[logout]參數:[]
【AOP環繞通知】方法[logout]開始執行
用戶登出成功
【AOP環繞通知】方法[logout]耗時:1ms
【AOP后置通知】方法[logout]執行結束
4. Spring AOP 的底層實現
Spring AOP 的織入過程本質是動態代理的生成過程:
- 容器啟動時,掃描所有
@Aspect
注解的切面類; - 解析切點表達式,匹配需要被增強的目標類方法;
- 對匹配的目標類,根據是否實現接口選擇 JDK Proxy 或 CGLib 生成代理對象;
- 將切面中的通知邏輯(Advice)嵌入代理對象的方法中;
- 客戶端從容器中獲取的是代理對象,所有方法調用均通過代理執行。