代理模式:定义、结构、实现、优缺点、应用场景
代理模式,说白了就是「找人办事」。你想想看,有时候你不想直接面对某个对象,或者直接调用它太麻烦、太危险,那就找个代理来替你搞定。我在项目中遇到过不少这样的场景——比如远程调用、权限控制、延迟加载,代理模式都是个很优雅的解决方案。
一、定义与核心思想
代理模式(Proxy Pattern)属于结构型设计模式。它提供了一个替身,来控制对另一个对象的访问。这个替身就是代理对象,它和被代理对象实现相同的接口,客户端通过代理来间接访问真实对象。
嗯,这里要注意:代理不是装饰器。装饰器是增强功能,代理是控制访问。我刚开始学的时候也容易搞混,后来在项目里踩过坑才真正分清楚。
核心思想:用一个代理对象来控制对真实对象的访问,可以在不修改真实对象的前提下,增加额外的控制逻辑。
二、结构组成
代理模式的结构其实挺简单的,就三个角色:
- 抽象主题(Subject):定义真实对象和代理对象的共同接口。这样客户端才能统一对待它们。
- 真实主题(RealSubject):真正干活的业务对象。代理对象背后就是它。
- 代理(Proxy):持有真实对象的引用,控制对它的访问。可以在调用前后加一些额外操作。
我习惯用一张图来理解它们的关系,你看下面这个:
三、实现方式:静态代理 vs 动态代理
代理模式有两种实现方式。静态代理是手写代理类,动态代理是运行时生成代理类。我个人更推荐动态代理,因为它更灵活。但静态代理在某些简单场景下反而更直观。
1. 静态代理
静态代理就是手动创建一个代理类,让它实现和真实对象相同的接口。每个真实对象都需要一个对应的代理类。代码写起来比较啰嗦,但好处是简单易懂。
// 抽象主题
interface Image {
void display();
}
// 真实主题
class RealImage implements Image {
private String filename;
public RealImage(String filename) {
this.filename = filename;
loadFromDisk(); // 构造时就加载,比较重
}
private void loadFromDisk() {
System.out.println("加载图片: " + filename);
}
@Override
public void display() {
System.out.println("显示图片: " + filename);
}
}
// 静态代理
class ImageProxy implements Image {
private RealImage realImage;
private String filename;
public ImageProxy(String filename) {
this.filename = filename;
}
@Override
public void display() {
// 延迟加载:第一次显示时才真正创建真实对象
if (realImage == null) {
realImage = new RealImage(filename);
}
realImage.display();
}
}
// 客户端
public class Client {
public static void main(String[] args) {
Image image = new ImageProxy("photo.jpg");
// 此时还没加载图片
image.display(); // 第一次调用,才真正加载
image.display(); // 第二次调用,直接显示
}
}
你看,静态代理的代码很直白。但问题来了——如果我有几十个类都需要代理,每个都手写一个代理类,那工作量就大了。我曾经在一个老项目里见过这种情况,光代理类就写了上百个,维护起来真是头疼。
2. 动态代理
动态代理就不一样了。它利用 Java 的反射机制,在运行时动态生成代理类。你只需要写一个 InvocationHandler,就能代理任意接口。这比静态代理灵活太多了。
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
// 通用的 InvocationHandler
class LogHandler implements InvocationHandler {
private Object target;
public LogHandler(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("[日志] 方法结束: " + method.getName());
return result;
}
}
// 客户端使用动态代理
public class DynamicProxyClient {
public static void main(String[] args) {
Image realImage = new RealImage("photo.jpg");
Image proxy = (Image) Proxy.newProxyInstance(
realImage.getClass().getClassLoader(),
realImage.getClass().getInterfaces(),
new LogHandler(realImage)
);
proxy.display();
}
}
小提示:动态代理只能代理接口,不能代理类。如果你需要代理类,可以考虑 CGLIB 或 ByteBuddy 这类库。我在做 Spring AOP 相关项目时,就经常用 CGLIB 来处理没有接口的类。
四、优缺点分析
| 优点 | 缺点 |
|---|---|
|
|
避坑指南:我曾经在一个项目中过度使用代理模式,给每个 Service 都加了代理层。结果代码变得非常绕,出了问题要跳好几层才能找到真实逻辑。后来我反思了一下——代理模式要用在真正需要控制访问的地方,不要为了用模式而用模式。
五、应用场景
代理模式在实际项目中应用非常广泛。我挑几个最常见的场景说说:
- 远程代理(RPC):比如 Dubbo、Feign 这些框架,客户端调用的其实是一个本地代理对象,代理负责把请求通过网络发送到远程服务器。你想想看,如果没有代理,客户端就得自己处理网络通信、序列化、异常重试这些事,那代码得多复杂。
- 虚拟代理(延迟加载):像上面图片加载的例子,大对象在真正需要时才创建。我在做报表系统时就用过这个思路——报表数据量很大,用户不点开查看就不加载,节省了不少内存。
- 保护代理(权限控制):在调用真实对象之前,先检查调用方有没有权限。比如 Spring Security 里的 @PreAuthorize 注解,底层就是通过 AOP 代理来实现的。
- 智能引用(引用计数):当对象被多个客户端引用时,代理可以记录引用次数,在引用数为零时释放资源。我记得在做一个缓存框架时就用到了这个机制。
- 日志记录与监控:通过代理在方法调用前后记录日志、统计耗时。Spring AOP 的 @Around 通知就是典型的代理应用。
说白了,代理模式的核心价值就是「控制访问」。它让你在不改变原有代码的情况下,给对象调用加上一层额外的逻辑。这个思想在很多框架中都有体现——Spring AOP、MyBatis 的 Mapper 代理、Hibernate 的懒加载代理,背后都是代理模式。
嗯,最后说一句:代理模式虽然好用,但别滥用。我见过有人给每个类都配一个代理,结果代码变得又臭又长。记住,模式是工具,不是目的。用对地方才是高手。