代理模式(Proxy Pattern)¶
一句话记忆口诀:代理控制访问,静态代理编译期织入,JDK 动态代理基于接口,CGLIB 基于继承,Spring AOP 优先 JDK 代理。
1. 引入:它解决了什么问题?¶
没有代理模式时的问题¶
在不修改原始类的前提下,需要为方法添加日志、权限校验、事务管理等横切逻辑时:
// ❌ 反例:直接在业务方法中混入非业务逻辑
public class OrderService {
public void createOrder(Order order) {
// 权限校验(非业务逻辑)
if (!SecurityContext.hasPermission("ORDER_CREATE")) {
throw new UnauthorizedException();
}
// 开启事务(非业务逻辑)
TransactionManager.begin();
try {
// 记录日志(非业务逻辑)
log.info("开始创建订单: {}", order.getId());
// ===== 真正的业务逻辑 =====
orderRepository.save(order);
inventoryService.deduct(order);
// ===========================
log.info("订单创建成功: {}", order.getId());
TransactionManager.commit();
} catch (Exception e) {
TransactionManager.rollback();
throw e;
}
}
// 每个方法都要重复这些非业务代码!
}
问题根因:业务逻辑与横切关注点(日志、事务、权限)混在一起,违反单一职责原则,且横切逻辑无法复用。
工作中的典型应用场景¶
| 场景 | Spring/JDK 中的例子 |
|---|---|
| AOP 事务管理 | @Transactional — Spring 动态代理 |
| 权限控制 | @PreAuthorize — Spring Security 代理 |
| 缓存 | @Cacheable — Spring Cache 代理 |
| 远程调用 | Feign Client — JDK 动态代理 |
| MyBatis Mapper | @Mapper 接口 — JDK 动态代理 |
2. 类比:用生活模型建立直觉¶
生活类比:明星经纪人¶
一位明星(真实对象)只负责表演(核心业务)。经纪人(代理)负责处理合同谈判、档期安排、费用收取(横切逻辑)。粉丝(调用方)联系经纪人,经纪人在适当时候让明星出场。
- 接口/抽象角色:演出合同(
Performer接口),定义"表演"行为 - 具体实现角色:明星本人(
RealStar),实现真正的表演 - 代理角色:经纪人(
AgentProxy),控制对明星的访问,在表演前后处理杂务 - 调用方:演出商(
Client),只与经纪人打交道
抽象定义¶
代理模式为另一个对象提供一个替身或占位符,以控制对这个对象的访问。
3. 原理:逐步拆解核心机制¶
UML 类图¶
classDiagram
class Subject {
<<interface>>
+request() void
}
class RealSubject {
+request() void
}
class Proxy {
-realSubject RealSubject
+request() void
-preProcess() void
-postProcess() void
}
class Client
Subject <|.. RealSubject
Subject <|.. Proxy
Proxy --> RealSubject : delegates
Client --> Subject
note for Proxy "代理和真实对象实现同一接口<br/>调用方无感知"
三种代理方式逐一对比¶
方式一:静态代理(编译期确定)¶
// ===== 公共接口 =====
public interface OrderService {
void createOrder(Order order);
Order getOrder(Long id);
}
// ===== 真实对象(核心业务)=====
public class OrderServiceImpl implements OrderService {
@Override
public void createOrder(Order order) {
System.out.println("创建订单: " + order.getId());
// 真正的业务逻辑
}
@Override
public Order getOrder(Long id) {
System.out.println("查询订单: " + id);
return new Order(id);
}
}
// ===== 静态代理(编译期确定,手动编写)=====
// 设计原因:在不修改 OrderServiceImpl 的前提下,添加日志功能
// 代价:每个接口都要写一个代理类,接口方法增加时代理类也要同步修改,维护成本高
public class LoggingOrderServiceProxy implements OrderService {
private final OrderService realService; // 持有真实对象的引用
public LoggingOrderServiceProxy(OrderService realService) {
this.realService = realService;
}
@Override
public void createOrder(Order order) {
System.out.println("[LOG] 开始创建订单");
long start = System.currentTimeMillis();
realService.createOrder(order); // 委托给真实对象
System.out.println("[LOG] 创建订单耗时: " + (System.currentTimeMillis() - start) + "ms");
}
@Override
public Order getOrder(Long id) {
System.out.println("[LOG] 开始查询订单");
Order order = realService.getOrder(id);
System.out.println("[LOG] 查询订单完成");
return order;
}
}
⚠️ 静态代理的代价:接口有 N 个方法,代理类就要实现 N 个方法,且每个方法都要写重复的前置/后置逻辑。如果有 100 个 Service 接口,就要写 100 个代理类。
方式二:JDK 动态代理(运行期生成,基于接口)¶
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
// ===== InvocationHandler:统一处理所有方法调用 =====
// 设计原因:运行期动态生成代理类,一个 Handler 可以代理任意接口
// 代价:被代理类必须实现接口(JDK 动态代理的核心限制)
// 原因:JDK 生成的代理类继承了 Proxy 类,Java 单继承限制导致无法再继承目标类
public class LoggingInvocationHandler implements InvocationHandler {
private final Object target; // 被代理的真实对象
public LoggingInvocationHandler(Object target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 前置处理(对所有方法生效,无需逐一实现)
System.out.println("[LOG] 调用方法: " + method.getName());
long start = System.currentTimeMillis();
// 通过反射调用真实对象的方法
Object result = method.invoke(target, args);
// 后置处理
System.out.println("[LOG] 方法耗时: " + (System.currentTimeMillis() - start) + "ms");
return result;
}
}
// ===== 使用 JDK 动态代理 =====
public class Main {
public static void main(String[] args) {
OrderService realService = new OrderServiceImpl();
// Proxy.newProxyInstance 在运行期动态生成代理类
// 参数1:类加载器(用于加载生成的代理类)
// 参数2:代理类要实现的接口列表
// 参数3:InvocationHandler(方法调用的处理器)
OrderService proxy = (OrderService) Proxy.newProxyInstance(
realService.getClass().getClassLoader(),
new Class[]{OrderService.class},
new LoggingInvocationHandler(realService)
);
proxy.createOrder(new Order(1L)); // 实际调用 InvocationHandler.invoke()
proxy.getOrder(1L);
}
}
JDK 动态代理的底层原理:
Proxy.newProxyInstance()在运行期通过ProxyGenerator生成一个继承Proxy类并实现目标接口的字节码,加载到 JVM 中。代理类的每个方法都调用InvocationHandler.invoke(),实现统一拦截。
方式三:CGLIB 动态代理(运行期生成,基于继承)¶
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
// ===== CGLIB MethodInterceptor =====
// 设计原因:目标类没有实现接口时,通过继承目标类生成子类代理
// 代价:
// 1. 目标类和方法不能是 final(final 类/方法无法被继承/覆盖)
// 2. 需要额外引入 CGLIB 依赖(Spring 已内置)
// 3. 生成子类的开销比 JDK 代理略大,但方法调用性能更好(FastClass 机制)
public class LoggingMethodInterceptor implements MethodInterceptor {
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy)
throws Throwable {
System.out.println("[CGLIB LOG] 调用方法: " + method.getName());
long start = System.currentTimeMillis();
// invokeSuper 调用父类(真实对象)的方法,避免反射开销
Object result = proxy.invokeSuper(obj, args);
System.out.println("[CGLIB LOG] 方法耗时: " + (System.currentTimeMillis() - start) + "ms");
return result;
}
}
// ===== 使用 CGLIB 代理 =====
public class Main {
public static void main(String[] args) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(OrderServiceImpl.class); // 设置父类(被代理类)
enhancer.setCallback(new LoggingMethodInterceptor());
// 生成 OrderServiceImpl 的子类实例
OrderServiceImpl proxy = (OrderServiceImpl) enhancer.create();
proxy.createOrder(new Order(1L));
}
}
核心流程图¶
flowchart TD
subgraph JDK 动态代理流程
A1[Client 调用 proxy.method] --> B1[代理类 invoke 方法]
B1 --> C1[InvocationHandler.invoke]
C1 --> D1[前置处理]
D1 --> E1[method.invoke 反射调用真实对象]
E1 --> F1[后置处理]
F1 --> G1[返回结果]
end
subgraph CGLIB 代理流程
A2[Client 调用 proxy.method] --> B2[子类覆盖方法]
B2 --> C2[MethodInterceptor.intercept]
C2 --> D2[前置处理]
D2 --> E2[proxy.invokeSuper 调用父类方法]
E2 --> F2[后置处理]
F2 --> G2[返回结果]
end
4. 特性:关键对比¶
三种代理方式对比¶
| 对比维度 | 静态代理 | JDK 动态代理 | CGLIB 动态代理 |
|---|---|---|---|
| 生成时机 | 编译期 | 运行期 | 运行期 |
| 实现机制 | 手动实现接口 | 实现接口,反射调用 | 继承目标类,字节码增强 |
| 接口要求 | 需要接口 | 必须有接口 | 不需要接口 |
| final 限制 | 无 | 无 | final 类/方法无法代理 |
| 性能 | 最好(直接调用) | 较好(JDK 8+ 优化后) | 好(FastClass 避免反射) |
| 维护成本 | 高(每个接口写一个代理类) | 低 | 低 |
| Spring 使用 | 不使用 | 有接口时优先使用 | 无接口时使用 |
代理模式 vs 装饰器模式(最容易混淆)¶
| 对比维度 | 代理模式 | 装饰器模式 |
|---|---|---|
| 目的 | 控制对对象的访问(权限、延迟加载) | 增强对象的功能 |
| 关系 | 代理通常自己创建真实对象 | 装饰器由外部传入被装饰对象 |
| 透明性 | 调用方不知道在访问代理 | 调用方知道在使用装饰器 |
| 典型例子 | Spring AOP、Feign Client | BufferedInputStream、Collections.unmodifiableList() |
Spring AOP 代理选择策略¶
flowchart TD
A[Spring 创建 Bean 代理] --> B{目标类是否实现接口?}
B -->|是| C{是否强制使用 CGLIB?}
C -->|否| D[使用 JDK 动态代理]
C -->|是 proxyTargetClass=true| E[使用 CGLIB 代理]
B -->|否| E
note1["Spring Boot 2.x 默认 proxyTargetClass=true<br/>即默认使用 CGLIB"]
⚠️ Spring Boot 2.x 的变化:Spring Boot 2.0 开始默认将
spring.aop.proxy-target-class=true,即默认使用 CGLIB 代理,即使目标类有接口。原因是避免某些场景下 JDK 代理导致的类型转换异常。
在 Spring / JDK 中的应用¶
| 框架/类 | 代理类型 | 说明 |
|---|---|---|
Spring @Transactional |
JDK/CGLIB | 事务管理 AOP |
Spring @Cacheable |
JDK/CGLIB | 缓存 AOP |
| Feign Client | JDK 动态代理 | 接口生成 HTTP 客户端 |
MyBatis @Mapper |
JDK 动态代理 | 接口生成 SQL 执行器 |
Collections.synchronizedList() |
静态代理 | 线程安全包装 |
5. 边界:异常情况与常见误区¶
误区一:Spring AOP 自调用失效(运行期问题)¶
// ❌ 错误:同一个类中方法 A 调用方法 B,B 上的 @Transactional 不生效
@Service
public class OrderService {
public void createOrder(Order order) {
// 直接调用同类方法,绕过了代理!
// this.saveOrder() 中的 this 是真实对象,不是代理对象
this.saveOrder(order);
}
@Transactional
public void saveOrder(Order order) {
// 事务不会生效!
orderRepository.save(order);
}
}
// 原因:Spring AOP 是基于代理的,只有通过代理对象调用方法才会触发拦截。
// 同类内部调用使用 this 引用,绕过了代理对象。
// ✅ 解决方案一:注入自身代理
@Service
public class OrderService {
@Autowired
private OrderService self; // 注入自身的代理对象
public void createOrder(Order order) {
self.saveOrder(order); // 通过代理调用,事务生效
}
@Transactional
public void saveOrder(Order order) {
orderRepository.save(order);
}
}
// ✅ 解决方案二:拆分到不同 Service 类
误区二:CGLIB 代理 final 类/方法(运行期异常)¶
// ❌ 错误:对 final 类使用 CGLIB 代理
@Service
public final class UserService { // final 类!
@Transactional
public void createUser(User user) { ... }
}
// 运行时抛出:Cannot subclass final class UserService
// 原因:CGLIB 通过继承生成子类,final 类无法被继承
// ✅ 正确:去掉 final 修饰符,或改用接口 + JDK 动态代理
public interface UserService {
void createUser(User user);
}
@Service
public class UserServiceImpl implements UserService {
@Transactional
public void createUser(User user) { ... }
}
误区三:JDK 动态代理强转为实现类(运行期 ClassCastException)¶
// ❌ 错误:将 JDK 动态代理对象强转为实现类
OrderService proxy = (OrderService) Proxy.newProxyInstance(...);
OrderServiceImpl impl = (OrderServiceImpl) proxy; // ClassCastException!
// 原因:JDK 动态代理生成的类继承自 Proxy,实现了 OrderService 接口,
// 但与 OrderServiceImpl 没有继承关系,强转失败
// ✅ 正确:只能转为接口类型
OrderService proxy = (OrderService) Proxy.newProxyInstance(...);
proxy.createOrder(order); // 通过接口调用
6. 总结:面试标准化表达¶
高频面试题¶
Q1:JDK 动态代理和 CGLIB 代理有什么区别?Spring 如何选择?
JDK 动态代理基于接口,运行期通过
Proxy.newProxyInstance()生成实现目标接口的代理类,目标类必须实现接口;CGLIB 基于继承,通过字节码增强生成目标类的子类,不需要接口,但 final 类/方法无法代理。Spring 的选择策略:Spring Boot 2.x 默认使用 CGLIB(proxyTargetClass=true);若显式配置proxyTargetClass=false,则有接口时用 JDK 代理,无接口时用 CGLIB。
Q2:Spring AOP 为什么会出现事务失效?
Spring AOP 基于代理实现,只有通过代理对象调用方法才会触发拦截逻辑(如事务管理)。当同一个类中方法 A 调用方法 B 时,使用的是
this引用(真实对象),绕过了代理,导致 B 上的@Transactional失效。解决方案:①将方法拆分到不同 Service 类;②通过ApplicationContext获取代理对象后调用;③注入自身代理(@Autowired注入自身)。
Q3:代理模式和装饰器模式有什么区别?
两者结构相似,都持有被包装对象的引用,但目的不同:代理模式的目的是控制访问,代理通常自己创建或持有真实对象,调用方不知道在访问代理(透明代理),典型例子是 Spring AOP、Feign Client;装饰器模式的目的是增强功能,被装饰对象由外部传入,调用方知道在使用装饰器,典型例子是
BufferedInputStream包装FileInputStream。
一句话记忆口诀:代理控制访问,JDK 代理基于接口(必须有接口),CGLIB 基于继承(不能 final),Spring Boot 2.x 默认 CGLIB,自调用会绕过代理导致 AOP 失效。