代理模式可能是最被"实际使用得最多,但意识到自己在用的次数最少"的设计模式 —— 因为它太基础了。Spring AOP、Hibernate 懒加载、RPC 框架、CDN、虚拟代理、安全代理,这些工程基础设施全是代理。这篇文章把代理从原理讲到 JDK 动态代理、CGLib、Mockito,讲清楚它和装饰器、适配器、外观的边界,以及"动态代理"这个让代理威力倍增的关键技术。
代理要解决什么
代理本质是在客户端和真实对象之间放一个"中间人"。这个中间人和真实对象长得一样(实现相同接口),客户端用它和用真实对象在调用方式上没有区别。但中间人内部可以做很多事:
- 权限控制:校验是否能访问。
- 延迟加载:真实对象太重,真正用到时才创建。
- 缓存:结果可缓存的就不再调真实对象。
- 远程调用:真实对象在远端,代理负责网络传输。
- 计数 / 日志 / 监控:统计调用次数、记录调用日志。
- 事务管理:开启事务、提交、回滚。
代理的标准结构
// 1. 共同接口
interface ImageService {
Image load(String url);
}
// 2. 真实主体
class RealImageService implements ImageService {
public Image load(String url) {
System.out.println("从网络加载 " + url + "(慢)");
return downloadAndDecode(url);
}
}
// 3. 代理:和真实主体实现同一接口,内部持有真实主体的引用
class CachingImageProxy implements ImageService {
private final ImageService real = new RealImageService();
private final Map<String, Image> cache = new HashMap<>();
public Image load(String url) {
if (cache.containsKey(url)) {
System.out.println("从缓存命中 " + url);
return cache.get(url);
}
Image img = real.load(url);
cache.put(url, img);
return img;
}
}
// 客户端:依赖接口,不关心拿到的是真实对象还是代理
ImageService svc = new CachingImageProxy();
svc.load("a.png"); // 从网络加载
svc.load("a.png"); // 缓存命中
关键认知:代理和真实对象对调用方表现完全相同。客户端只看到 ImageService 接口,代理或真实对象可以无感替换。
代理的几种类型
1. 虚拟代理(Virtual Proxy):延迟加载
class LazyImageProxy implements ImageService {
private Image realImage; // 还没创建
private final String url;
public LazyImageProxy(String url) { this.url = url; }
public Image load(String url) {
if (realImage == null) realImage = downloadAndDecode(url); // 真用到才加载
return realImage;
}
}
典型例子:Hibernate 的懒加载、Word 文档里的"图片占位符"(滚动到时才真正加载)。
2. 保护代理(Protection Proxy):权限控制
class AdminImageProxy implements ImageService {
private final ImageService real;
private final User currentUser;
public Image load(String url) {
if (!currentUser.hasRole("admin")) throw new AccessDeniedException();
return real.load(url);
}
}
这是 Spring Security 的 @PreAuthorize 注解背后的机制 —— 生成一个代理,在调用真实方法前先校验权限。
3. 远程代理(Remote Proxy):RPC 客户端
// RPC 客户端代理:本地调用看着像普通方法,内部走网络
class UserServiceRpcProxy implements UserService {
private final RpcClient client;
public User getUser(long id) {
return client.call("UserService.getUser", id); // 序列化、网络、反序列化
}
}
Dubbo、gRPC、Thrift、Spring Cloud Feign 都是远程代理 —— 业务代码调用 userService.getUser(1) 感觉像调本地方法,实际是远程调用。
4. 缓存代理
上面例子里的 CachingImageProxy 就是。Spring 的 @Cacheable 注解通过代理实现。
5. 日志代理 / 监控代理
class LoggingProxy implements UserService {
private final UserService real;
public User getUser(long id) {
long start = System.currentTimeMillis();
try {
User u = real.getUser(id);
log.info("getUser({}) returned {}, took {}ms", id, u, System.currentTimeMillis() - start);
return u;
} catch (Exception e) {
log.error("getUser({}) failed", id, e);
throw e;
}
}
}
静态代理的痛点
上面的代理都叫"静态代理":代理类在编译期就写好。问题:接口里有 N 个方法,代理类要实现 N 个方法 —— 每个都是"前置 + 委托 + 后置"的模板。如果有 50 个 Service,每个都要写一个 LoggingProxy,代码量爆炸。
解决方案:动态代理 —— 运行时根据接口动态生成代理类。
JDK 动态代理
JDK 自带的动态代理 —— 利用反射在运行时生成实现给定接口的代理类:
import java.lang.reflect.*;
interface UserService {
User getUser(long id);
void updateUser(User u);
}
class RealUserService implements UserService {
public User getUser(long id) { return new User(id, "mores"); }
public void updateUser(User u) { /* ... */ }
}
// InvocationHandler:统一处理代理收到的所有调用
class LoggingHandler implements InvocationHandler {
private final Object target;
public LoggingHandler(Object target) { this.target = target; }
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
long start = System.currentTimeMillis();
try {
Object result = method.invoke(target, args);
log.info("{}({}) -> {} ({}ms)",
method.getName(), Arrays.toString(args), result, System.currentTimeMillis() - start);
return result;
} catch (InvocationTargetException e) {
throw e.getTargetException();
}
}
}
// 创建动态代理
UserService real = new RealUserService();
UserService proxy = (UserService) Proxy.newProxyInstance(
UserService.class.getClassLoader(),
new Class<?>[] { UserService.class },
new LoggingHandler(real)
);
proxy.getUser(1);
proxy.updateUser(new User(...));
关键认知:同一个 LoggingHandler 可以代理任何接口。所有方法的日志都统一在 invoke 里实现,不再需要为每个接口写代理类。
CGLib:对类(不是接口)做代理
JDK 动态代理有个限制:只能代理接口,不能代理普通类(因为它生成的代理类继承 Proxy 实现你的接口)。CGLib 通过字节码生成可以为普通类生成子类形式的代理:
import net.sf.cglib.proxy.*;
class UserService { // 不是接口,是普通类
public User getUser(long id) { return new User(id, "mores"); }
}
// 用 MethodInterceptor 拦截
class LoggingInterceptor implements MethodInterceptor {
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
log.info("call {}", method.getName());
return proxy.invokeSuper(obj, args); // 调用父类(真实方法)
}
}
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(UserService.class);
enhancer.setCallback(new LoggingInterceptor());
UserService proxy = (UserService) enhancer.create();
proxy.getUser(1);
Spring 默认情况下,如果被代理对象实现了接口用 JDK 动态代理,否则用 CGLib(可通过 @EnableAspectJAutoProxy(proxyTargetClass=true) 强制 CGLib)。
Spring AOP:代理模式的工程化巅峰
// 业务代码完全干净
@Service
public class OrderService {
@Transactional // 事务代理
@Cacheable("orders") // 缓存代理
@PreAuthorize("hasRole('USER')") // 权限代理
public Order getOrder(long id) {
return orderRepo.findById(id);
}
}
// Spring 在容器启动时,根据注解给 OrderService 生成代理:
// 调用 getOrder() 时,代理 -> 鉴权 -> 看缓存 -> 开事务 -> 真实 getOrder -> 提交事务 -> 存缓存 -> 返回
@Around("@annotation(Cacheable)")
public Object cacheAround(ProceedingJoinPoint pjp) throws Throwable {
String key = generateKey(pjp);
Object cached = cache.get(key);
if (cached != null) return cached;
Object result = pjp.proceed(); // 调用真实方法
cache.put(key, result);
return result;
}
这套机制让横切关注点(事务、缓存、权限、日志)和业务逻辑彻底解耦。业务代码看着只有 orderRepo.findById 一行,但实际跑起来背后有一长串代理在工作。这就是 Spring 框架"轻量但功能强大"的核心秘密。
Mockito:测试里的代理
// 用 Mockito 生成一个 UserService 的代理
UserService mock = Mockito.mock(UserService.class);
when(mock.getUser(1)).thenReturn(new User(1, "test"));
// 在测试里把这个代理注入业务对象
OrderService svc = new OrderService(mock);
// 验证调用
svc.placeOrder(...);
verify(mock).getUser(eq(1));
Mockito 内部用 CGLib / ByteBuddy 给你的类生成代理,所有方法都被拦截,可以预设返回值或验证调用。这是单元测试的基础设施,每个 Java 工程师都用过。
代理 vs 装饰器 vs 适配器
三者都是"在客户端和目标之间套一层",意图不同:
- 代理:控制访问。代理决定要不要让调用真的发生(权限、缓存、延迟)。
- 装饰器:增强行为。被装饰对象的调用一定会发生,装饰器只是前后加事(日志、计时)。装饰器倾向于叠加多层。
- 适配器:转换接口。两边接口不同,适配器做翻译。
区分一句话:代理是"卫兵",装饰器是"装备",适配器是"翻译"。
各语言里的代理
JavaScript:Proxy 是语言关键字
// JS 内置 Proxy 对象,直接拦截任何属性访问
const target = { name: 'mores', age: 30 };
const handler = {
get(obj, prop) {
console.log(`get ${prop}`);
return obj[prop];
},
set(obj, prop, value) {
console.log(`set ${prop} = ${value}`);
obj[prop] = value;
return true;
},
};
const p = new Proxy(target, handler);
p.name; // 打印 'get name',返回 'mores'
p.age = 31; // 打印 'set age = 31'
// Vue 3 响应式就是用 Proxy 实现的
Python:__getattr__ 实现动态代理
class LoggingProxy:
def __init__(self, target):
self._target = target
def __getattr__(self, name):
attr = getattr(self._target, name)
if callable(attr):
def wrapper(*args, **kwargs):
print(f"call {name}({args}, {kwargs})")
return attr(*args, **kwargs)
return wrapper
return attr
real = UserService()
proxy = LoggingProxy(real)
proxy.get_user(1) # 打印 call get_user((1,), {})
代理的常见坑
坑 1:Spring 中的"自调用"问题
@Service
public class OrderService {
@Transactional
public void outer() {
inner(); // 直接调 inner 不走代理!事务不生效
}
@Transactional
public void inner() { ... }
}
原因:Spring 代理拦截的是"从外部进入的调用"。outer() 里调 inner() 是同一个对象内部调用,绕过了代理。修复:把 inner 抽到另一个 Service,或者注入自身 proxy:
@Service
public class OrderService {
@Autowired
private OrderService self; // 注入自己的代理
public void outer() {
self.inner(); // 通过 self 调用,走代理
}
}
坑 2:final 方法不能被 CGLib 代理
CGLib 靠"生成子类 + 重写方法"实现拦截,final 方法不能重写 —— 这些方法的调用绕过代理直达原方法。解决:不要在被代理类里用 final 方法,或者改用接口 + JDK 动态代理。
坑 3:动态代理性能
反射调用比直接调用慢几倍到几十倍。在每秒上万次的热路径上,代理可能成为瓶颈。优化:用 ByteBuddy 生成"非反射"代理类(直接生成字节码调原方法),Spring 5.3+ 内置类似优化。
坑 4:代理 + 序列化的坑
代理对象可能不可序列化(Proxy 类没有 readObject)。如果你不小心把一个 Spring bean 通过远程接口返回,反序列化会失败。规则:代理 stay 在服务端,跨边界传输的是 DTO 不是 Service。
识别代理场景
关键问题:
- 调用"真实对象"前/后要做事吗?(权限、日志、缓存)
- 真实对象的创建很昂贵,想推迟到第一次用?(虚拟代理)
- 真实对象在远端,需要本地透明调用?(远程代理)
- 这些"做事"逻辑要与业务代码解耦?
有 yes 就考虑代理。如果"做事"还要叠加多种(权限 + 缓存 + 日志),那是代理 + 装饰器组合,Spring AOP 就是这么干的。
写在最后
代理模式是当代 Java / Spring / .NET 生态的脊梁。注解驱动开发(@Transactional / @Cacheable / @Async)能存在,本质就是因为运行时代理 + 反射能在你的方法前后插入逻辑而不需要你手写。理解代理,你就理解了为什么一个 @Transactional 能让方法自动跑在事务里 —— 那不是魔法,那是代理。
给一个工程心法:当一段逻辑反复出现在多个方法的开头或结尾(日志、计时、权限、事务、缓存),它就是横切关注点,该用代理 / AOP 抽出去。业务代码里不该堆"开事务-业务-提交事务"这种模板,把模板交给代理,业务代码会瞬间变干净。这是设计模式里 ROI 最稳定的几个之一,几乎所有大型 Java 项目都重度依赖它。
—— 别看了 · 2026