代理模式:为其他对象提供一种代理以控制对这个对象的访问
代理模式,说白了就是「找个中间人」。你想想看,生活中我们经常这么干——找中介租房、找律师打官司、找秘书接电话。程序里的代理模式,道理一模一样。
我刚开始接触这个模式时,觉得它有点多余。直接调用对象不就行了?干嘛非要绕一圈?后来在项目中踩过坑才明白,有些场景下,你确实需要一个人替你「挡在前面」。
为什么需要代理?
直接访问对象,有时候会出问题。比如:
- 对象创建很耗时,你不想每次都用的时候才创建
- 对象在远程服务器上,你不能直接 new 一个
- 你想在访问前后加点额外逻辑,比如权限检查、日志记录
这些场景下,代理模式就派上用场了。代理对象和被代理对象实现同一个接口,客户端只跟代理打交道。代理在背后,帮你处理那些「脏活累活」。
核心思想:代理对象控制着对真实对象的访问。客户端不知道也不关心背后是谁在干活。
三种代理实现方式
Java 世界里,代理有三种主流实现。我按自己的理解,把它们分成三个层次:
| 代理类型 | 原理 | 适用场景 | 个人评价 |
|---|---|---|---|
| 静态代理 | 手动编写代理类 | 代理逻辑固定,类少 | 简单粗暴,但代码冗余 |
| JDK 动态代理 | 运行时生成代理对象 | 被代理对象有接口 | 灵活,但必须面向接口 |
| CGLIB 代理 | 通过继承生成子类 | 被代理对象没有接口 | 强大,但注意 final 方法 |
静态代理:最原始的方式
静态代理就是你自己写一个代理类,实现和目标类相同的接口。每个方法里,你手动调用目标方法,并在前后加上自己的逻辑。
我在早期的一个项目中用过这种方式。当时要给一个支付接口加日志,我直接写了个 PaymentProxy,里面包了一个 PaymentServiceImpl。代码倒是清晰,但后来接口多了,代理类也跟着膨胀。嗯,维护起来挺痛苦的。
// 接口
public interface PaymentService {
void pay(String account, double amount);
}
// 真实对象
public class PaymentServiceImpl implements PaymentService {
public void pay(String account, double amount) {
System.out.println("支付 " + amount + " 到 " + account);
}
}
// 静态代理
public class PaymentServiceProxy implements PaymentService {
private PaymentService target;
public PaymentServiceProxy(PaymentService target) {
this.target = target;
}
public void pay(String account, double amount) {
System.out.println("[日志] 开始支付");
target.pay(account, amount);
System.out.println("[日志] 支付完成");
}
}
我的建议:静态代理适合代理类很少、逻辑固定的场景。如果代理逻辑经常变,或者代理类超过三五个,赶紧换动态代理。
JDK 动态代理:运行时生成
JDK 动态代理利用 java.lang.reflect.Proxy 和 InvocationHandler,在运行时动态创建代理对象。你只需要写一个 InvocationHandler,所有方法调用都会经过它。
我记得第一次用动态代理时,感觉挺神奇的。不用写那么多代理类了,一个 InvocationHandler 搞定所有。但有个坑——被代理对象必须实现接口。如果你的类没有接口,JDK 动态代理就无能为力了。
public class LogHandler implements InvocationHandler {
private Object target;
public LogHandler(Object target) {
this.target = target;
}
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;
}
}
// 使用
PaymentService proxy = (PaymentService) Proxy.newProxyInstance(
PaymentService.class.getClassLoader(),
new Class[]{PaymentService.class},
new LogHandler(new PaymentServiceImpl())
);
proxy.pay("张三", 100.0);
注意:JDK 动态代理只能代理接口。如果你的类没有实现任何接口,运行时会抛出 IllegalArgumentException。我曾经在这个坑里爬了半天才出来。
CGLIB 代理:没有接口也能代理
CGLIB 通过继承目标类来生成子类,子类就是代理对象。它不要求目标类实现接口,所以比 JDK 动态代理更通用。但有个限制——final 方法和 final 类无法被代理,因为 CGLIB 靠继承,而 final 方法不能被重写。
Spring 框架里,如果 Bean 实现了接口,默认用 JDK 动态代理;如果没有接口,就用 CGLIB。这个设计挺聪明的,兼顾了灵活性和性能。
// 目标类,没有接口
public class UserService {
public void saveUser(String name) {
System.out.println("保存用户: " + name);
}
}
// CGLIB 代理
public class CglibProxy implements MethodInterceptor {
private Enhancer enhancer = new Enhancer();
public Object getProxy(Class clazz) {
enhancer.setSuperclass(clazz);
enhancer.setCallback(this);
return enhancer.create();
}
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
System.out.println("[CGLIB] 方法前");
Object result = proxy.invokeSuper(obj, args);
System.out.println("[CGLIB] 方法后");
return result;
}
}
// 使用
UserService proxy = (UserService) new CglibProxy().getProxy(UserService.class);
proxy.saveUser("李四");
个人经验:能用 JDK 动态代理就用 JDK,性能更好。CGLIB 是备选方案,别一上来就用它。另外,CGLIB 对 final 方法无能为力,设计类时要注意。
三种代理的对比
我整理了一张对比表,方便你快速决策:
| 维度 | 静态代理 | JDK 动态代理 | CGLIB 代理 |
|---|---|---|---|
| 是否需要接口 | 是 | 是 | 否 |
| 代理类数量 | 每个目标类一个 | 一个 InvocationHandler | 一个 MethodInterceptor |
| 性能 | 高(编译期) | 中(反射调用) | 中(字节码增强) |
| 灵活性 | 低 | 高 | 高 |
| 限制 | 代码冗余 | 必须实现接口 | 不能代理 final 类/方法 |
代理模式的核心逻辑
下面这张图,展示了代理模式的核心结构。你看一眼就能明白:客户端只跟代理打交道,代理在背后控制着对真实对象的访问。
避坑指南
代理模式用起来顺手,但坑也不少。我把自己踩过的坑列出来,你注意避开:
- 不要滥用代理——每个方法都加代理,性能会下降。只对需要控制访问的方法加代理。
- 注意代理链的顺序——多个代理嵌套时,顺序很重要。比如先做权限检查,再做日志记录,顺序反了可能出问题。
- CGLIB 代理的 final 陷阱——我曾经给一个 Service 类加了 final 方法,结果 CGLIB 代理后调用那个方法,直接走了父类逻辑,没走代理逻辑。排查了半天才发现。
- JDK 动态代理的转型问题——代理对象只能转型为接口类型,不能转型为具体实现类。否则会抛 ClassCastException。
曾经踩过的坑:有一次我用 JDK 动态代理代理了一个没有接口的类,运行时报错。后来换成 CGLIB 才解决。所以,设计阶段就要想清楚:你的类有没有接口?如果没有,提前准备好 CGLIB 依赖。
总结
代理模式不是什么高深的技术,它就是「找个中间人」这个朴素思想的体现。三种实现方式各有优劣,选哪个取决于你的具体场景:
- 代理类少、逻辑固定 → 静态代理
- 有接口、需要灵活 → JDK 动态代理
- 没有接口、需要代理 → CGLIB 代理
我个人更倾向于 JDK 动态代理,因为它干净、轻量。但遇到没有接口的类,CGLIB 就是救命稻草。记住一点:代理模式是为了控制访问,不是为了炫技。别为了用代理而用代理,想清楚你要解决什么问题。
公众号:蓝海资料掘金营,微信deep3321