设计模式预计阅读 23 分钟9153 字3804 次浏览

设计模式修炼之路 ⑬ 代理模式——控制访问,而非增强

控制访问而非增强、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,包的那几层全是"控制":这次让不让调?现在调还是待会儿调?在本地调还是发到远端调?——问题千变万化,套路只有一个:替身。

二、代理模式的优缺点

优点

  1. 职责隔离,横切逻辑有处安放:权限、日志、事务、监控这些"每个方法都要、又都不属于业务"的逻辑,从业务类里搬进代理——业务类一身轻松。
  2. 客户端零感知:代理实现同一接口,客户端代码一行不改就获得了全部控制能力——Spring 里一个 @Transactional 注解,背后代理已经换了一茬,你的 Service 代码毫无察觉。
  3. 控制点丰富:延迟创建(对象很重,用到再说)、拒绝访问(没权限直接 return)、远程转发(本地调用变网络调用)、结果缓存(第二次调用不出门)——全在转发前后那一小段里做文章。
  4. 动态代理通吃全家族:静态代理一个接口写一个类,动态代理一份代码适配任意接口的任意方法——这就是 AOP 能"一处声明、处处生效"的根。

缺点

  1. 类与间接层变多:每个要代理的接口都得有代理(静态),或运行时凭空生成代理类(动态)——请求链路多了一跳,新手 debug 时满屏 $Proxy0、CGLIB$$,容易迷路。
  2. JDK 动态代理有硬门槛:目标类必须实现接口——Java 单继承,运行时生成的代理类已经把唯一的继承名额用在了 Proxy 类上,只剩"实现接口"这一条路。
  3. CGLIB 有自己的坑:靠生成子类工作,final 类、final/private/static 方法统统拦不住;代理对象和目标对象行为"几乎"一致,这个"几乎"偶尔咬人。
  4. 自调用失效:目标对象内部 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 无下单权限,拒绝

业务只有一行,旁边站着五个横切逻辑。三宗罪:

  1. 业务类被淹没:想看"下单到底干了啥",得在权限、日志、计时的夹缝里找那一行;
  2. 横切逻辑不可复用:再有个 cancelOrder,同样的五段复制一遍;再有个 UserService,再复制一遍——N 个类 × M 个方法,复制 N×M 次;
  3. 改一处横切逻辑,全城大修:日志格式从中文换英文?几十个方法挨个进。

病根只有一个:横切逻辑和业务逻辑写在同一个方法体里,物理上分不开。

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});
    }
}

看懂这个结构,三件事同时明白:

  1. 为什么必须基于接口:$Proxy0 的 extends 名额已经给了 Proxy 类,Java 单继承,它只剩"实现接口"一条路能和 OrderService 挂上钩——目标类没接口,JDK 动态代理直接罢工;
  2. 为什么一份 Handler 通吃所有方法:每个方法的方法体都是同一句"打包路由给 h",method 对象在运行时才知道是 placeOrder 还是 getUserName——invoke 里那个 method.getName() 打印什么,全看客户端调了什么;
  3. 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 框架/工厂悄悄塞给你
客户端知情吗 知道,就是它自己套的 不知道,以为在调真身
生命周期 装饰器被动接收被装饰对象 代理常常自己掌管真身(懒加载时真身还没出生)
层数 随便叠,三层五层皆可 通常就一层

三连问做个了断:

  1. new BufferedInputStream(new FileInputStream(file))——谁写的这行?你,客户端自己。你清楚知道自己在层层加料,这是装饰器;
  2. getMapper(OrderMapper.class)——谁给你的这个对象?MyBatis。你不知道也不关心它是不是替身,它甚至没有真身。这是代理;
  3. Spring 容器里 @Autowired 进来的 Service——框架给的,带事务带日志的全功能替身,代理。

再补一个工程上的判别法:看构造器参数的语义。装饰器的构造参数是"被装饰者",纯转发目的;代理的构造参数常常是"配置"或干脆无参——因为真身要么由代理延迟创建,要么在远端根本不存在。

写在最后

回顾这一篇:代理模式在客户端和真实对象之间安插替身——同接口、持真身、控访问;静态代理手写一个类,胜在直观、败在一接口一类;JDK 动态代理运行时生成 $Proxy0,一份 InvocationHandler 通吃任意接口任意方法,代价是目标必须有接口;CGLIB 生成子类补上这块短板,拦不住的换成了 final/private/static;远程、懒加载、保护、缓存四类代理各霸一方,Spring AOP、MyBatis Mapper、Feign、Retrofit 全是它的产业;和装饰器的分界不在结构在意图——主动加料是装饰,无感接管是代理。至此,结构型七将(本系列含过滤器共八员)全部收官。

下一篇 ⑭ 模板模式,行为型第一位出场,整个系列进入下半场。结构型管"类怎么组装",行为型管"类怎么协作"——模板模式打头阵,干的事你天天在干:父类把流程骨架焊死,子类只往空位里填肉。HttpServlet 的 doGet/doPost、AbstractList 的 get、JUnit 的生命周期注解,还有那个"面试必问"的 AQS,全是它的地盘。

花

花开空白

开发者 · 西安

相关文章

评论(0)

还没有评论,来抢沙发吧

发表评论