代理模式:AOP 实现——为其他对象提供一种代理以控制对这个对象的访问
代理模式,说白了就是“找人办事”。你不想直接跟目标对象打交道,或者你没法直接访问它,那就找个代理来替你完成。这个代理跟你长得一样,接口也一样,但它在背后偷偷做了很多额外的事情。
我在项目中用过很多次代理模式。最典型的,就是 Spring AOP。你想想看,如果没有代理,你怎么在不修改业务代码的情况下,给每个方法加上日志、事务、权限校验?嗯,代理模式就是干这个的。
代理模式的核心结构
代理模式的结构其实很简单,就三个角色:
- 抽象主题(Subject):定义真实对象和代理对象的公共接口。
- 真实主题(RealSubject):真正干活的业务对象。
- 代理(Proxy):持有真实主题的引用,控制对它的访问,并在调用前后做一些额外操作。
我习惯把代理模式理解为“中间层”。你在调用者和目标对象之间插了一层,这一层可以做任何事情:缓存、延迟加载、权限校验、远程通信……
Spring AOP 中的 JDK 动态代理和 CGLIB 代理
Spring AOP 是代理模式最经典的应用。它有两种实现方式:JDK 动态代理和 CGLIB 代理。
JDK 动态代理
JDK 动态代理只能代理接口。它通过 java.lang.reflect.Proxy 和 InvocationHandler 在运行时动态生成代理类。
我记得有一次,团队里有个同事问:“为什么我的 Service 类加了 @Transactional 注解,事务就是不生效?”我一看,他那个 Service 类没有实现接口。Spring 默认用 JDK 动态代理,而 JDK 动态代理要求目标对象必须实现接口。没有接口,代理就生成不了,事务自然不生效。
// 定义接口
public interface UserService {
void addUser(String name);
}
// 真实业务类
public class UserServiceImpl implements UserService {
@Override
public void addUser(String name) {
System.out.println("添加用户:" + name);
}
}
// 实现 InvocationHandler
public class LogInvocationHandler implements InvocationHandler {
private Object target;
public LogInvocationHandler(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;
}
}
// 使用
UserService userService = new UserServiceImpl();
UserService proxy = (UserService) Proxy.newProxyInstance(
userService.getClass().getClassLoader(),
userService.getClass().getInterfaces(),
new LogInvocationHandler(userService)
);
proxy.addUser("张三");
CGLIB 代理
CGLIB 代理不需要接口。它通过继承目标类,在运行时生成目标类的子类作为代理。Spring 在目标对象没有实现接口时,会自动切换到 CGLIB 代理。
我个人建议,如果你能确保业务类都实现接口,就用 JDK 动态代理。如果有些类确实没有接口,那就用 CGLIB。不过要注意,CGLIB 代理的是类,所以 final 方法不能被代理,因为子类无法重写 final 方法。
// 不需要接口
public class UserService {
public void addUser(String name) {
System.out.println("添加用户:" + name);
}
}
// 使用 CGLIB 的 MethodInterceptor
public class LogMethodInterceptor 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;
}
}
// 使用
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(UserService.class);
enhancer.setCallback(new LogMethodInterceptor());
UserService proxy = (UserService) enhancer.create();
proxy.addUser("李四");
核心区别:JDK 动态代理基于接口,CGLIB 代理基于继承。Spring 默认优先使用 JDK 动态代理,如果目标对象没有实现接口,则使用 CGLIB。
远程代理(RPC)
远程代理,说白了就是让你像调用本地方法一样调用远程服务。你在本地调用代理对象的方法,代理对象把调用信息序列化后通过网络发送到远程服务器,远程服务器执行真正的业务逻辑,再把结果返回。
我在项目中用过 Dubbo 和 gRPC,它们底层都用了远程代理。你配置一个接口,框架自动生成代理对象。你调用代理对象的方法,框架帮你完成网络通信、序列化、反序列化、负载均衡等复杂操作。
嗯,这里要注意:远程代理虽然方便,但性能开销比本地调用大得多。网络延迟、序列化开销、连接池管理,这些都是你需要考虑的。
虚拟代理(延迟加载大图)
虚拟代理,就是“先给你一个假的,等真正需要的时候再加载真的”。最常见的场景是延迟加载大图。
我曾经做过一个图片浏览应用,用户打开一个包含几百张高清图片的页面。如果一次性加载所有图片,页面直接卡死。我的做法是:先用虚拟代理对象占位,显示一个缩略图或加载中的图标。只有当用户滚动到某张图片的位置时,才真正加载原图。
// 图片接口
public interface Image {
void display();
}
// 真实图片
public class HighResolutionImage implements Image {
private String filename;
public HighResolutionImage(String filename) {
this.filename = filename;
loadFromDisk();
}
private void loadFromDisk() {
System.out.println("加载高清图片:" + filename);
// 模拟耗时加载
try { Thread.sleep(2000); } catch (InterruptedException e) {}
}
@Override
public void display() {
System.out.println("显示图片:" + filename);
}
}
// 虚拟代理
public class ImageProxy implements Image {
private String filename;
private HighResolutionImage realImage;
public ImageProxy(String filename) {
this.filename = filename;
}
@Override
public void display() {
if (realImage == null) {
realImage = new HighResolutionImage(filename);
}
realImage.display();
}
}
提示:虚拟代理的核心思想是“延迟初始化”。它适用于创建开销大的对象,比如大图、大文件、数据库连接等。只有真正需要使用时,才去创建真实对象。
保护代理(权限控制)
保护代理,就是控制谁可以访问目标对象。在调用真实对象的方法之前,代理先检查调用者是否有权限。
我记得在一个后台管理系统中,不同角色有不同的操作权限。管理员可以删除用户,普通用户只能查看。我用保护代理来实现这个逻辑:代理对象在调用真实对象的 deleteUser 方法之前,先检查当前用户的角色。如果不是管理员,直接抛出异常或返回错误提示。
public class UserServiceProxy implements UserService {
private UserService target;
private String currentUserRole;
public UserServiceProxy(UserService target, String currentUserRole) {
this.target = target;
this.currentUserRole = currentUserRole;
}
@Override
public void deleteUser(int userId) {
if (!"ADMIN".equals(currentUserRole)) {
throw new SecurityException("没有删除用户的权限");
}
target.deleteUser(userId);
}
}
注意:保护代理只能做应用层的权限控制。真正的安全防护需要结合框架级的安全机制,比如 Spring Security。保护代理更多是作为一种轻量级的权限校验手段,适用于简单场景。
代理模式的知识体系
下面这张图展示了代理模式的核心知识结构,包括它的定义、三种主要类型以及在实际项目中的应用场景。
代理模式与其他模式的区别
| 模式 | 核心目的 | 典型场景 |
|---|---|---|
| 代理模式 | 控制对目标对象的访问 | AOP、RPC、延迟加载、权限控制 |
| 装饰器模式 | 动态增强对象的功能 | IO 流、日志装饰 |
| 适配器模式 | 转换接口,让不兼容的类能一起工作 | 电源适配器、接口兼容 |
| 外观模式 | 提供统一的简化接口 | 子系统封装、门面接口 |
代理模式和装饰器模式长得有点像,但目的完全不同。代理模式重在“控制访问”,装饰器模式重在“增强功能”。你想想看,代理模式是“我替你做,但我要先检查你有没有资格让我做”,装饰器模式是“你本来就能做,我帮你做得更好”。
个人经验:我曾经在项目中把代理模式和装饰器模式搞混过。后来我总结了一个简单的判断方法:如果你在调用前后做的是“控制性”的操作(权限、日志、事务),那就是代理模式;如果你做的是“增强性”的操作(加功能、改行为),那就是装饰器模式。
代理模式在实际项目中无处不在。Spring AOP 用它实现声明式事务、日志、权限校验;RPC 框架用它实现远程调用;前端框架用它实现图片的延迟加载。说白了,只要你想在不修改原有代码的情况下,给对象调用加上一层控制逻辑,代理模式就是你的首选。