GoF 代理模式详解
一、什么是代理模式
代理模式(Proxy Pattern)是 GoF 23 种设计模式中的一种结构型模式。它的核心思想是:为其他对象提供一种代理,以控制对这个对象的访问。
通俗理解:你不想或不能直接访问某个对象时,通过一个"中间人"来访问。中间人可以在访问前后做一些额外处理,比如权限校验、日志记录、延迟加载、事务控制等。
生活中的例子:租房找中介。房东是真实对象,中介是代理。租客不直接找房东,而是通过中介看房、签合同。中介可以在带看前审核租客资质,签合同后收中介费。
二、代理模式的结构
代理模式包含三个角色:
| 角色 | 说明 |
|---|---|
| 抽象主题(Subject) | 定义真实对象和代理对象的公共接口 |
| 真实主题(RealSubject) | 真正执行业务逻辑的对象 |
| 代理(Proxy) | 持有真实对象的引用,控制对真实对象的访问 |
代理对象和真实对象实现同一个接口,客户端面向接口编程,不关心背后是真实对象还是代理。
三、静态代理
静态代理是最简单的实现方式:手动编写代理类,实现与目标类相同的接口,在方法调用前后插入增强逻辑。
代码示例
抽象主题:
java
public interface UserService {
void save(User user);
User findById(Long id);
}
真实主题:
java
public class UserServiceImpl implements UserService {
@Override
public void save(User user) {
System.out.println("保存用户:" + user.getName());
}
@Override
public User findById(Long id) {
System.out.println("查询用户:" + id);
return new User(id, "张三");
}
}
代理:
java
public class UserServiceProxy implements UserService {
private final UserService target;
public UserServiceProxy(UserService target) {
this.target = target;
}
@Override
public void save(User user) {
System.out.println("【日志】开始保存用户");
target.save(user);
System.out.println("【日志】保存用户完成");
}
@Override
public User findById(Long id) {
System.out.println("【日志】开始查询用户");
User user = target.findById(id);
System.out.println("【日志】查询用户完成");
return user;
}
}
客户端:
java
public class Client {
public static void main(String[] args) {
UserService target = new UserServiceImpl();
UserService proxy = new UserServiceProxy(target);
proxy.save(new User(1L, "张三"));
}
}
静态代理的缺点
- 类数量翻倍:每个目标类都要写一个代理类。
- 维护成本高:接口新增方法时,真实类和代理类都要修改。
- 重复代码:多个代理类的增强逻辑相似,无法复用。
静态代理只适合目标类少、增强逻辑简单的场景。
四、动态代理
动态代理在运行时动态生成代理类,不需要手动编写。Java 中有两种主流实现:JDK 动态代理和 CGLIB 动态代理。
1. JDK 动态代理
JDK 动态代理基于接口,要求目标类必须实现至少一个接口。核心是 java.lang.reflect.Proxy 和 InvocationHandler。
java
public class JdkProxyHandler implements InvocationHandler {
private final Object target;
public JdkProxyHandler(Object target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("【日志】调用方法:" + method.getName());
Object result = method.invoke(target, args);
System.out.println("【日志】方法执行完成");
return result;
}
}
生成代理对象:
java
UserService target = new UserServiceImpl();
UserService proxy = (UserService) Proxy.newProxyInstance(
target.getClass().getClassLoader(),
target.getClass().getInterfaces(),
new JdkProxyHandler(target)
);
proxy.save(new User(1L, "张三"));
特点:
- 基于接口,目标类必须实现接口。
- 代理类和目标类实现同一个接口。
- 通过
InvocationHandler.invoke()拦截所有方法调用。 - 生成代理类速度快,但调用性能略低于直接调用。
2. CGLIB 动态代理
CGLIB(Code Generation Library)通过继承目标类生成子类来实现代理。目标类不需要实现接口,但不能是 final 类,被代理的方法不能是 final 或 private。
java
public class CglibProxyInterceptor implements MethodInterceptor {
@Override
public Object intercept(Object obj, Method method, Object[] args,
MethodProxy proxy) throws Throwable {
System.out.println("【日志】调用方法:" + method.getName());
Object result = proxy.invokeSuper(obj, args);
System.out.println("【日志】方法执行完成");
return result;
}
}
生成代理对象:
java
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(UserServiceImpl.class);
enhancer.setCallback(new CglibProxyInterceptor());
UserServiceImpl proxy = (UserServiceImpl) enhancer.create();
proxy.save(new User(1L, "张三"));
特点:
- 基于继承,不需要接口。
- 代理类是目标类的子类。
- 无法代理 final 类和 final 方法。
- 生成代理类较慢,但调用性能优于 JDK 动态代理。
3. JDK 动态代理 vs CGLIB
| 对比维度 | JDK 动态代理 | CGLIB |
|---|---|---|
| 代理方式 | 基于接口 | 基于继承 |
| 目标类要求 | 必须实现接口 | 不能是 final 类 |
| 代理类 | 实现同一接口 | 继承目标类 |
| 方法限制 | 只能代理接口中定义的方法 | 不能代理 final/private 方法 |
| 性能 | 生成快,调用稍慢 | 生成慢,调用快 |
| 依赖 | JDK 内置 | 需要 CGLIB 库 |
| Spring 默认 | 有接口时使用 | 无接口时使用 |
五、代理模式在 Spring 中的应用
Spring AOP 的底层就是动态代理。当 Bean 需要被增强时(如 @Transactional、@Cacheable、@Async、自定义切面),Spring 会为 Bean 生成代理对象,放入容器。
1. 代理选择规则
- 目标类实现了接口,默认使用 JDK 动态代理。
- 目标类没有实现接口,使用 CGLIB。
- 可以通过
@EnableAspectJAutoProxy(proxyTargetClass = true)强制使用 CGLIB。 - Spring Boot 2.x 之后,AOP 自动配置默认
proxyTargetClass = true,即默认使用 CGLIB。
2. 代理对开发的影响
内部调用失效: 同一个类中方法 A 调用方法 B,如果 B 需要被 AOP 增强,不会生效。因为内部调用走的是原始对象,不是代理对象。
java
@Service
public class UserService {
public void methodA() {
methodB(); // 直接调用,不经过代理
}
@Transactional
public void methodB() {
// 事务不会生效
}
}
注入的是代理对象: 从容器中 getBean() 拿到的可能是代理对象,不是原始对象。所以 bean.getClass() 可能不是原始类。
3. Spring 中代理的典型场景
@Transactional:事务代理,在方法前后开启/提交/回滚事务。@Cacheable:缓存代理,先查缓存,未命中才执行方法。@Async:异步代理,把方法调用提交到线程池。- Spring Security:安全代理,方法调用前校验权限。
- 自定义 AOP 切面:日志、监控、限流等。
六、代理模式 vs 装饰器模式
两者都通过组合持有目标对象,都实现同一接口,容易混淆。
| 对比维度 | 代理模式 | 装饰器模式 |
|---|---|---|
| 目的 | 控制对目标对象的访问 | 动态给目标对象添加功能 |
| 关注点 | 访问控制、延迟加载、权限 | 功能增强、组合扩展 |
| 目标对象 | 通常自己创建或注入 | 由客户端传入 |
| 代理数量 | 通常一个代理对应一个目标 | 可以多层装饰 |
| 典型场景 | 事务、权限、远程调用 | IO 流、Java 集合 |
代理模式侧重"控制",装饰器模式侧重"增强"。但在实际应用中,两者的边界有时模糊,Spring AOP 既有控制(事务)也有增强(日志)。
七、代理模式的优缺点
优点
- 降低耦合:客户端面向接口,不直接依赖真实对象。
- 增强功能:在访问前后插入日志、事务、权限等横切逻辑。
- 控制访问:可以限制对真实对象的访问,实现延迟加载、权限校验。
- 符合开闭原则:新增代理不需要修改真实对象和客户端。
缺点
- 类数量增加:静态代理需要为每个目标类写代理类。
- 性能开销:动态代理通过反射调用,比直接调用慢。
- 调试复杂:代理链层层嵌套,栈跟踪不直观。
- 内部调用失效:Spring AOP 中同一个类内部方法调用不经过代理。
八、适用场景
| 场景 | 说明 |
|---|---|
| 远程代理 | 隐藏远程调用的网络细节,如 RPC 框架 |
| 虚拟代理 | 延迟加载大对象,如大图、大文件 |
| 保护代理 | 权限校验,如 Spring Security |
| 缓存代理 | 缓存查询结果,如 @Cacheable |
| 日志代理 | 记录方法调用日志 |
| 事务代理 | 声明式事务管理 |
九、总结
| 维度 | 核心要点 |
|---|---|
| 本质 | 为其他对象提供代理,控制对它的访问 |
| 角色 | 抽象主题、真实主题、代理 |
| 静态代理 | 手动编写代理类,类数量多,维护成本高 |
| JDK 动态代理 | 基于接口,Proxy + InvocationHandler |
| CGLIB 动态代理 | 基于继承,Enhancer + MethodInterceptor |
| Spring AOP | 底层是动态代理,有接口用 JDK,无接口用 CGLIB |
| 关键问题 | 内部调用失效、注入的是代理对象 |
| 典型应用 | 事务、缓存、异步、权限、日志 |
代理模式是 Spring AOP 的基石。理解静态代理和两种动态代理的实现原理,就能理解 Spring 如何在不修改业务代码的情况下,为 Bean 添加事务、缓存、日志等横切能力。同时也能解释为什么 @Transactional 在同类内部调用时会失效------因为内部调用没有经过代理对象。