主题
代理与装饰器:一层包装的两种意图
本文是设计模式系统学习系列的 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)