代理模式是软件开发中一种经典的设计模式,它通过引入 "代理对象" 间接访问目标对象,从而在不修改目标对象代码的前提下,实现功能增强(如日志记录、事务管理)、权限控制等横切需求。从简单的静态代理到灵活的动态代理,再到 Spring AOP 的工业化实现,代理模式的演进极大地提升了代码的可扩展性和可维护性。本文将从原理到实践,全面解析代理模式的各种实现方式及其在 Spring AOP 中的应用。
一、代理模式的核心思想
代理模式的本质是 "控制访问":通过代理对象作为目标对象的 "中间人",所有对目标对象的访问都必须经过代理,从而在代理中嵌入额外逻辑(如前置检查、后置处理)。其核心价值在于:
- 解耦:将核心业务逻辑与横切关注点(如日志、事务)分离,符合 "单一职责原则";
- 增强:在不修改目标对象代码的前提下,动态扩展功能;
- 隔离:通过代理隔离客户端与目标对象,保护目标对象的直接访问(如远程代理中隐藏网络通信细节)。
代理模式的通用结构包含三个角色:
- 抽象接口(Subject):定义目标对象和代理对象的共同行为,是代理模式的 "契约";
- 目标对象(Target):实现抽象接口,包含核心业务逻辑,是被代理的对象;
- 代理对象(Proxy):实现抽象接口,持有目标对象的引用,在调用目标方法前后嵌入增强逻辑。
二、静态代理:编译时确定的代理关系
静态代理是代理模式最基础的实现方式,其代理类在编译期就已确定,与目标对象的关系是 "硬编码" 的。
1. 静态代理的实现原理
静态代理要求代理类与目标对象实现相同的抽象接口,代理类内部持有目标对象的实例,在重写的接口方法中调用目标对象的对应方法,并在调用前后添加增强逻辑。
实现步骤:
- 定义抽象接口(Subject):规范目标对象和代理对象的行为;
- 实现目标对象(Target):完成核心业务逻辑;
- 实现代理对象(Proxy):持有目标对象引用,在方法中嵌入增强逻辑;
- 客户端通过代理对象访问目标功能。
2. 静态代理实战案例:日志增强
以 "给用户服务添加操作日志" 为例,演示静态代理的实现。
(1)抽象接口:定义用户服务行为
java
// 抽象接口(Subject)
public interface UserService {
void login(String username); // 登录方法
void logout(); // 登出方法
}
(2)目标对象:实现核心业务逻辑
java
// 目标对象(Target)
public class UserServiceImpl implements UserService {
@Override
public void login(String username) {
System.out.println("用户[" + username + "]登录成功");
}
@Override
public void logout() {
System.out.println("用户登出成功");
}
}
(3)代理对象:嵌入日志增强逻辑
java
// 代理对象(Proxy)
public class UserServiceProxy implements UserService {
// 持有目标对象引用
private UserService target;
// 通过构造器注入目标对象
public UserServiceProxy(UserService target) {
this.target = target;
}
@Override
public 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");
}
@Override
public void logout() {
// 前置增强
System.out.println("【日志】登出方法开始执行");
// 调用目标方法
target.logout();
// 后置增强
System.out.println("【日志】登出方法执行结束");
}
}
(4)客户端调用
java
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
:封装增强逻辑
java
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 目标方法返回值
*/
@Override
public 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:生成代理对象并调用
java
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. 动态生成代理对象
// 参数:类加载器、目标接口数组、InvocationHandler
UserService 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)
XML
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>3.3.0</version>
</dependency>
步骤 2:定义无接口的目标类
java
// 无接口的目标类
public class OrderService {
public void createOrder(String goods) {
System.out.println("订单创建成功,商品:" + goods);
}
public void cancelOrder(Long orderId) {
System.out.println("订单[" + orderId + "]取消成功");
}
}
步骤 3:实现MethodInterceptor
:定义拦截逻辑
java
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 目标方法返回值
*/
@Override
public 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:生成代理对象并调用
java
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)
XML
<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)
java
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:定义切面类(日志切面)
java
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 并测试
java
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)嵌入代理对象的方法中;
- 客户端从容器中获取的是代理对象,所有方法调用均通过代理执行。