Skip to content

代理与装饰器:一层包装的两种意图

本文是设计模式系统学习系列的 L2 核心篇。前置:1. SOLID 与设计原则:从坏味道说起。 学完可以配合面试题食用:Spring AOP:JDK 代理 vs CGLIB

为什么要区分代理和装饰器

代理和装饰器的代码结构几乎一样:都包一层,都在调用目标前后加逻辑。两者的区别是常被讨论的话题。

但意图不同。代理要控制对目标的访问——延迟加载、权限校验、远程调用转发。装饰器要给目标加功能——压缩、加密、缓存、计数。一个能不用就不用,一个巴不得套三层。

写好代理的前提是理解 Java 动态代理的三条路:静态代理、JDK 动态代理、CGLIB。每一条的取舍在 Spring Boot 2.x 切换默认 CGLIB 的决策里能看得很清楚。

静态代理:最直白,也最僵

代理类持有真实对象的引用,实现相同接口,每个方法调用真实对象前加逻辑。

java
interface UserService {
    User findById(Long id);
}

class UserServiceImpl implements UserService {
    public User findById(Long id) {
        // 查数据库
        return new User(id);
    }
}

class UserServiceProxy implements UserService {
    private UserService target;

    public UserServiceProxy(UserService target) {
        this.target = target;
    }

    public User findById(Long id) {
        System.out.println("[LOG] query user: " + id);
        long start = System.nanoTime();
        User result = target.findById(id);
        System.out.println("[LOG] cost: " + (System.nanoTime() - start) / 1_000_000 + "ms");
        return result;
    }
}

问题:每多一个接口就多一个代理类。10 个 Service 接口就 10 个代理类,日志/权限/事务逻辑重复一遍。所以静态代理只在接口极少、变更为零的场景能用。

JDK 动态代理:接口驱动

java
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

class LogHandler implements InvocationHandler {
    private Object target;
    public LogHandler(Object target) { this.target = target; }

    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("[LOG] " + method.getName());
        long start = System.nanoTime();
        Object result = method.invoke(target, args);
        System.out.println("[LOG] cost: " + (System.nanoTime() - start) / 1_000_000 + "ms");
        return result;
    }
}

// 使用
UserService proxy = (UserService) Proxy.newProxyInstance(
    UserService.class.getClassLoader(),
    new Class[]{UserService.class},
    new LogHandler(new UserServiceImpl())
);

Proxy.newProxyInstance 在运行时生成字节码创建代理类,拿到 $Proxy0 这种类名。InvocationHandler.invoke 接管所有方法调用。

制约:目标必须实现接口。没有接口的类(比如第三方库的类)没法用 JDK 动态代理。Spring 早期版本因此要求 Bean 实现接口才能被 AOP 代理。

CGLIB:子类代理

CGLIB 对目标类生成子类,覆盖父类方法。不需要接口。

java
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

class LogInterceptor implements MethodInterceptor {
    private Object target;
    public LogInterceptor(Object target) { this.target = target; }

    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
        System.out.println("[LOG] " + method.getName());
        long start = System.nanoTime();
        Object result = proxy.invoke(target, args);
        System.out.println("[LOG] cost: " + (System.nanoTime() - start) / 1_000_000 + "ms");
        return result;
    }
}

// 使用
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(UserServiceImpl.class);
enhancer.setCallback(new LogInterceptor(new UserServiceImpl()));
UserServiceImpl proxy = (UserServiceImpl) enhancer.create();

CGLIB 的限制更少,但有两个注意点:不能代理 final 类和方法;生成子类时调用父类构造方法(无参构造是必备)。

Spring Boot 2.x 之后默认用 CGLIB。原因简单:多数 Service 类不实现接口,强行要求接口增加无意义代码。

代理 vs 装饰器:意图决定一切

结构一样,但装饰器的用法不同。

java
// 装饰器模式:叠加功能
InputStream is = new FileInputStream("data.txt");
InputStream bis = new BufferedInputStream(is);           // 加缓冲
InputStream zis = new GZIPInputStream(bis);               // 加解压

// 装饰器要暴露原始接口,让调用方继续用 InputStream
// 代理要隐藏目标,调用方只跟代理打交道

对比总结

维度代理装饰器
意图控制访问(权限/延迟/远程)添加功能(缓冲/加密/统计)
创建时机编译期或启动时确定运行时动态组合
调用方可见性调用方只看到代理调用方看到接口,装饰器透明
典型容器Spring AOP、RPC 框架Java IO 流、Collections.synchronizedList

学完这段,可以回到 Spring 去看 AOP 的实现:切面本质就是代理+拦截器链。切点表达式匹配出哪些方法需要代理,通知链作为 InvocationHandler 的 invoke 逻辑串联执行。

动手实操

完整的 JDK 动态代理 + CGLIB 对比 demo,以及手写 IO 装饰器链。

java
// ============== JDK 动态代理 ==============
interface Calculator {
    int add(int a, int b);
}

class CalculatorImpl implements Calculator {
    public int add(int a, int b) {
        return a + b;
    }
}

Calculator origin = new CalculatorImpl();
Calculator proxyJdk = (Calculator) Proxy.newProxyInstance(
    Calculator.class.getClassLoader(),
    new Class[]{Calculator.class},
    (proxy, method, args) -> {
        System.out.println("[BEFORE] " + method.getName());
        Object result = method.invoke(origin, args);
        System.out.println("[AFTER] result=" + result);
        return result;
    }
);
System.out.println(proxyJdk.add(3, 4)); // 输出 BEFORE / AFTER / 7

// ============== CGLIB(无接口类)==============
class PlainCalc {
    public int add(int a, int b) { return a + b; }
}

Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(PlainCalc.class);
enhancer.setCallback((MethodInterceptor) (obj, method, args, proxy) -> {
    System.out.println("[CGLIB BEFORE] " + method.getName());
    Object result = proxy.invokeSuper(obj, args);
    System.out.println("[CGLIB AFTER] result=" + result);
    return result;
});
PlainCalc proxyCglib = (PlainCalc) enhancer.create();
System.out.println(proxyCglib.add(3, 4));

// ============== 手写 IO 装饰器链 ==============
abstract class FilterInputStream extends InputStream {
    protected InputStream in;
    public FilterInputStream(InputStream in) { this.in = in; }
    public int read() throws IOException { return in.read(); }
}

class LowerCaseInputStream extends FilterInputStream {
    public LowerCaseInputStream(InputStream in) { super(in); }
    public int read() throws IOException {
        int c = super.read();
        return (c == -1) ? c : Character.toLowerCase((char) c);
    }
}

// 使用
InputStream is = new LowerCaseInputStream(
    new BufferedInputStream(
        new FileInputStream("hello.txt")
    )
);
// 读取的内容全部小写

关键点:proxy.invokeSuper 不走代理对象的递归调用,直接调父类原始方法,避免死循环。

常见误区与小结

  • 误区一:JDK 动态代理比 CGLIB 快。CGLIB 2.x 确实慢,但 CGLIB 3.x 之后性能接近,且 Spring Boot 默认 CGLIB 说明满足绝大多数场景
  • 误区二:装饰器模式就是代理模式。结构相同但意图不同,理解意图差异才是关键
  • 误区三:CGLIB 能代理任何类。final 类和方法不行,没有无参构造也不行
  • 误区四:代理模式必须用反射。静态代理就不反射,动态代理才需要
  • 误区五:InvocationHandler.invoke 的 proxy 参数没用。它用来在 invoke 里递归调用目标方法,或者做方法选择判断

小结:代理和装饰器是"包一层"的两个分支。代理侧重控制,装饰器侧重增强。理解它们的关键不是背代码结构,而是看意图:

  • 只有接口 → JDK 动态代理
  • 没有接口或有 final 类 → CGLIB
  • 要叠加功能且透明 → 装饰器

下一篇 策略与模板方法:消灭 if-else 的两把刀 继续讲行为型模式,这两者的组合用法在 Spring 里随处可见。

参考

参考:《Effective Java》第 3 版 Item 18(组合优于继承);《Spring 实战》第 6 版 AOP 章节;CGLIB 源码解析(github.com/cglib/cglib

手撕 → 框架 → 生产化,一步步把 AI Agent 工程化搞透。
粤ICP备2026104257号-1