代理对象和目标对象有什么区别?为什么不能直接使用目标对象?
简化版
代理对象是对外暴露的增强入口,目标对象是真正执行业务逻辑的对象。直接使用目标对象会绕过权限、事务、缓存、日志等增强逻辑,所以在 Spring AOP、RPC 客户端、权限代理等场景中,调用方应依赖代理对象而不是目标对象。
详细版
代理模式的结构通常是:
- Subject:统一接口;
- RealSubject:真实业务对象;
- Proxy:代理对象,对外表现得像真实对象;
- Client:调用方,只依赖接口。
代理对象和目标对象通常实现同一接口或暴露相似方法,但职责不同。目标对象负责业务本身,代理对象负责访问控制、懒加载、缓存、事务、远程调用、监控埋点等附加能力。
在 Spring 中,容器注入给调用方的 Bean 可能是代理对象。如果你通过反射、手动 new、错误地暴露 target,直接调用目标对象,就会绕过增强。RPC 客户端也类似:调用方拿到的是本地代理对象,真正网络请求由代理完成;如果没有代理,就没有序列化、负载均衡、超时重试等能力。
面试回答要强调:代理对象不是“多余的一层”,而是横切能力和访问边界所在的位置。
完整版教学
一、代理对象和目标对象的职责分工
目标对象负责核心业务逻辑。比如 UserServiceImpl.createUser() 只关心创建用户。代理对象负责在业务调用前后增加额外逻辑,比如开启事务、检查权限、打印日志、记录耗时、处理远程调用。
Client
-> Proxy(UserService)
-> before: 权限/事务/日志
-> Target(UserServiceImpl)
-> after: 提交事务/记录耗时
代理对象对调用方伪装成同一个接口,所以调用方不用知道自己面对的是代理还是真实对象。这正是代理模式的价值:控制访问,同时不改变调用方的主要代码。
记忆钩子:目标对象负责“做事”,代理对象负责“让这件事按规则发生”。
二、为什么直接使用目标对象会出问题
直接使用目标对象的最大问题是绕过增强链。比如一个方法本来需要事务代理保护,如果调用方拿到的是原始目标对象,事务拦截器就不会执行。业务代码看起来成功调用了方法,但少了外层保障。
UserService target = new UserServiceImpl();
target.createUser(command); // 没有事务、没有权限、没有日志
在 Spring 里,正确做法是从容器拿 Bean:
UserService service = applicationContext.getBean(UserService.class);
service.createUser(command); // 可能经过代理
如果目标方法里有 3 个增强:权限校验、事务、耗时监控,直接调用目标对象就会一次性绕过 3 个增强。风险不是少了一条日志,而是访问边界整体失效。
三、用数字例子理解增强链价值
假设一次下单调用需要 4 个步骤:鉴权、开启事务、写订单、记录审计。目标对象只实现写订单,代理链负责另外 3 个步骤。
代理调用:
1. checkPermission(user, "ORDER_CREATE")
2. beginTransaction()
3. target.createOrder()
4. commitTransaction()
5. auditLog()
目标对象直调:
1. target.createOrder()
如果直调目标对象,执行步骤从 5 个变成 1 个,看起来更快,但语义已经错了。系统安全、数据一致性和可观测性都可能被破坏。面试中说“代理会带来性能开销”没错,但不能因此绕过代理,因为代理承载的是业务边界。
四、Spring 中如何识别代理对象
Spring 中可以通过工具判断一个 Bean 是否是代理对象。JDK 动态代理生成的对象通常是 $Proxy...,CGLIB 代理的类名通常包含 $$SpringCGLIB$$ 或类似标记。
Object bean = applicationContext.getBean(UserService.class);
System.out.println(AopUtils.isAopProxy(bean));
System.out.println(AopUtils.isJdkDynamicProxy(bean));
System.out.println(AopUtils.isCglibProxy(bean));
这类检查在排查事务、缓存、切面不生效时很有用。你要确认三件事:调用方拿到的是不是容器里的 Bean;这个 Bean 是不是代理;调用的方法是不是能被代理拦截。
排查顺序:
1. 对象是否由 Spring 创建
2. 注入对象是否为代理对象
3. 调用是否经过代理入口
4. 方法是否 public / 非 final / 可匹配切点
五、JDK 代理和 CGLIB 下的对象边界差异
JDK 动态代理基于接口,代理对象和目标对象通常不是同一个具体类。调用方应该依赖接口,否则可能注入失败或类型转换失败。CGLIB 基于子类,代理对象是目标类的子类,因此可以按目标类类型注入,但 final 类和 final 方法会受限制。
| 维度 | JDK 动态代理 | CGLIB 代理 |
|---|---|---|
| 代理基础 | 接口 | 子类 |
| 代理对象类型 | 实现接口的代理类 | 目标类子类 |
| 推荐注入类型 | 接口 | 接口或类 |
| 限制 | 目标类需有接口 | final 类/方法难代理 |
| 目标对象关系 | 代理持有目标对象 | 子类代理调用父类逻辑 |
无论哪种代理,核心都一样:调用方应通过代理入口访问增强能力,而不是绕开代理去拿原始对象。
六、RPC 中代理对象更像本地门面
RPC 客户端里的代理对象并不只是增强本地对象,它甚至没有本地真实业务实现。调用方调用一个接口方法,代理负责把方法名、参数、版本、超时时间等信息封装成网络请求。
client.userService.getById(1001)
-> 本地代理
-> 序列化请求
-> 选择服务实例
-> 网络发送
-> 反序列化响应
-> 返回 UserDTO
如果没有代理对象,调用方就要自己写序列化、连接池、负载均衡、超时、重试、熔断等逻辑。这里代理对象就是本地调用体验和远程通信复杂度之间的隔离层。
七、代理边界设计的工程原则
代理对象应该只增强访问过程,不应该偷偷改变业务语义。比如日志代理可以记录参数和耗时,权限代理可以拒绝非法访问,缓存代理可以返回等价结果。但代理不应该随意改订单金额、吞掉关键异常、绕过目标对象的不变量。
合理代理:
权限校验、事务控制、缓存、懒加载、远程访问、监控
危险代理:
静默修改业务参数
隐藏失败异常
改变核心返回语义
在代理里堆复杂业务流程
代理边界越清楚,系统越容易排查。否则问题出现时,调用方以为是目标方法行为,目标对象以为自己没写那段逻辑,最后只能在代理链里慢慢翻。
八、常见误区与追问
- 误区:代理对象和目标对象只是类型不同,行为一样。 代理对象多了承载横切逻辑的入口,行为边界可能完全不同。
- 误区:为了性能可以直接调用目标对象。 这样会绕过事务、权限、缓存、监控等关键规则,通常不可接受。
- 误区:CGLIB 代理对象就是目标对象本身。 它通常是目标类的子类代理,仍然有代理入口和增强链。
- 追问:怎么判断 Spring Bean 是不是代理? 使用
AopUtils.isAopProxy(),或观察运行时类名和注入类型。 - 追问:为什么建议面向接口注入? JDK 动态代理只能按接口代理,面向接口能减少代理实现差异带来的注入问题。
- 追问:RPC 代理的目标对象在哪里? 通常在远程服务端,本地代理负责把方法调用转成网络请求。
- 追问:代理中能不能写业务逻辑? 可以写访问控制类横切逻辑,但不应承载核心业务流程。
九、加强记忆
代理对象和目标对象的区别可以记成“入口”和“执行者”:代理是入口,负责规则、增强和访问控制;目标是执行者,负责核心业务。凡是事务、缓存、RPC、权限这类能力失效,先检查调用是不是绕过了代理入口。