少女祈祷中...
残云的学习笔记

代理模式:从静态代理到动态代理,以及它和装饰器到底差在哪

代理(Proxy)是结构型里最有存在感的一个——你每天用的 @Transactional、@Cacheable、MyBatis 的 Mapper 接口、Feign 客户端,背后都是它。本文讲清代理的四种经典形态、三种 Java 落地方式(静态 / JDK 动态 / CGLIB),并解决一个高频困惑:同样都是"包一层",代理和装饰器到底差在哪。

一、问题的起点:为什么不能直接访问对象

你想调用一个对象,但直接调用不合适。常见理由有四类:

障碍直接调的后果
它在别的进程/机器上要写网络通信、序列化、重试——调用方被迫处理一堆与业务无关的事
它创建起来很贵页面一打开就把大图、大报表全加载了,卡顿且浪费
不是谁都该用敏感接口需要按角色鉴权,但业务代码里到处写 if (role == ADMIN) 会烂掉
访问时需要附带动作每调一次都要记日志、计时、写缓存、加引用计数

代理模式给出的答案很朴素:让代理对象和真实对象实现同一个接口,调用方只面对代理,由代理决定"要不要真的转给真实对象、转之前之后做什么"。

GoF 的意图原文是:为其他对象提供一种代理以控制对这个对象的访问。 注意动词是控制——不是"增强功能",也不是"换接口"。这个用词上的区别,正是它和装饰器、适配器的分水岭。

二、代理的四种经典形态

GoF 归纳了四种典型用法,它们的共同点是"代理挡住真实对象",区别在于挡的理由:

proxy-1-four-kinds

记住这张图的实用价值是:面试问"代理模式有什么用",你答的不是"包一层",而是这四类具体需求 + 各自在框架里的真实例子。

三、结构:三个角色与一次访问

代理的结构简单到有点"看不出来是个模式"——因为它和装饰器长得几乎一样:

proxy-2-structure

三个角色:

  • Subject(抽象主题):代理与真实对象共同的接口;
  • RealSubject(真实主题):真正干活的对象;
  • Proxy(代理):持有真实对象的引用,在转发前后做"控制"。

一次访问的时序是这样的——注意代理里那些"前置/后置"动作才是这个模式的全部意义:

proxy-3-sequence

下面是一个静态代理的完整例子(访问控制 + 耗时统计)。静态代理就是"手写一个实现同一接口的类",最直白,也最能看清代理的本质:

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(数字随生成顺序变)
    }
}

它一次解决了静态代理的所有痛点:新增接口不用再写代理类、新增方法不用改代理。代价是三条硬性约束:

  1. 只能代理接口。Proxy.newProxyInstance 的第二个参数就是接口数组;没有接口就没有代理(这正是 CGLIB 存在的理由,见下一节)。
  2. 每个调用都走反射(method.invoke),比直接调用多一层间接;现代 JVM 下通常可接受,但热路径要留意。
  3. 调试变难:栈里出现 $Proxy0,出问题时得先想清楚"我现在停在代理还是真实对象里"。

运行时这条链路值得记住:

proxy-4-jdk-chain

想把生成的代理类挖出来看,加启动参数:-Djdk.proxy.ProxyGenerator.saveGeneratedFiles=true,会在工作目录下 dump 出 $Proxy0.class,反编译即可看到它是怎么把你的接口方法全转给 invoke() 的。

五、动态代理(二):CGLIB / ByteBuddy——生成子类来代理

JDK 方案有一道硬门槛:必须有接口。可现实中大量类没有接口(或接口不完整),于是就有了第二条路线——运行时生成目标类的子类,覆写同名方法来插入拦截逻辑。这就是 CGLIB(Code Generation Library),它也是 Spring AOP 的默认实现。

proxy-5-cglib-chain

和 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
    }
}

三条限制全部来自"继承"这件事:

  1. final 类与 final 方法代理不了——子类无法覆写;
  2. private 方法代理不了——同理;Spring AOP 本身也只作用于可覆写的实例方法;
  3. 构造器不会被调用: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 stubSpring 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);
    }
}

三种修法,按推荐度排序:

  1. 拆到另一个 Bean:把 audit() 移到 AuditService 里,跨 Bean 调用必然经过代理(最干净,也最符合单一职责);
  2. 注入自己(@Autowired private OrderService self; 或用 @Lazy 打断循环依赖)后调 self.audit(...);
  3. AopContext.currentProxy()(需 @EnableAspectJAutoProxy(exposeProxy = true),侵入性最强)。

理解这个坑的关键就是本文的结构图:代理是调用方拿到的那层壳,而 this 是壳里的真身。任何"绕过壳"的调用,注解都会静默失效——不报错,只是不生效,所以格外难查。

其他框架里的代理:

  • MyBatis Mapper:你只写 interface UserMapper,运行时由 JDK 动态代理生成实现,invoke() 里根据方法名去找 SQL;
  • Feign / Dubbo:接口 = 远程服务的本地门面,代理负责序列化、网络调用、重试——远程代理的标准形态;
  • Hibernate / JPA 懒加载:集合与关联对象被替换成代理,首次访问时才查询——虚拟代理;
  • @Cacheable / @PreAuthorize / @Async:都是"智能引用"或"保护代理"的落地。

八、代理 vs 装饰器 vs 适配器

这是结构型模式里最经典的对比题,因为代理和装饰器的 UML 几乎一模一样:都实现同一接口、都持有目标对象引用。区别只在意图:

proxy-6-three-patterns

维度代理装饰器适配器
意图控制访问增强职责转换接口
对外接口与真实对象相同与真实对象相同不同(适配成调用方要的样子)
目标对象通常在编译期就确定(自己创建或注入)通常由调用方传入(可自由组合、层层嵌套)被适配者通常是他人提供的类
是否可叠加一般单层,链式代理较少可任意层叠(这也是它的卖点)一般不叠加
典型例子Spring AOP、MyBatis Mapper、RPC stubBufferedInputStream、Collections.unmodifiableListInputStreamReader、Arrays.asList

一句话记忆:代理管"能不能访问",装饰器管"多做什么",适配器管"接口不对付"。

顺带把最容易连带混淆的外观(Facade)也放进来:外观是"把多个子系统接口简化成一个入口",它不实现子系统接口,属于"重新组织接口",与上面三者都不同。

九、什么时候不要用代理

  • 只是为了记日志:如果只是零散的几行 log.info,直接写比引入代理层更好读;代理的价值在于横切、统一、可复用。
  • 性能敏感的热路径:动态代理的反射/分派开销在大循环里会累积。能静态代理就用静态代理,或者干脆把逻辑写进真实对象。
  • 调试成本 > 收益:栈里全是 $Proxy / $$EnhancerBySpringCGLIB 时,排查问题的时间会明显变长。团队如果不熟 AOP,慎用"隐式代理"。
  • 想靠代理解决"设计问题":代理掩盖不了糟糕的接口设计。如果一个类的方法太多、职责太杂,先重构它,别急着给它套代理。

十、参考资料

  • GoF《设计模式:可复用面向对象软件的基础》——Proxy 章(四种形态、与 Decorator 的对比)
  • java.lang.reflect.Proxy / InvocationHandler JDK 文档(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 在用)
分享到

评论