从这一篇开始,我们离开"对象怎么造出来"(创建型),进入结构型模式——它们关心的是另一件事:已经造好的类和对象,该如何组合、连接、搭配,才能既满足需求又保持松耦合。 结构型七个模式,几乎都是在玩"包装"和"组合"的艺术。我们从其中实战出镜率最高的一个开场——代理模式(Proxy)。它是 Spring AOP 的底层基石,几乎每个用过 Spring 的人都在享受它的红利,却未必知道它的原理。

代理的思想在生活里随处可见:你买房不直接对接房东,而是找中介;明星不亲自谈商演,而是通过经纪人。中介和经纪人,就是"代理"——他们和本人对外提供同样的能力(都能谈房、谈商演),但你和他们打交道,而不是和本人;他们还能在这个过程里加一些本人不方便做的事(筛选、议价、收中介费)。 代理模式在代码里干的是一模一样的事:给某个对象找一个"替身",外界通过替身来访问真实对象,替身则可以在调用真实对象前后,插入一些额外的动作——记日志、做权限校验、加缓存、开事务。

这一篇我们就从"给订单服务加一圈横切逻辑"这个真实需求出发,一步步走过代理的三种形态:手写的静态代理 → 运行时生成的 JDK 动态代理 → 基于继承的 CGLIB,讲清它们各自的原理、局限和取舍,最后落到 Spring AOP 到底用了哪个、怎么选的。

这篇文章按这条线索展开:先说清代理要解决什么问题、它的核心结构;再手写一个静态代理,看它怎么把日志逻辑和业务分开;接着指出静态代理"一个接口配一个代理类"的类爆炸天花板;然后引出 JDK 动态代理——运行时凭空生成代理,一套逻辑通吃所有接口;再看它"只能代理接口"的局限,以及 CGLIB 如何用继承补上这一环;最后对比三者、落到 Spring AOP,并划清代理和下一篇装饰器的界限。贯穿例是给 OrderService 加日志、权限、缓存。

目录

  1. 代理要解决什么:给对象找个替身
  2. 静态代理:手写一个替身
  3. 静态代理的天花板:类爆炸
  4. JDK 动态代理:运行时凭空生成
  5. CGLIB:连接口都不要,直接继承
  6. 三种代理对比,与 Spring AOP
  7. 代理 vs 装饰器,以及现实身影

一、代理要解决什么:给对象找个替身

先看需求。我们有一个订单服务,业务逻辑很纯粹:

public interface OrderService {
    void createOrder(long userId);
}

public class OrderServiceImpl implements OrderService {
    public void createOrder(long userId) {
        System.out.println("创建订单,用户:" + userId);   // 纯业务逻辑
    }
}

现在产品提了一堆"附加要求":每次下单前后要打日志、下单前要校验用户有没有权限、高频查询要加缓存、涉及金额要开事务。这些要求有个共同特点——它们都不是订单业务本身,而是"横切"在业务外围的通用关注点(专业叫法是 cross-cutting concern,横切关注点)。

最粗暴的做法,是把这些逻辑直接塞进 createOrder:

public void createOrder(long userId) {
    System.out.println("[日志] 开始下单");        // 混入日志
    if (!hasPermission(userId)) return;          // 混入权限
    System.out.println("创建订单,用户:" + userId); // 真正的业务
    System.out.println("[日志] 下单完成");        // 又是日志
}

这就把第一篇批过的毛病全犯了:业务逻辑和横切逻辑搅在一起(违反单一职责),每个方法都要抄一遍日志和权限代码(大量重复),而且想给别的服务也加同样的逻辑,还得再抄。我们需要的是:在完全不动 OrderServiceImpl 业务代码的前提下,给它套上一圈横切逻辑。 这正是代理的使命。

代理模式的结构非常简单,三个角色:

  • 抽象主题(Subject):真实对象和代理共同的接口(OrderService),保证两者对外长得一样;
  • 真实主题(RealSubject):干实事的那个(OrderServiceImpl);
  • 代理(Proxy):持有真实对象的引用,对外实现同样的接口,在转发调用的前后插入额外逻辑。

关键就在那句"对外实现同样的接口"——因为代理和真实对象长得一模一样,所以调用方拿到代理时,根本感觉不到自己面对的是替身,可以无缝替换。这也是代理能"偷偷"加逻辑的前提。

二、静态代理:手写一个替身

最直接的实现,是手写一个代理类,让它也实现 OrderService 接口,内部持有真实对象,在调用前后加上横切逻辑。这叫静态代理——"静态"是指这个代理类在编译期就由你写好、确定下来了。

public class OrderServiceProxy implements OrderService {
    private final OrderService target;   // 持有真实对象

    public OrderServiceProxy(OrderService target) {
        this.target = target;
    }

    @Override
    public void createOrder(long userId) {
        // —— 调用前的横切逻辑 ——
        System.out.println("[日志] 开始下单,用户:" + userId);
        long start = System.currentTimeMillis();

        target.createOrder(userId);      // 转发给真实对象干活

        // —— 调用后的横切逻辑 ——
        System.out.println("[日志] 下单完成,耗时:"
                + (System.currentTimeMillis() - start) + "ms");
    }
}

用起来,调用方拿到的是代理,但因为类型都是 OrderService,它浑然不觉:

OrderService service = new OrderServiceProxy(new OrderServiceImpl());
service.createOrder(1001L);   // 自动带上了日志和耗时统计

这一步的收益很实在:OrderServiceImpl 一个字没改,日志逻辑却加上了,而且业务和日志彻底分离——业务归业务类,横切归代理类,各司其职。这正是单一职责和开闭原则的体现:要加横切逻辑,不改原类,而是加一个代理。

静态代理清晰、直观,在代理类不多的时候很好用。但它有一个随着系统变大就会暴露的硬伤。

三、静态代理的天花板:类爆炸

想象一下,系统里不止 OrderService,还有 UserServiceProductServicePaymentService……几十个 service。如果都想加上"日志 + 耗时统计"这套逻辑,用静态代理意味着什么?

你得为每一个接口,手写一个几乎一模一样的代理类: OrderServiceProxyUserServiceProxyProductServiceProxy……每个代理类里,那段"前面打日志、中间转发、后面记耗时"的代码几乎完全重复,只是转发的目标和方法不同。这就是静态代理的两个致命问题:

  • 类爆炸:有多少个要代理的接口,就得写多少个代理类。几十个 service 就是几十个代理类,全是重复样板。
  • 难以维护:哪天日志格式要改一下,你得挨个去改几十个代理类,一个都不能漏。

更麻烦的是,一个接口如果有多个方法,代理类里每个方法都要写一遍转发和横切逻辑,重复进一步放大。静态代理的根本局限在于:代理类必须在编译期就为每一个具体接口写死。 而"打日志、记耗时"这套逻辑其实和具体是哪个接口、哪个方法毫无关系——它是通用的。我们真正想要的是:把这套通用的横切逻辑只写一次,让它能自动应用到任意接口、任意方法上。

能做到这一点吗?能——但代理类不能再由我们手写了,得让程序在运行时自动生成。这就是动态代理。

四、JDK 动态代理:运行时凭空生成

动态代理的核心思想:不再手写代理类,而是在程序运行时,由 JVM 动态地、凭空地生成一个代理对象。 你只需要把"要插入的横切逻辑"写在一个地方,剩下的"生成代理类、实现接口、转发调用"全交给 JDK 自动完成。

JDK 自带的动态代理,靠两个核心 API:InvocationHandler 接口和 Proxy 类。

第一步,把横切逻辑写进一个 InvocationHandler。它只有一个 invoke 方法,所有被代理的方法调用,最终都会汇集到这个 invoke 里来:

public class LogHandler implements InvocationHandler {
    private final Object target;   // 真实对象,注意是 Object,不限定类型

    public LogHandler(Object target) {
        this.target = target;
    }

    // 对代理对象的任何方法调用,都会转到这里
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // —— 前置横切逻辑(只写这一次!)——
        System.out.println("[日志] 调用方法:" + method.getName());
        long start = System.currentTimeMillis();

        Object result = method.invoke(target, args);   // 反射调用真实对象的方法

        // —— 后置横切逻辑 ——
        System.out.println("[日志] " + method.getName() + " 耗时:"
                + (System.currentTimeMillis() - start) + "ms");
        return result;
    }
}

第二步,用 Proxy.newProxyInstance 在运行时生成代理对象:

OrderService target = new OrderServiceImpl();

OrderService proxy = (OrderService) Proxy.newProxyInstance(
        target.getClass().getClassLoader(),        // 类加载器
        target.getClass().getInterfaces(),         // 要实现哪些接口
        new LogHandler(target));                    // 横切逻辑

proxy.createOrder(1001L);   // 走的是动态生成的代理,自动带日志

关键的飞跃在这里:同一个 LogHandler,可以套在任何对象上。 因为它持有的是 Object target、通过反射的 Method 来调用,完全不关心具体是哪个接口。给 UserServiceProductService 加日志?一行 newProxyInstance 换个 target 就行,再也不用为每个接口手写代理类了。第三节那个类爆炸问题,被彻底解决——横切逻辑只写一次,通吃所有接口。

Proxy.newProxyInstance 到底做了什么?它在运行时动态生成了一个类(名字通常形如 $Proxy0),这个类实现了你指定的那些接口,每个方法内部都转调你的 handler.invoke()这个类是 JVM 在内存里现造出来的,编译期根本不存在。 这正是"动态"二字的含义,也是它比静态代理强大的根本原因。

不过,细心的你可能注意到了那行 target.getClass().getInterfaces()——JDK 动态代理是基于接口的。这既是它的机制,也是它的局限。

五、CGLIB:连接口都不要,直接继承

JDK 动态代理有一个硬性前提:被代理的类必须实现了接口。 因为它生成的代理类,是靠"实现和真实对象相同的接口"来伪装成真实对象的。如果你有一个类压根没实现任何接口,只是个光秃秃的类,JDK 动态代理就无能为力了。

可现实中确实有很多类没有接口。这时候就轮到 CGLIB(Code Generation Library) 登场了。它换了一个思路:既然不能靠"实现接口"来伪装,那就靠"继承"——动态生成一个目标类的子类,当作代理。

// CGLIB:通过继承目标类来生成代理,不需要接口
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("[日志] 调用:" + method.getName());
        Object result = proxy.invokeSuper(obj, args);   // 调用父类(即原方法)
        System.out.println("[日志] 完成");
        return result;
    }
});
OrderServiceImpl proxy = (OrderServiceImpl) enhancer.create();
proxy.createOrder(1001L);

CGLIB 动态生成的代理类,是目标类的一个子类,它重写了父类的方法,在重写的方法里插入横切逻辑、再通过 invokeSuper 调用父类的原始实现。因为是子类,所以它天然就是目标类的一种(is-a),同样能无缝替换,而且完全不需要目标类实现任何接口

但"靠继承"也带来了它自己的局限,根源是 Java 继承的规则:

  • 无法代理 final:final 类不能被继承,CGLIB 生成子类的路子直接断了;
  • 无法代理 final 方法:final 方法不能被重写,这些方法上的横切逻辑就加不上。

所以 JDK 动态代理和 CGLIB 恰好互补:一个靠实现接口(要求有接口),一个靠继承子类(要求可被继承)。 到这里,代理的三种形态就集齐了,该做个总对比,并看看 Spring 是怎么用它们的。

六、三种代理对比,与 Spring AOP

把三种代理放在一起,一张表看清:

静态代理JDK 动态代理CGLIB
代理类产生时机编译期,手写运行时,JVM 生成运行时,生成子类
实现机制手写实现接口反射 + 实现接口继承(生成子类)
是否要求接口要(要么接口要么继承)必须有接口不要求接口
主要局限类爆炸、难维护只能代理接口方法不能代理 final 类/方法
适用代理对象很少、逻辑简单目标有接口目标无接口

用一张图把这三者的关系和演进串起来看最清楚:

在这里插入图片描述

现在回到最实际的问题:Spring AOP 用的是哪种? 答案是——两种都用,按情况自动选择。Spring AOP 的默认策略是:

  • 如果目标类实现了接口,默认用 JDK 动态代理;
  • 如果目标类没有实现接口,就用 CGLIB(生成子类)。

这套"横切逻辑"在 Spring AOP 里有个专门的名字叫通知(Advice),而"在哪些方法上织入"由切点(Pointcut) 决定。你平时用的 @Transactional(声明式事务)、@Cacheable(声明式缓存)、@Async(异步)、以及各种自定义注解拦截,底层全都是动态代理在干活:Spring 在启动时为你的 Bean 生成一个代理,把事务的开启/提交、缓存的读写、日志记录等逻辑,以代理的方式织入到方法调用的前后。

一个极其常见的坑:正因为 Spring 用的是代理,所以 @Transactional@Cacheable 这类注解有个著名的"自调用失效"问题——在同一个类里,方法 A 直接调用本类的方法 B(this.B()),这个调用不经过代理对象,而是直接走了原始对象的 this,于是 B 上的事务/缓存注解不生效。理解了"代理是一个套在外面的替身对象、只有经过它的调用才会被增强",这个坑就一目了然了。解决办法是让调用重新经过代理(比如注入自己、或拆到另一个 Bean)。这个坑几乎每个 Spring 开发者都踩过,而它的根源就是这一篇讲的代理机制。

七、代理 vs 装饰器,以及现实身影

代理在整个技术栈里无处不在,认出它们:

  • Spring AOP:如上,@Transactional 等全靠它,是代理最重要的工业级应用。
  • MyBatis 的 Mapper:你只写了一个 OrderMapper 接口,从没写实现类,却能直接调用——因为 MyBatis 用 JDK 动态代理为接口生成了代理,在 invoke 里把方法调用翻译成 SQL 执行。
  • RPC 框架(Dubbo、Feign):你调用一个"远程服务接口",本地其实拿到的是个代理,它在 invoke 里把调用序列化、发网络请求、再把结果返回,让远程调用看起来像本地调用。
  • java.lang.reflect.Proxy:JDK 动态代理的官方入口,前面用过。

按代理的用途,还能细分出几个经典变体(它们结构相同,目的不同):远程代理(RPC,代理远端对象)、虚拟代理(延迟加载,访问时才真正创建重对象,如懒加载图片)、保护代理(权限控制,校验通过才转发)、缓存代理(缓存结果)。它们全是同一套代理结构的不同应用。

最后,划清一条极易混淆的界限——代理 vs 装饰器(下一篇的主角)。这两个模式的代码结构几乎一模一样:都实现同一接口、都持有一个被包装对象、都在转发前后加逻辑。区别不在结构,而在意图:

  • 代理:目的是控制访问。代理通常自己决定要不要、以及如何访问真实对象(比如权限不够就直接不转发),它和真实对象是"替身"关系,代理"替你管着"真实对象。你甚至可能不知道真实对象的存在。
  • 装饰器:目的是增强功能。装饰器是由外部把被包装对象传进来,层层叠加新能力,你很清楚自己在一层层地包装。它和被包装对象是"增强"关系。

一句话记:代理重在"控制"(管你能不能访问、怎么访问),装饰器重在"增强"(给你叠加新功能)。 结构相似,意图相反,这也是为什么它们被分成两个模式。下一篇讲装饰器时,我们会从另一头把这条界限再夯实一遍。


小结。代理模式给对象找一个"替身",让外界通过替身访问真实对象,从而在不改动真实对象的前提下,把日志、权限、缓存、事务这些横切逻辑织入进来。它有三种形态:静态代理手写清晰但会类爆炸;JDK 动态代理在运行时基于接口凭空生成代理,一套 InvocationHandler 通吃所有接口,解决了类爆炸,但只能代理有接口的类;CGLIB 改用继承生成子类,补上了"无接口"的场景,代价是不能代理 final。Spring AOP 正是"有接口用 JDK、无接口用 CGLIB"地把这两者用到了极致,而 @Transactional 的自调用失效坑,根源就在代理机制。记住代理和装饰器"结构相同、意图相反"——代理管控制,装饰器管增强。下一篇我们就正式讲装饰器模式,看它如何像洋葱一样层层包裹,给对象动态叠加职责。

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐