结构型模式(上)——代理、适配器与装饰器

摘要

结构型设计模式(Structural Patterns)关注的是如何将类和对象组合成更大的结构,同时保持这些结构的灵活性和可扩展性。本文深入剖析三个使用最广泛的结构型模式:代理模式(Proxy)——为目标对象提供一个替代者或占位符,控制对目标对象的访问,这是 Spring AOP 的底层基础;将从静态代理出发,精确剖析 JDK 动态代理的 InvocationHandler 机制与字节码生成原理,以及 CGLIB 基于继承的字节码增强方案,并对比两者的适用边界。适配器模式(Adapter)——将一个类的接口转换成客户期望的另一个接口,使原本不兼容的类可以协同工作,是处理遗留系统集成和第三方 API 适配的标准解法;剖析类适配器(继承)和对象适配器(组合)的选择依据。装饰器模式(Decorator)——在不改变原有对象的基础上,通过组合的方式动态地为对象添加新功能;与代理模式高度相似却又有本质区别:代理控制访问,装饰器扩展功能;分析 Java I/O 流体系中装饰器的完整设计,以及为什么装饰器比继承更灵活。


第 1 章 代理模式(Proxy)

1.1 动机:为什么需要代理

在软件开发中,有一类需求极其普遍:你有一个核心业务对象,想在它的方法调用前后”加点东西”——记录日志、检查权限、统计耗时、管理事务——但这些”附加功能”与核心业务逻辑无关,把它们混写在业务类里会违反 SRP,也使代码难以测试。

代理模式(Proxy Pattern) 解决的正是这个问题:为目标对象提供一个代理对象,所有对目标对象的调用都经过代理,代理在转发请求前后可以插入任意附加逻辑

调用方面对的是代理,以为在直接调用真实对象,实际上代理悄悄地在中间做了许多事情。这是一种典型的透明拦截

1.2 静态代理:直观但不可扩展

理解动态代理之前,先看静态代理——最直觉的实现,但它的缺陷是理解动态代理必要性的前提。

// 业务接口
public interface UserService {
    User findById(Long id);
    void save(User user);
}
 
// 真实业务实现
public class UserServiceImpl implements UserService {
    @Override
    public User findById(Long id) {
        // 数据库查询
        return db.query(id);
    }
    
    @Override
    public void save(User user) {
        db.insert(user);
    }
}
 
// 静态代理:手工编写,为每个方法加日志
public class UserServiceLoggingProxy implements UserService {
    private final UserService target;   // 被代理的真实对象
    
    public UserServiceLoggingProxy(UserService target) {
        this.target = target;
    }
    
    @Override
    public User findById(Long id) {
        long start = System.currentTimeMillis();
        log.info("Calling findById({})", id);
        try {
            User result = target.findById(id);  // 委托给真实对象
            log.info("findById({}) completed in {}ms", id, System.currentTimeMillis() - start);
            return result;
        } catch (Exception e) {
            log.error("findById({}) failed", id, e);
            throw e;
        }
    }
    
    @Override
    public void save(User user) {
        long start = System.currentTimeMillis();
        log.info("Calling save({})", user.getId());
        try {
            target.save(user);
            log.info("save({}) completed in {}ms", user.getId(), System.currentTimeMillis() - start);
        } catch (Exception e) {
            log.error("save({}) failed", user.getId(), e);
            throw e;
        }
    }
}

静态代理的问题一目了然:

  • 代码冗余:每个方法都要写相同的”记录开始时间、调用目标、记录耗时、捕获异常”模板代码;
  • 维护困难:如果接口新增了第 10 个方法,代理类必须同步新增;如果日志格式需要统一修改,必须逐一修改每个方法的代理代码;
  • 类爆炸:如果有 50 个 Service 接口都需要日志代理,需要写 50 个代理类。

不这样做会怎样? 静态代理在小型项目或只有 2-3 个方法的接口中是可以接受的,但在大型系统中(数十个 Service、每个 Service 有数十个方法),这种方式会带来难以维护的代码量,且每次”横切关注点”(Logging、Transaction、Security)的需求变化都需要大规模修改。

1.3 JDK 动态代理:运行时生成代理类

JDK 动态代理通过 java.lang.reflect.Proxy 类和 java.lang.reflect.InvocationHandler 接口,在运行时动态生成代理类的字节码,而不需要手工编写代理类:

// InvocationHandler:所有方法调用都会路由到这里
public class LoggingInvocationHandler implements InvocationHandler {
    private final Object target;   // 被代理的真实对象
    
    public LoggingInvocationHandler(Object target) {
        this.target = target;
    }
    
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // method 是被调用的方法,args 是参数列表
        long start = System.currentTimeMillis();
        log.info("Calling {}.{}({})", target.getClass().getSimpleName(), method.getName(), args);
        
        try {
            // 通过反射调用真实对象的方法
            Object result = method.invoke(target, args);
            log.info("{} completed in {}ms", method.getName(), System.currentTimeMillis() - start);
            return result;
        } catch (InvocationTargetException e) {
            // InvocationTargetException 包装了目标方法抛出的异常
            log.error("{} failed", method.getName(), e.getCause());
            throw e.getCause();  // 解包,重新抛出原始异常
        }
    }
}
 
// 使用:一行代码创建任意接口的日志代理
UserService userService = (UserService) Proxy.newProxyInstance(
    UserServiceImpl.class.getClassLoader(),   // 类加载器
    new Class[]{UserService.class},           // 代理需要实现的接口列表
    new LoggingInvocationHandler(new UserServiceImpl())  // Handler
);
 
// 调用代理的方法,会路由到 LoggingInvocationHandler.invoke()
User user = userService.findById(123L);

JDK 动态代理的底层原理

Proxy.newProxyInstance() 在运行时动态生成一个实现了指定接口(如 UserService)的代理类(类名通常是 $Proxy0$Proxy1 等),并将其实例化。这个动态生成的类的每个方法实现都是:找到对应的 Method 对象,然后调用 InvocationHandler.invoke(this, method, args)

通过 JVM 参数 -Dsun.misc.ProxyGenerator.saveGeneratedFiles=true 可以把动态生成的类保存到磁盘,用反编译工具查看,大致结构如下:

// JDK 动态生成的代理类(反编译后的大致结构)
public final class $Proxy0 extends Proxy implements UserService {
    // 静态字段:缓存方法的 Method 对象(避免每次反射查找)
    private static Method m1;  // findById
    private static Method m2;  // save
    // ...
    
    static {
        m1 = UserService.class.getMethod("findById", Long.class);
        m2 = UserService.class.getMethod("save", User.class);
    }
    
    // 每个接口方法都委托给 InvocationHandler
    @Override
    public User findById(Long id) {
        return (User) h.invoke(this, m1, new Object[]{id});  // h 是 InvocationHandler
    }
    
    @Override
    public void save(User user) {
        h.invoke(this, m2, new Object[]{user});
    }
}

JDK 动态代理的限制

最重要的限制是:JDK 动态代理只能代理接口,不能代理没有实现接口的普通类。原因是生成的代理类需要继承 java.lang.reflect.Proxy,而 Java 是单继承的,代理类无法再继承目标类。

1.4 CGLIB:基于继承的字节码增强

当目标类没有实现接口时,需要使用 CGLIB(Code Generation Library)。CGLIB 通过生成目标类的子类来创建代理,子类覆写了父类的所有非 final 方法,并在调用前后插入拦截逻辑:

// CGLIB 代理
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(UserServiceImpl.class);  // 目标类作为父类
enhancer.setCallback(new MethodInterceptor() {
    @Override
    public Object intercept(Object obj, Method method, Object[] args, 
                            MethodProxy proxy) throws Throwable {
        long start = System.currentTimeMillis();
        log.info("Calling {}", method.getName());
        try {
            // proxy.invokeSuper() 调用父类(真实对象)的方法
            // 注意:不能用 method.invoke(obj, args),会导致无限递归
            Object result = proxy.invokeSuper(obj, args);
            log.info("{} completed in {}ms", method.getName(), 
                     System.currentTimeMillis() - start);
            return result;
        } catch (Exception e) {
            log.error("{} failed", method.getName(), e);
            throw e;
        }
    }
});
 
UserServiceImpl proxy = (UserServiceImpl) enhancer.create();

CGLIB 的限制

  • 不能代理 final 类:final 类无法被继承,CGLIB 无法生成子类;
  • 不能代理 final 方法:final 方法无法被覆写,CGLIB 代理无法拦截 final 方法的调用;
  • 需要无参构造器(默认情况):CGLIB 生成的子类需要调用父类构造器。

1.5 JDK 代理 vs CGLIB 对比

维度JDK 动态代理CGLIB
代理机制实现接口继承目标类
是否需要接口必须有接口不需要接口
final 类/方法无限制(只代理接口方法)不能代理
性能(Java 8+)与 CGLIB 接近与 JDK 代理接近
Spring 默认选择有接口时优先 JDK无接口或 proxyTargetClass=true

Spring AOP 的选择策略

在 Spring 5.x 之前,有接口的 Bean 默认使用 JDK 代理,没有接口的 Bean 使用 CGLIB。Spring Boot 2.x(对应 Spring 5.x)将默认策略改为始终使用 CGLIBspring.aop.proxy-target-class=true),原因是 CGLIB 代理不要求目标类必须实现接口,更通用,且现代 JVM 上性能差距可以忽略。

1.6 代理模式的常见用途

用途一:权限控制(访问控制代理)

public class SecuredUserServiceProxy implements UserService {
    private final UserService target;
    private final AuthService authService;
    
    @Override
    public void save(User user) {
        // 在真正执行业务前检查权限
        if (!authService.hasPermission(SecurityContext.currentUser(), "USER_WRITE")) {
            throw new AccessDeniedException("No permission to save user");
        }
        target.save(user);
    }
    // ...
}

用途二:缓存代理

public class CachingUserServiceProxy implements UserService {
    private final UserService target;
    private final Cache<Long, User> cache;
    
    @Override
    public User findById(Long id) {
        return cache.get(id, () -> target.findById(id));
    }
}

用途三:延迟加载(虚拟代理)

Hibernate/JPA 的懒加载就是典型的虚拟代理:关联对象不在查询时加载,而是在第一次访问时才触发数据库查询。对象的”持有者”实际上持有的是一个代理,只有调用代理上的方法时才真正加载数据。


第 2 章 适配器模式(Adapter)

2.1 动机:接口不兼容时的协同问题

适配器模式(Adapter Pattern)解决的问题是:两个接口不兼容的类需要协同工作。就像电源适配器把 220V 的插座转换成笔记本电脑需要的 19V 电压,代码中的适配器把一个类的接口”翻译”成另一个类期望的接口。

这类问题在以下场景中最常见:

场景一:集成第三方库

你的系统定义了 PaymentGateway 接口,内部代码都按这个接口编程。现在要集成一个第三方支付 SDK(如 Alipay SDK),但 SDK 有自己的接口定义(AlipayClient),与 PaymentGateway 不兼容。你不能修改 SDK(第三方代码),也不想修改自己系统的 PaymentGateway 接口(已有大量依赖)。适配器是唯一的优雅解法。

场景二:集成遗留系统

老系统有一个 LegacyUserSystem,它的方法签名与新系统的 UserRepository 接口不一致。重写老系统代价太高,直接修改新系统接口影响太广,适配器在两者之间架起桥梁。

场景三:复用已有类

有一个功能完善的工具类 XmlParser,但它的接口与当前系统需要的 DocumentParser 接口不一致,无法直接替换,用适配器包装后即可使用。

2.2 对象适配器:组合实现

对象适配器使用组合方式——适配器持有被适配者的实例,在自己的方法实现中调用被适配者的对应方法:

// 目标接口:系统内部使用的支付接口
public interface PaymentGateway {
    PaymentResult charge(String userId, BigDecimal amount, String currency);
    RefundResult refund(String transactionId, BigDecimal amount);
}
 
// 被适配者:第三方 Stripe SDK(接口不受我们控制)
public class StripeClient {
    public StripeCharge createCharge(String customerId, long amountInCents, 
                                     String currency) { ... }
    public StripeRefund createRefund(String chargeId, long amountInCents) { ... }
}
 
// 对象适配器:将 StripeClient 适配为 PaymentGateway
public class StripePaymentAdapter implements PaymentGateway {
    private final StripeClient stripeClient;  // 持有被适配者的引用
    
    public StripePaymentAdapter(StripeClient stripeClient) {
        this.stripeClient = stripeClient;
    }
    
    @Override
    public PaymentResult charge(String userId, BigDecimal amount, String currency) {
        // 接口转换:BigDecimal 元 → long 分,userId → customerId
        long amountInCents = amount.multiply(BigDecimal.valueOf(100)).longValue();
        StripeCharge charge = stripeClient.createCharge(userId, amountInCents, currency);
        
        // 结果转换:StripeCharge → PaymentResult
        return PaymentResult.builder()
            .transactionId(charge.getId())
            .status(mapStatus(charge.getStatus()))
            .amount(amount)
            .build();
    }
    
    @Override
    public RefundResult refund(String transactionId, BigDecimal amount) {
        long amountInCents = amount.multiply(BigDecimal.valueOf(100)).longValue();
        StripeRefund refund = stripeClient.createRefund(transactionId, amountInCents);
        return RefundResult.success(refund.getId());
    }
    
    private PaymentStatus mapStatus(String stripeStatus) {
        return switch (stripeStatus) {
            case "succeeded" -> PaymentStatus.SUCCESS;
            case "pending"   -> PaymentStatus.PENDING;
            case "failed"    -> PaymentStatus.FAILED;
            default          -> PaymentStatus.UNKNOWN;
        };
    }
}
 
// 使用:系统代码只依赖 PaymentGateway 接口
PaymentGateway payment = new StripePaymentAdapter(new StripeClient(apiKey));
PaymentResult result = payment.charge("user_123", new BigDecimal("99.99"), "CNY");

以后如果要从 Stripe 换成 Alipay,只需要新写一个 AlipayPaymentAdapter,系统其他代码一行不改。

2.3 类适配器:继承实现

类适配器通过多重继承(在 Java 中通过继承被适配者 + 实现目标接口)实现:

// 类适配器:继承 StripeClient,实现 PaymentGateway
public class StripePaymentClassAdapter extends StripeClient implements PaymentGateway {
    
    @Override
    public PaymentResult charge(String userId, BigDecimal amount, String currency) {
        // 直接调用继承的父类方法(无需持有引用)
        long amountInCents = amount.multiply(BigDecimal.valueOf(100)).longValue();
        StripeCharge charge = createCharge(userId, amountInCents, currency);  // 直接调用
        return buildPaymentResult(charge);
    }
}

对象适配器 vs 类适配器

维度对象适配器(组合)类适配器(继承)
Java 实现实现目标接口 + 持有被适配者引用继承被适配者 + 实现目标接口
灵活性高(可以适配被适配者的子类)低(只能适配特定的被适配者类)
耦合度低(组合耦合)高(继承耦合)
是否可覆写行为需要委托,不能直接覆写可以直接覆写父类方法
推荐程度首选(符合”组合优于继承”原则)仅特殊场景

在 Java 中,优先选择对象适配器。原因是:

  1. Java 是单继承,类适配器的继承会占用唯一的父类位置;
  2. 组合比继承更灵活,运行时可以换一个不同的被适配者实例;
  3. 继承暴露了父类的所有 public 方法,调用方可能误用被适配者自身的方法(破坏封装)。

2.4 适配器模式在 JDK 中的体现

Arrays.asList():将数组 T[] 适配为 List<T> 接口,使得数组可以用在接受 List 的地方。内部实现是一个 Arrays.ArrayList 内部类,它实现了 List 接口,委托到底层数组操作。

InputStreamReader:将字节流 InputStream(字节接口)适配为字符流 Reader(字符接口)。InputStreamReader 实现了 Reader,内部持有 InputStream 引用并通过字符集解码进行转换。这实际上同时体现了适配器模式和装饰器模式。

Collections.list(Enumeration):将老旧的 Enumeration(JDK 1.0 的遍历接口)适配为 ArrayList,使其可以用新的集合 API 处理。


第 3 章 装饰器模式(Decorator)

3.1 动机:运行时动态扩展行为

装饰器模式(Decorator Pattern)解决的问题是:在不改变原有类的基础上,动态地为对象添加新功能

为什么不用继承来扩展功能?来看一个真实的困境:

Coffee(咖啡基类)
├── Espresso(意式浓缩)
├── Americano(美式咖啡)
└── Latte(拿铁)

附加选项(可以自由组合):
- 加糖
- 加牛奶
- 加奶泡
- 加香草糖浆
- 加巧克力酱

如果用继承来表达所有可能的组合:

  • EspressoWithSugar
  • EspressoWithMilk
  • EspressoWithSugarAndMilk
  • EspressoWithSugarAndMilkAndVanilla
  • AmericanoWithSugar

3 种基础咖啡 × 5 种附加选项的组合数是指数级的——这是”类爆炸”问题,继承无法优雅地表达任意组合。

装饰器模式的解法:将附加功能(加糖、加牛奶)封装在”装饰器”类中,装饰器实现与被装饰对象相同的接口,并在内部持有一个被装饰对象的引用。装饰器在调用被装饰对象的方法前后添加自己的逻辑,且装饰器可以层层嵌套:

// 咖啡接口
public interface Coffee {
    String getDescription();
    double getCost();
}
 
// 基础实现:意式浓缩
public class Espresso implements Coffee {
    @Override
    public String getDescription() { return "Espresso"; }
    @Override
    public double getCost() { return 15.0; }
}
 
// 抽象装饰器基类:实现相同接口,持有被装饰对象
public abstract class CoffeeDecorator implements Coffee {
    protected final Coffee decoratedCoffee;  // 被装饰的咖啡
    
    public CoffeeDecorator(Coffee coffee) {
        this.decoratedCoffee = coffee;
    }
    
    @Override
    public String getDescription() {
        return decoratedCoffee.getDescription();  // 默认委托
    }
    
    @Override
    public double getCost() {
        return decoratedCoffee.getCost();  // 默认委托
    }
}
 
// 具体装饰器一:加糖
public class SugarDecorator extends CoffeeDecorator {
    public SugarDecorator(Coffee coffee) { super(coffee); }
    
    @Override
    public String getDescription() {
        return decoratedCoffee.getDescription() + " + 糖";
    }
    
    @Override
    public double getCost() {
        return decoratedCoffee.getCost() + 2.0;  // 加糖额外 2 元
    }
}
 
// 具体装饰器二:加牛奶
public class MilkDecorator extends CoffeeDecorator {
    public MilkDecorator(Coffee coffee) { super(coffee); }
    
    @Override
    public String getDescription() {
        return decoratedCoffee.getDescription() + " + 牛奶";
    }
    
    @Override
    public double getCost() {
        return decoratedCoffee.getCost() + 5.0;
    }
}
 
// 具体装饰器三:加香草糖浆
public class VanillaDecorator extends CoffeeDecorator {
    public VanillaDecorator(Coffee coffee) { super(coffee); }
    
    @Override
    public String getDescription() {
        return decoratedCoffee.getDescription() + " + 香草糖浆";
    }
    
    @Override
    public double getCost() {
        return decoratedCoffee.getCost() + 8.0;
    }
}
 
// 使用:自由组合,层层包装
Coffee order = new VanillaDecorator(
                   new MilkDecorator(
                       new SugarDecorator(
                           new Espresso())));
 
System.out.println(order.getDescription());  // "Espresso + 糖 + 牛奶 + 香草糖浆"
System.out.println(order.getCost());         // 15 + 2 + 5 + 8 = 30.0

3.2 Java I/O 流:装饰器模式的教科书案例

Java I/O 流体系是装饰器模式在标准库中最典型的应用,也是很多人初学时觉得”怎么要 new 这么多层”的原因:

// 层层包装:每一层都是一个装饰器
InputStream fileStream = new FileInputStream("data.txt");    // 基础:文件流
InputStream bufferedStream = new BufferedInputStream(fileStream);  // 装饰:缓冲
InputStream gzipStream = new GZIPInputStream(bufferedStream);     // 装饰:解压缩
DataInputStream dataStream = new DataInputStream(gzipStream);     // 装饰:读取基本类型

这段代码的每一层都是一个装饰器:

  • FileInputStream:基础字节流,从文件读取原始字节;
  • BufferedInputStream:缓冲装饰器,减少系统调用次数(将小块读取合并为大块);
  • GZIPInputStream:解压缩装饰器,对上层透明地进行 gzip 解压;
  • DataInputStream:类型转换装饰器,提供 readInt()readLong() 等方法读取基本类型。

各装饰器都实现了 InputStream 接口(或其子类),持有另一个 InputStream 作为被装饰对象,在 read() 方法中添加自己的逻辑后委托给内层。

这种设计的优势:

  • 组合自由:可以按需组合任意装饰器,不需要为每种组合创建子类;
  • 单一职责:每个装饰器只做一件事(缓冲/解压/类型转换);
  • 对称性:输出流 OutputStream 有对应的装饰器体系(BufferedOutputStreamGZIPOutputStreamDataOutputStream),与输入流体系完全对称。

3.3 装饰器模式 vs 代理模式:相似但本质不同

装饰器和代理在结构上极其相似(都持有同接口的对象引用,都委托调用),但意图不同

维度代理模式装饰器模式
意图控制对目标对象的访问为对象动态添加功能
关系方向代理通常由系统(框架)创建,客户端不选择调用方主动选择装饰哪些功能
对目标对象的知识代理可以完全替代目标(甚至不需要真实对象,如虚拟代理)装饰器需要持有真实对象,才能委托调用
叠加代理通常不多层叠加装饰器设计上就是为了层层叠加
典型用途日志、权限、事务、缓存(横切关注点)动态扩展对象能力(如 I/O 流功能组合)

设计哲学

代理模式的核心词是”控制”——控制对真实对象的访问,在访问前后插入”关卡”;装饰器模式的核心词是”增强”——不改变对象的核心行为,在外层添加更多能力。代理通常在调用方不知情的情况下由框架(如 Spring AOP)透明织入;装饰器通常由调用方主动选择(new BufferedInputStream(new FileInputStream(...)))。

3.4 装饰器模式的边界与反例

装饰器的适用条件:被装饰的功能是可选的可组合的,且不同功能的组合数量很多。如果功能总是全部需要,或者不同功能之间有复杂的依赖关系,装饰器可能并不是最佳选择。

反例:装饰器链太深导致调试困难

如果将 7、8 层装饰器叠加在一起,当发生异常时,调用栈非常深,难以定位问题出在哪一层。在这种情况下,可以考虑引入”管道”(Pipeline)模式或责任链模式,提供更清晰的结构。


总结

本篇深入解析了三个最常用的结构型模式:

  • 代理模式通过为目标对象提供代理,实现透明拦截——日志、权限、事务等横切关注点可以从业务代码中分离。静态代理方式简单但不可扩展;JDK 动态代理在运行时生成实现接口的代理类,只适用于有接口的目标;CGLIB 通过子类化适用于无接口的普通类。Spring AOP 基于这两种动态代理实现,@Transactional@Cacheable 等注解背后都是代理在工作;

  • 适配器模式通过包装不兼容的接口,使两个本来无法协同工作的类能够在一起运行。对象适配器(组合)是首选,类适配器(继承)仅在特殊场景使用。适配器是集成第三方 SDK、遗留系统的标准解法,JDK 中的 Arrays.asList()InputStreamReader 都是典型例子;

  • 装饰器模式通过层层包装为对象动态添加功能,相比继承避免了”类爆炸”,相比直接修改类保持了 OCP。Java I/O 流体系(BufferedInputStreamGZIPInputStream 等)是装饰器模式的经典示范。装饰器与代理结构相似但意图不同:代理控制访问,装饰器增强功能,前者通常由框架透明织入,后者通常由调用方主动选择。

下一篇继续结构型模式中的外观模式(子系统简化入口)、桥接模式(抽象与实现的独立演进)、组合模式(树形结构统一处理)和享元模式(大量细粒度对象的共享):05 结构型模式(下)——外观、桥接、组合与享元


参考资料

  • GoF,《Design Patterns: Elements of Reusable Object-Oriented Software》, 1994
  • Spring Framework 文档:AOP 代理机制
  • Brian Goetz,《Java Concurrency in Practice》, Chapter 4(关于代理与不可变性)
  • JDK 源码:java.lang.reflect.Proxyjava.io.FilterInputStream

思考题

  1. JDK 动态代理要求目标类实现接口,CGLIB 通过继承目标类实现代理。Spring AOP 默认对接口使用 JDK 代理、对类使用 CGLIB 代理。Spring Boot 2.0+ 默认全部使用 CGLIB——这个改变的原因是什么?CGLIB 代理在什么场景下会失败(如 final 类/方法)?
  2. 装饰器模式和代理模式在结构上几乎相同(都持有目标对象的引用并委托调用),但意图不同——装饰器增强功能,代理控制访问。Java IO 的 BufferedInputStream(new FileInputStream(...)) 是典型的装饰器。在实际代码中,你如何区分一个类是装饰器还是代理?是否有明确的设计准则?
  3. Spring AOP 的代理对象在调用 this.method() 时不会触发 AOP 增强——因为 this 引用的是原始对象而非代理对象。这个’自调用’问题是 Spring AOP 最常见的陷阱之一。除了 AopContext.currentProxy()@EnableAspectJAutoProxy(exposeProxy=true) 之外,还有哪些解决方案?AspectJ 编译时织入(CTW)为什么不存在这个问题?