什么是代理模式?应用场景有哪些?
结论先行:代理模式(Proxy Pattern)给目标对象套一个同类型的替身,替身负责控制访问——加日志、加事务、加缓存、加权限,而目标对象的代码一行不改。Java 里有三种落地方式:静态代理、JDK 动态代理(基于接口 + 反射)、CGLIB(基于继承 + 字节码生成)。
1. 结构:调用方以为自己在调真实对象
Client ──► Proxy(替身) ──转发──► RealSubject(真实业务逻辑)
│ ▲
│ 前置/后置增强 │
└──────────────────────────┘
约束:Proxy 与 RealSubject 实现同一个 Subject 接口,调用方无感
关键:代理与真实对象实现同一个接口,所以调用方无感。这就是"对扩展开放、对修改关闭"最经典的落地。
2. 三种实现方式的差别
| 维度 | 静态代理 | JDK 动态代理 | CGLIB |
|---|---|---|---|
| 生成时机 | 手写,编译期 | 运行时 Proxy.newProxyInstance | 运行时生成子类字节码 |
| 前提条件 | 无 | 目标类必须实现接口 | 目标类不能是 final,方法不能 final |
| 代理数量 | 一个接口一个类 | 通用,一个 handler 代理所有 | 通用 |
| 性能 | 最快 | 反射调用,略慢 | 直接调用,较快 |
| 典型使用者 | 早期手写 | Spring AOP(有接口时) | Spring AOP(无接口时) |
3. 静态代理:写死,类会膨胀
interface UserService {
User findById(long id);
}
class UserServiceImpl implements UserService {
public User findById(long id) { return queryFromDb(id); }
}
// 手写代理:每个方法都要重复写日志和计时
class UserServiceProxy implements UserService {
private final UserService target;
UserServiceProxy(UserService target) { this.target = target; }
public User findById(long id) {
long start = System.nanoTime();
try {
return target.findById(id);
} finally {
System.out.println("findById 耗时 " + (System.nanoTime() - start) + " ns");
}
}
}
接口一多,代理类就成倍增长,且增强逻辑到处重复——于是有了动态代理。
4. JDK 动态代理:一个 handler 打天下
import java.lang.reflect.*;
public class TimingHandler implements InvocationHandler {
private final Object target;
public TimingHandler(Object target) { this.target = target; }
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
long start = System.nanoTime();
try {
return method.invoke(target, args); // 反射调用真实方法
} catch (InvocationTargetException e) {
throw e.getTargetException(); // 剥掉反射包装,抛出原始异常
} finally {
System.out.println(method.getName() + " 耗时 " + (System.nanoTime() - start) + " ns");
}
}
}
// 使用:返回的对象实现了 UserService 接口
UserService proxy = (UserService) Proxy.newProxyInstance(
UserService.class.getClassLoader(),
new Class<?>[]{ UserService.class },
new TimingHandler(new UserServiceImpl()));
proxy.findById(42L);
三个参数分别是:类加载器、要实现的接口数组、调用处理器。注意异常必须用 getTargetException() 还原,否则业务代码 catch 具体异常会失效。
5. CGLIB:没有接口时的兜底
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(UserServiceImpl.class); // 生成子类
enhancer.setCallback((MethodInterceptor) (obj, method, args, proxy) -> {
long start = System.nanoTime();
try {
return proxy.invokeSuper(obj, args); // 调父类方法
} finally {
System.out.println(method.getName() + " 耗时 " + (System.nanoTime() - start));
}
});
UserServiceImpl proxy = (UserServiceImpl) enhancer.create();
原理是运行期生成 UserServiceImpl$$EnhancerByCGLIB 子类并重写方法。因此 final 类、final 方法、private 方法都代理不了。
6. 应用场景
| 场景 | 代理角色做什么 | 真实案例 |
|---|---|---|
| 远程代理(Remote) | 把本地调用编码成网络请求 | RPC 客户端 stub、Dubbo、Feign |
| 虚拟代理(Virtual) | 延迟创建重对象 | Hibernate 懒加载、图片占位 |
| 保护代理(Protection) | 调用前校验权限 | @PreAuthorize、接口鉴权 |
| 缓存代理 | 命中直接返回,未命中原调用 | @Cacheable |
| 同步/事务代理 | 方法前后开闭事务 | @Transactional |
| 日志/监控代理 | 记录耗时、埋点 | AOP 切面、链路追踪 |
7. 常见追问
Q:JDK 动态代理为什么必须基于接口?
生成的代理类已经 extends Proxy,Java 单继承,所以只能靠实现接口来"看起来是目标类型"。
Q:Spring AOP 什么时候用 JDK、什么时候用 CGLIB?
Spring Boot 2.x 起默认 proxyTargetClass=true,统一用 CGLIB;有接口也会用 CGLIB,除非显式配置 spring.aop.proxy-target-class=false。
Q:为什么同类方法自调用,事务/缓存注解不生效?
因为增强发生在代理对象上。this.b() 走的是原始对象,绕过了代理,自然没有增强。改成注入自身代理、拆到另一个 bean,或用 AopContext.currentProxy()。
Q:代理和装饰器的区别? 结构几乎一样,意图不同:代理控制访问(常常自己创建目标),装饰器叠加功能(目标由外部传入,可无限层叠)。
小结
- 代理 = 同类型替身 + 控制访问,目标对象零侵入。
- 静态代理手写、易膨胀;JDK 动态代理需接口、靠反射;CGLIB 靠继承、怕
final。 - 记住三大高频场景:RPC stub、Spring AOP(事务/缓存)、权限校验。
- 追问常落在"自调用失效"和"JDK 与 CGLIB 的选择"上。