设计模式修炼之路 ⑬ 代理模式——控制访问,而非增强
控制访问而非增强、JDK 动态代理的 $Proxy0、CGLIB 与 Spring AOP 的老搭档
目录
上一篇享元模式给它留足了排面:"UML 和装饰器几乎一个模子:同样实现同一个接口、同样手里攥着一个同类对象——区别全在意图上"。今天这位压轴的结构型选手登场:代理模式(Proxy Pattern),GoF 23 人名单正牌选手、结构型七将的最后一位。装饰器回答"怎么给它加功能",代理回答"怎么替它管住访问"——懒加载、权限校验、远程调用,还有 JDK 动态代理、CGLIB、Spring AOP 这整座大厦的地基,全是这一篇的内容。前面十二篇的模式你也许日常真没写几个,但这一篇的东西,你的每一行 Spring 代码背后都在跑。
一、什么是代理模式?
先看定义:
为其他对象提供一种代理以控制对这个对象的访问。
翻译成人话:在客户端和真实对象之间插一个"替身"——替身和真身实现同一个接口,客户端调的其实全是替身;替身可以在转发前做检查、在转发后做收尾,甚至压根不转发。客户端对这一切无感,它以为自己一直在和真身说话。
生活类比是明星经纪人:粉丝想找明星商演,联系的全是经纪人;经纪人审资质(权限校验)、排档期(延迟安排)、谈完再转告明星(转发真身),粉丝从头到尾以为自己在"和明星那边"打交道。对照一下装饰器的贴膜:贴膜是你自己跑到店里主动选的,经纪人是明星团队塞给你、你没法绕开的——主动组装的叫装饰,被动接管的叫代理,这句先记下,第五节展开。
它有三个角色:
- Subject(抽象主题):真身和代理共同实现的接口,本篇里是 OrderService;
- RealSubject(真实主题):真正干活的对象,本篇里是 OrderServiceImpl;
- Proxy(代理):持有真身的引用,对外冒充真身,对内控制访问——本篇里先有手写的 OrderServiceProxy,后有运行时凭空生成的 $Proxy0。
精髓一句话:客户端只认 Subject,Proxy 里三层外三层地包着 RealSubject,包的那几层全是"控制":这次让不让调?现在调还是待会儿调?在本地调还是发到远端调?——问题千变万化,套路只有一个:替身。
二、代理模式的优缺点
优点
- 职责隔离,横切逻辑有处安放:权限、日志、事务、监控这些"每个方法都要、又都不属于业务"的逻辑,从业务类里搬进代理——业务类一身轻松。
- 客户端零感知:代理实现同一接口,客户端代码一行不改就获得了全部控制能力——Spring 里一个 @Transactional 注解,背后代理已经换了一茬,你的 Service 代码毫无察觉。
- 控制点丰富:延迟创建(对象很重,用到再说)、拒绝访问(没权限直接 return)、远程转发(本地调用变网络调用)、结果缓存(第二次调用不出门)——全在转发前后那一小段里做文章。
- 动态代理通吃全家族:静态代理一个接口写一个类,动态代理一份代码适配任意接口的任意方法——这就是 AOP 能"一处声明、处处生效"的根。
缺点
- 类与间接层变多:每个要代理的接口都得有代理(静态),或运行时凭空生成代理类(动态)——请求链路多了一跳,新手 debug 时满屏 $Proxy0、CGLIB$$,容易迷路。
- JDK 动态代理有硬门槛:目标类必须实现接口——Java 单继承,运行时生成的代理类已经把唯一的继承名额用在了 Proxy 类上,只剩"实现接口"这一条路。
- CGLIB 有自己的坑:靠生成子类工作,final 类、final/private/static 方法统统拦不住;代理对象和目标对象行为"几乎"一致,这个"几乎"偶尔咬人。
- 自调用失效:目标对象内部 this.method() 的调用不走代理——@Transactional 方法里 this 调另一个事务方法,事务不生效,这是 Spring 面试必考题,病根就在代理这一层。
三、代理模式的应用场景
按"控制什么"分四类,每一类都有扛鼎框架:
- 远程代理(Remote Proxy)——控制"在哪调":Dubbo、Feign 的客户端。你注入一个 XxxService 调方法,看似本地调用,代理把方法名和参数序列化成网络请求发到远端,再把返回值反序列化回来——RPC 的"像调本地方法一样调远程"全靠这层替身。
- 虚拟代理(Virtual Proxy)——控制"何时调":懒加载。MyBatis 关联查询配置 lazy 后,你 getOrders() 拿到的不是真 List,是个代理 List——首次遍历时它才真正发 SQL(N+1 问题的元凶也是它,功过一体)。Hibernate 的懒加载实体同理。
- 保护代理(Protection Proxy)——控制"谁能量调":Spring Security 的方法级鉴权,@PreAuthorize("hasRole('ADMIN')") 后面站着的就是一圈代理在把门。
- 缓存/智能代理——控制"要不要真调":Spring 的 @Cacheable,命中缓存直接返回,方法体根本不执行;MyBatis 一级二级缓存也是包在查询外层的代理思想。
再点名三个天天用却未必意识到的:
- Spring AOP 的全部:@Transactional(事务开启/提交/回滚包在方法外)、@Async(调用线程换成线程池)、@Cacheable、全局日志切面——实现清一色动态代理。可以说没有代理模式就没有 Spring 的半条命。
- MyBatis 的 Mapper:你只写接口、不写实现类,sqlSession.getMapper(OrderMapper.class) 拿到的是 MapperProxy 代理对象,把方法调用翻译成 SQL 执行——接口连实现类都不存在,全靠代理凭空造。本系列第六次在 MyBatis 撞见老朋友。
- Retrofit / Feign 的 HTTP 客户端:接口方法加注解,代理把方法调用变成 HTTP 请求——和 Mapper 一个思路。
什么时候该用:想要的能力是"包在方法外面的"(校验、事务、日志、缓存、远程、延迟),且希望业务代码对此无感——就该在代理上做文章,而不是往业务方法里塞。
四、代理模式的实现方式
示例代码在 design-pattern 项目的 proxy 包里:
design-pattern/src/main/java
└── proxy // 代理模式
├── BadOrderService.java // 反面教材:横切逻辑长在业务里
├── OrderService.java // 抽象主题:业务接口
├── OrderServiceImpl.java // 真实主题:纯业务
├── OrderServiceProxy.java // 静态代理:手写的替身
├── UserService.java // 第二个业务接口(证明动态代理不挑接口)
├── UserServiceImpl.java // 第二个真实主题
├── JdkProxyFactory.java // JDK 动态代理工厂
└── ProxyTest.java // 测试入口
场景:下单。权限校验、日志、耗时统计这些横切逻辑要有,但不能让它们长在业务里。三步走:先看病,再手写静态代理,最后上 JDK 动态代理。
1. 反面教材:横切逻辑长在业务里
// 反面教材:权限校验、日志、耗时统计全横在业务方法里
public class BadOrderService {
public void placeOrder(String user, String orderId) {
// 横切逻辑 1:权限校验
if (!"admin".equals(user) && !"vip".equals(user)) {
System.out.println("[反面教材] " + user + " 无下单权限,拒绝");
return;
}
// 横切逻辑 2:日志
System.out.println("[反面教材] 开始下单:" + orderId);
long start = System.currentTimeMillis();
// —— 真正的业务,只有一行 ——
System.out.println("[反面教材] 订单 " + orderId + " 已创建");
// 横切逻辑 3:耗时统计
System.out.println("[反面教材] 耗时:" + (System.currentTimeMillis() - start) + "ms");
}
}
运行结果:
===== 反面教材:横切逻辑长在业务里 =====
[反面教材] 开始下单:ORDER-1001
[反面教材] 订单 ORDER-1001 已创建
[反面教材] 耗时:6ms
[反面教材] guest 无下单权限,拒绝
业务只有一行,旁边站着五个横切逻辑。三宗罪:
- 业务类被淹没:想看"下单到底干了啥",得在权限、日志、计时的夹缝里找那一行;
- 横切逻辑不可复用:再有个 cancelOrder,同样的五段复制一遍;再有个 UserService,再复制一遍——N 个类 × M 个方法,复制 N×M 次;
- 改一处横切逻辑,全城大修:日志格式从中文换英文?几十个方法挨个进。
病根只有一个:横切逻辑和业务逻辑写在同一个方法体里,物理上分不开。
2. 静态代理:手写一个替身
先立接口,真身和代理的共同身份:
// 抽象主题:业务接口,真实对象和代理共同实现它,客户端只认它
public interface OrderService {
void placeOrder(String user, String orderId);
}
// 真实主题:只写业务,一行横切逻辑都没有
public class OrderServiceImpl implements OrderService {
@Override
public void placeOrder(String user, String orderId) {
System.out.println("[真实对象] 订单 " + orderId + " 已创建");
}
}
替身登场:
// 静态代理:和真实主题实现同一个接口,在转发前后做访问控制
public class OrderServiceProxy implements OrderService {
private final OrderService target;
public OrderServiceProxy(OrderService target) {
this.target = target;
}
@Override
public void placeOrder(String user, String orderId) {
// 前置控制:权限校验,不合法直接拒之门外
if (!"admin".equals(user) && !"vip".equals(user)) {
System.out.println("[静态代理] " + user + " 无下单权限,拒绝");
return;
}
System.out.println("[静态代理] 开始下单:" + orderId);
long start = System.currentTimeMillis();
// 转发给真实对象
target.placeOrder(user, orderId);
// 后置控制:耗时统计
System.out.println("[静态代理] 耗时:" + (System.currentTimeMillis() - start) + "ms");
}
}
测试:
OrderService proxy = new OrderServiceProxy(new OrderServiceImpl());
proxy.placeOrder("vip", "ORDER-1001");
proxy.placeOrder("guest", "ORDER-1002");
运行结果:
===== 正解 1:静态代理 =====
[静态代理] 开始下单:ORDER-1001
[真实对象] 订单 ORDER-1001 已创建
[静态代理] 耗时:1ms
[静态代理] guest 无下单权限,拒绝
立竿见影:OrderServiceImpl 回归一行业务的纯粹,权限、日志、计时全搬进了代理;客户端只认 OrderService 接口,它分不清(也不需要分清)手里是真是假。guest 那单被拦在门外,真身根本不知道世界上有过这单——控制访问,四个字落地。
但静态代理的死穴也在这儿:一个接口一个代理类。OrderService 写个 OrderServiceProxy,UserService 就得写个 UserServiceProxy,接口一多,代理类成灾;而且 placeOrder 加个 cancelOrder 方法,代理类也得跟着加一份转发——纯搬运,毫无营养。
3. JDK 动态代理:运行时凭空造替身
JDK 里藏着一个神器:Proxy.newProxyInstance——运行时动态生成代理类,把"实现接口 + 转发"这套体力活全包了,你只需要提供一个 InvocationHandler,告诉它"方法调用进来时怎么处理":
// JDK 动态代理工厂:运行时生成代理类,一个 InvocationHandler 适配任意接口的任意方法
public class JdkProxyFactory {
@SuppressWarnings("unchecked")
public static <T> T create(T target) {
return (T) Proxy.newProxyInstance(
target.getClass().getClassLoader(),
target.getClass().getInterfaces(),
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 任意接口的任意方法调用,都先路由到这——AOP 的雏形
System.out.println("[JDK代理] 开始调用 " + method.getName() + ",参数:" + Arrays.toString(args));
long start = System.currentTimeMillis();
Object result = method.invoke(target, args);
System.out.println("[JDK代理] " + method.getName() + " 耗时:" + (System.currentTimeMillis() - start) + "ms");
return result;
}
});
}
}
一个工厂,通吃两个互不相识的接口:
OrderService orderProxy = JdkProxyFactory.create(new OrderServiceImpl());
orderProxy.placeOrder("admin", "ORDER-1001");
UserService userProxy = JdkProxyFactory.create(new UserServiceImpl());
String name = userProxy.getUserName("U-9527");
System.out.println("拿到结果:" + name);
// 看看 JDK 在运行时生成的代理类到底叫什么
System.out.println("orderProxy 的真实类名:" + orderProxy.getClass().getName());
运行结果:
===== 正解 2:JDK 动态代理 =====
[JDK代理] 开始调用 placeOrder,参数:[admin, ORDER-1001]
[真实对象] 订单 ORDER-1001 已创建
[JDK代理] placeOrder 耗时:0ms
[JDK代理] 开始调用 getUserName,参数:[U-9527]
[真实对象] 查询用户:U-9527
[JDK代理] getUserName 耗时:0ms
拿到结果:用户U-9527
orderProxy 的真实类名:jdk.proxy1.$Proxy0
最后一行是本篇的彩蛋:orderProxy 的类名是 jdk.proxy1.$Proxy0——你翻遍整个项目源码都找不到这个类,它是 Proxy.newProxyInstance 在运行时动态生成、编译、加载进 JVM 的。想象一下它大致长什么样:
// JDK 运行时生成的代理类(示意,还原大致结构)
public final class $Proxy0 extends Proxy implements OrderService {
public $Proxy0(InvocationHandler h) { super(h); }
@Override
public void placeOrder(String user, String orderId) {
// 没有一行自己的逻辑:把"哪个方法、什么参数"打包,全部路由给 InvocationHandler
super.h.invoke(this, placeOrderMethod, new Object[]{user, orderId});
}
}
看懂这个结构,三件事同时明白:
- 为什么必须基于接口:$Proxy0 的 extends 名额已经给了 Proxy 类,Java 单继承,它只剩"实现接口"一条路能和 OrderService 挂上钩——目标类没接口,JDK 动态代理直接罢工;
- 为什么一份 Handler 通吃所有方法:每个方法的方法体都是同一句"打包路由给 h",method 对象在运行时才知道是 placeOrder 还是 getUserName——invoke 里那个 method.getName() 打印什么,全看客户端调了什么;
- AOP 的本质:把上面这个 invoke 换成"先开事务 → method.invoke → 提交事务 → catch 里回滚",就是 Spring @Transactional 的全部原理。所谓切面编程,就是给 InvocationHandler 加了"哪些方法、什么条件才拦"的配置化能力。
五、CGLIB:没有接口,还有子类
JDK 动态代理的门槛是"必须有接口",可老项目里一堆没接口的类怎么办?CGLIB(Code Generation Library)换了个思路:不实现接口了,直接生成目标类的子类,重写非 final 方法来做拦截:
// CGLIB 的用法(示意,需要引入 cglib 或 spring 的 repackaged 版本)
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(OrderServiceImpl.class); // 目标类:当爹
enhancer.setCallback(new MethodInterceptor() {
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
System.out.println("[CGLIB] 拦截到 " + method.getName()); // 相当于 invoke
return proxy.invokeSuper(obj, args); // 相当于转发,但走的是 FastClass 机制,比纯反射快
}
});
OrderServiceImpl proxy = (OrderServiceImpl) enhancer.create(); // 产物是 OrderServiceImpl 的子类
两套动态代理摆在一起看:
| JDK 动态代理 | CGLIB | |
|---|---|---|
| 依赖 | JDK 自带,零依赖 | 第三方库(Spring 已内置打包版) |
| 前提 | 目标类实现接口 | 目标类可被继承 |
| 手段 | 运行时生成"实现同一接口"的类 | 运行时生成"目标类的子类" |
| 拦不住的 | 接口里没有的方法 | final 类、final/private/static 方法 |
| 拦截回调 | InvocationHandler.invoke | MethodInterceptor.intercept |
| Spring 的选择 | 目标有接口时(旧默认) | Spring Boot 2.x 起全面默认 |
Spring Boot 2.x 为什么把默认改成 CGLIB(proxyTargetClass=true)?因为 JDK 代理只认接口,容器里注入具体类型就报错——@Autowired OrderServiceImpl 在 JDK 代理下直接翻车,代理对象根本不是 OrderServiceImpl 的实例。CGLIB 代理是子类,is-a 关系还在,怎么注都不炸——框架选择"默认不咬人"的方案,天经地义。
六、代理 vs 装饰器:一模一样的结构,相反的意图
五节之前欠的债现在还:代理和装饰器,UML 放一起你根本分不出谁是谁——都实现 Subject、都持有 Subject 字段、方法体都是"干点自己的 + 转发"。GoF 把它们列成两个模式,分的不是结构,是意图:
| 装饰器模式 | 代理模式 | |
|---|---|---|
| 意图 | **增强**:给对象加职责 | **控制**:管住对对象的访问 |
| 谁组装 | 客户端主动一层层 new | 框架/工厂悄悄塞给你 |
| 客户端知情吗 | 知道,就是它自己套的 | 不知道,以为在调真身 |
| 生命周期 | 装饰器被动接收被装饰对象 | 代理常常自己掌管真身(懒加载时真身还没出生) |
| 层数 | 随便叠,三层五层皆可 | 通常就一层 |
三连问做个了断:
new BufferedInputStream(new FileInputStream(file))——谁写的这行?你,客户端自己。你清楚知道自己在层层加料,这是装饰器;getMapper(OrderMapper.class)——谁给你的这个对象?MyBatis。你不知道也不关心它是不是替身,它甚至没有真身。这是代理;- Spring 容器里
@Autowired进来的 Service——框架给的,带事务带日志的全功能替身,代理。
再补一个工程上的判别法:看构造器参数的语义。装饰器的构造参数是"被装饰者",纯转发目的;代理的构造参数常常是"配置"或干脆无参——因为真身要么由代理延迟创建,要么在远端根本不存在。
写在最后
回顾这一篇:代理模式在客户端和真实对象之间安插替身——同接口、持真身、控访问;静态代理手写一个类,胜在直观、败在一接口一类;JDK 动态代理运行时生成 $Proxy0,一份 InvocationHandler 通吃任意接口任意方法,代价是目标必须有接口;CGLIB 生成子类补上这块短板,拦不住的换成了 final/private/static;远程、懒加载、保护、缓存四类代理各霸一方,Spring AOP、MyBatis Mapper、Feign、Retrofit 全是它的产业;和装饰器的分界不在结构在意图——主动加料是装饰,无感接管是代理。至此,结构型七将(本系列含过滤器共八员)全部收官。
下一篇 ⑭ 模板模式,行为型第一位出场,整个系列进入下半场。结构型管"类怎么组装",行为型管"类怎么协作"——模板模式打头阵,干的事你天天在干:父类把流程骨架焊死,子类只往空位里填肉。HttpServlet 的 doGet/doPost、AbstractList 的 get、JUnit 的生命周期注解,还有那个"面试必问"的 AQS,全是它的地盘。