代理(Proxy)是结构型里最有存在感的一个——你每天用的
@Transactional、@Cacheable、MyBatis 的 Mapper 接口、Feign 客户端,背后都是它。本文讲清代理的四种经典形态、三种 Java 落地方式(静态 / JDK 动态 / CGLIB),并解决一个高频困惑:同样都是"包一层",代理和装饰器到底差在哪。
一、问题的起点:为什么不能直接访问对象
你想调用一个对象,但直接调用不合适。常见理由有四类:
| 障碍 | 直接调的后果 |
|---|---|
| 它在别的进程/机器上 | 要写网络通信、序列化、重试——调用方被迫处理一堆与业务无关的事 |
| 它创建起来很贵 | 页面一打开就把大图、大报表全加载了,卡顿且浪费 |
| 不是谁都该用 | 敏感接口需要按角色鉴权,但业务代码里到处写 if (role == ADMIN) 会烂掉 |
| 访问时需要附带动作 | 每调一次都要记日志、计时、写缓存、加引用计数 |
代理模式给出的答案很朴素:让代理对象和真实对象实现同一个接口,调用方只面对代理,由代理决定"要不要真的转给真实对象、转之前之后做什么"。
GoF 的意图原文是:为其他对象提供一种代理以控制对这个对象的访问。 注意动词是控制——不是"增强功能",也不是"换接口"。这个用词上的区别,正是它和装饰器、适配器的分水岭。
二、代理的四种经典形态
GoF 归纳了四种典型用法,它们的共同点是"代理挡住真实对象",区别在于挡的理由:

记住这张图的实用价值是:面试问"代理模式有什么用",你答的不是"包一层",而是这四类具体需求 + 各自在框架里的真实例子。
三、结构:三个角色与一次访问
代理的结构简单到有点"看不出来是个模式"——因为它和装饰器长得几乎一样:

三个角色:
- Subject(抽象主题):代理与真实对象共同的接口;
- RealSubject(真实主题):真正干活的对象;
- Proxy(代理):持有真实对象的引用,在转发前后做"控制"。
一次访问的时序是这样的——注意代理里那些"前置/后置"动作才是这个模式的全部意义:

下面是一个静态代理的完整例子(访问控制 + 耗时统计)。静态代理就是"手写一个实现同一接口的类",最直白,也最能看清代理的本质:
import java.time.Duration;
import java.time.Instant;
public class StaticProxyDemo {
interface DataService { // Subject
String query(String id);
}
static class RealDataService implements DataService { // RealSubject
@Override public String query(String id) {
return "数据-" + id;
}
}
static class DataServiceProxy implements DataService { // Proxy
private final DataService target;
private final String role;
DataServiceProxy(DataService target, String role) {
this.target = target;
this.role = role;
}
@Override public String query(String id) {
if (!"admin".equals(role)) { // 前置:访问控制
throw new SecurityException("无权限查询: " + id);
}
Instant start = Instant.now();
try {
return target.query(id); // 委托给真实主题
} finally {
System.out.println("query(" + id + ") 耗时 "
+ Duration.between(start, Instant.now()).toMillis() + "ms");
}
}
}
public static void main(String[] args) {
DataService admin = new DataServiceProxy(new RealDataService(), "admin");
System.out.println("admin -> " + admin.query("A-1"));
DataService guest = new DataServiceProxy(new RealDataService(), "guest");
try {
guest.query("A-2");
} catch (SecurityException e) {
System.out.println("guest -> 被代理拦下: " + e.getMessage());
}
}
}
静态代理的痛点:DataService 有几个方法,代理就要实现几个;再来一个 UserService、OrderService……每个都要抄一遍"鉴权 + 计时"。这就是动态代理要解决的问题。
四、动态代理(一):JDK 原生方案
JDK 自带 java.lang.reflect.Proxy:你只写一个 InvocationHandler,代理类由运行时生成,所有方法调用都会汇集到 invoke() 这一个方法里。
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.Map;
public class DynamicProxyDemo {
interface DataService {
String query(String id);
int count();
}
static class RealDataService implements DataService {
@Override public String query(String id) { return "数据-" + id; }
@Override public int count() { return 42; }
}
/** 一个 handler 统管所有方法:缓存 + 日志 + 委托 */
static class CacheLogHandler implements InvocationHandler {
private final Object target; // 任意真实对象
private final Map<String, Object> cache = new HashMap<>();
CacheLogHandler(Object target) { this.target = target; }
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if ("count".equals(method.getName())) { // 某些方法不走缓存
return method.invoke(target, args);
}
String key = method.getName() + ":" + (args == null ? "" : args[0]);
if (cache.containsKey(key)) {
System.out.println("[缓存命中] " + key);
return cache.get(key);
}
Object result = method.invoke(target, args); // 委托:反射调用真实方法
cache.put(key, result);
System.out.println("[回源] " + key + " -> " + result);
return result;
}
}
@SuppressWarnings("unchecked")
static <T> T proxyOf(Object target, Class<T> iface) {
return (T) Proxy.newProxyInstance(
iface.getClassLoader(), // 类加载器:必须能看见接口
new Class<?>[]{iface}, // 要代理的接口(可以给多个)
new CacheLogHandler(target)); // 方法分派全交给它
}
public static void main(String[] args) {
DataService service = proxyOf(new RealDataService(), DataService.class);
System.out.println(service.query("A-1")); // 回源
System.out.println(service.query("A-1")); // 缓存命中
System.out.println("count = " + service.count());
System.out.println("代理类: " + service.getClass().getName());
// JDK 8: com.sun.proxy.$Proxy0;JDK 9+ : jdk.proxy2.$Proxy0(数字随生成顺序变)
}
}
它一次解决了静态代理的所有痛点:新增接口不用再写代理类、新增方法不用改代理。代价是三条硬性约束:
- 只能代理接口。
Proxy.newProxyInstance的第二个参数就是接口数组;没有接口就没有代理(这正是 CGLIB 存在的理由,见下一节)。 - 每个调用都走反射(
method.invoke),比直接调用多一层间接;现代 JVM 下通常可接受,但热路径要留意。 - 调试变难:栈里出现
$Proxy0,出问题时得先想清楚"我现在停在代理还是真实对象里"。
运行时这条链路值得记住:

想把生成的代理类挖出来看,加启动参数:
-Djdk.proxy.ProxyGenerator.saveGeneratedFiles=true,会在工作目录下 dump 出$Proxy0.class,反编译即可看到它是怎么把你的接口方法全转给invoke()的。
五、动态代理(二):CGLIB / ByteBuddy——生成子类来代理
JDK 方案有一道硬门槛:必须有接口。可现实中大量类没有接口(或接口不完整),于是就有了第二条路线——运行时生成目标类的子类,覆写同名方法来插入拦截逻辑。这就是 CGLIB(Code Generation Library),它也是 Spring AOP 的默认实现。

和 JDK 方案并排看,两个关键差别立刻显现:
| JDK 动态代理 | CGLIB | |
|---|---|---|
| 代理方式 | 实现同一个接口 | 继承同一个类 |
| 委托调用 | method.invoke(target, args)(反射) | methodProxy.invokeSuper(obj, args)(直接调父类实现,无反射) |
| 前提 | 必须有接口 | 类与方法不能是 final,方法不能是 private |
// 需要 CGLIB 依赖;Spring 已在 spring-core 内置 repackage 版本(org.springframework.cglib)
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
public class CglibProxyDemo {
/** 目标类:注意它没有实现任何接口 */
static class OrderService {
public void createOrder(String id) {
System.out.println("下单: " + id);
}
}
/** 拦截器:等价于 JDK 方案里的 InvocationHandler */
static class TxInterceptor implements MethodInterceptor {
private final Object target;
TxInterceptor(Object target) { this.target = target; }
@Override
public Object intercept(Object obj, Method method, Object[] args,
MethodProxy proxy) throws Throwable {
System.out.println("[开启事务] " + method.getName());
try {
return proxy.invokeSuper(obj, args); // 直接调父类实现,不经过反射
} finally {
System.out.println("[提交事务] " + method.getName());
}
}
}
@SuppressWarnings("unchecked")
static <T> T proxyOf(T target, Class<T> type) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(type); // 关键:把目标类当作父类
enhancer.setCallback(new TxInterceptor(target)); // 方法拦截回调
return (T) enhancer.create(); // 生成子类实例
}
public static void main(String[] args) {
OrderService service = proxyOf(new OrderService(), OrderService.class);
service.createOrder("A-1");
System.out.println("代理类: " + service.getClass().getName());
// 形如 CglibProxyDemo$OrderService$$EnhancerByCGLIB$$xxxxxxxx
}
}
三条限制全部来自"继承"这件事:
final类与final方法代理不了——子类无法覆写;private方法代理不了——同理;Spring AOP 本身也只作用于可覆写的实例方法;- 构造器不会被调用:CGLIB 生成子类实例时,Spring 借助 Objenesis 直接构造对象,所以"依赖构造器做初始化"的类要留意——这和 JDK 的
$Proxy一样,都属于"代理绕过了构造函数"那一类坑。
ByteBuddy 是更现代的字节码生成库(Hibernate、Mockito 等在用),思路与 CGLIB 一致(生成子类),但在 JDK 9+ 的模块化与字节码 API 上更干净。在 Spring 里你通常不必直接接触它——记住"有接口走 JDK,没接口走子类代理"这条规则就够了。
六、三种落地方式怎么选
三条路线放在一起对照(CGLIB 的运行机制见上一节):
| 维度 | 静态代理 | JDK 动态代理 | CGLIB / ByteBuddy |
|---|---|---|---|
| 是否需要接口 | 不强制(但通常有) | 必须有 | 不需要 |
| 代理类数量 | 每个接口手写一个 | 运行时按接口生成,一个 handler 复用 | 运行时按类生成子类 |
能否代理 final 类/方法 | 可以(自己写实现) | 无接口即不可用 | 不可以(子类覆写不了 final) |
| 调用开销 | 0(直接调用) | 反射调用,多一层间接 | 接近直接调用,但仍有方法分派 |
| 调试体验 | 最好 | 栈里出现 $Proxy0 | 栈里出现 Xxx$$EnhancerBySpringCGLIB$$xxx |
| 典型场景 | 层次少、要精确控制 | MyBatis Mapper、Feign、RMI stub | Spring AOP 默认、代理无接口的类 |
Spring 的选择规则(实战常问):
- 目标类实现了接口 → 默认用 JDK 动态代理;
- 目标类没有接口 → 只能用 CGLIB;
- 想强制用 CGLIB:
spring.aop.proxy-target-class=true(Spring Boot 2.x 起默认就是true,所以你现在大概率一直在用 CGLIB 代理); - 无论哪种,代理都要求目标类是"能被代理的":JDK 代理要接口,CGLIB 要非
final的类与方法,private方法更是谁都代理不了(Spring AOP 只作用于可覆写的实例方法,且 JDK 代理下只有public方法会生效)。
七、框架里的代理:一个经典失效现场
代理在生产代码里最典型的露面方式就是 AOP。下面这段代码是面试与踩坑榜双料常客——@Transactional 自调用失效:
// 说明:这是 Spring 容器里的行为示意(无法脱离容器单跑),关注注释里的 ⚠ / ✅
@Service
public class OrderService {
@Autowired private OrderMapper orderMapper;
@Autowired private AuditService auditService;
/** 外部调用:调用方拿到的是代理 → 事务生效 */
@Transactional
public void createOrder(Order order) {
orderMapper.insert(order);
// ⚠ 自调用:this 指向真实对象,绕过了代理,
// 因此 audit() 上的 @Transactional(REQUIRES_NEW) / @Cacheable 全部失效
this.audit(order);
// ✅ 修法一:注入自己(Spring 会注入代理对象,注意别造成构造器循环依赖)
// self.audit(order);
}
/** 如果它被外部直接调用,事务是生效的;被同类方法调用则不会 */
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void audit(Order order) {
auditService.log(order);
}
}
三种修法,按推荐度排序:
- 拆到另一个 Bean:把
audit()移到AuditService里,跨 Bean 调用必然经过代理(最干净,也最符合单一职责); - 注入自己(
@Autowired private OrderService self;或用@Lazy打断循环依赖)后调self.audit(...); AopContext.currentProxy()(需@EnableAspectJAutoProxy(exposeProxy = true),侵入性最强)。
理解这个坑的关键就是本文的结构图:代理是调用方拿到的那层壳,而 this 是壳里的真身。任何"绕过壳"的调用,注解都会静默失效——不报错,只是不生效,所以格外难查。
其他框架里的代理:
- MyBatis Mapper:你只写
interface UserMapper,运行时由 JDK 动态代理生成实现,invoke()里根据方法名去找 SQL; - Feign / Dubbo:接口 = 远程服务的本地门面,代理负责序列化、网络调用、重试——远程代理的标准形态;
- Hibernate / JPA 懒加载:集合与关联对象被替换成代理,首次访问时才查询——虚拟代理;
@Cacheable/@PreAuthorize/@Async:都是"智能引用"或"保护代理"的落地。
八、代理 vs 装饰器 vs 适配器
这是结构型模式里最经典的对比题,因为代理和装饰器的 UML 几乎一模一样:都实现同一接口、都持有目标对象引用。区别只在意图:

| 维度 | 代理 | 装饰器 | 适配器 |
|---|---|---|---|
| 意图 | 控制访问 | 增强职责 | 转换接口 |
| 对外接口 | 与真实对象相同 | 与真实对象相同 | 不同(适配成调用方要的样子) |
| 目标对象 | 通常在编译期就确定(自己创建或注入) | 通常由调用方传入(可自由组合、层层嵌套) | 被适配者通常是他人提供的类 |
| 是否可叠加 | 一般单层,链式代理较少 | 可任意层叠(这也是它的卖点) | 一般不叠加 |
| 典型例子 | Spring AOP、MyBatis Mapper、RPC stub | BufferedInputStream、Collections.unmodifiableList | InputStreamReader、Arrays.asList |
一句话记忆:代理管"能不能访问",装饰器管"多做什么",适配器管"接口不对付"。
顺带把最容易连带混淆的外观(Facade)也放进来:外观是"把多个子系统接口简化成一个入口",它不实现子系统接口,属于"重新组织接口",与上面三者都不同。
九、什么时候不要用代理
- 只是为了记日志:如果只是零散的几行
log.info,直接写比引入代理层更好读;代理的价值在于横切、统一、可复用。 - 性能敏感的热路径:动态代理的反射/分派开销在大循环里会累积。能静态代理就用静态代理,或者干脆把逻辑写进真实对象。
- 调试成本 > 收益:栈里全是
$Proxy/$$EnhancerBySpringCGLIB时,排查问题的时间会明显变长。团队如果不熟 AOP,慎用"隐式代理"。 - 想靠代理解决"设计问题":代理掩盖不了糟糕的接口设计。如果一个类的方法太多、职责太杂,先重构它,别急着给它套代理。
十、参考资料
- GoF《设计模式:可复用面向对象软件的基础》——Proxy 章(四种形态、与 Decorator 的对比)
java.lang.reflect.Proxy/InvocationHandlerJDK 文档(newProxyInstance的三个参数与限制)- Spring Framework 文档:AOP Proxies(JDK 动态代理 vs CGLIB、
proxy-target-class的选择规则) - Spring 文档:Declarative Transaction Management(自调用为何失效、
exposeProxy的作用) - MyBatis 文档:Mapper 接口与
MapperProxy(动态代理在实际框架中的实现) - CGLIB(Code Generation Library)与 Spring 内置的 repackage 版本
org.springframework.cglib(Enhancer+MethodInterceptor) - ByteBuddy 文档(现代字节码生成库;思路同 CGLIB,Hibernate / Mockito 在用)
评论