结构型模式(上)——代理、适配器与装饰器
摘要
结构型设计模式(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)将默认策略改为始终使用 CGLIB(spring.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 中,优先选择对象适配器。原因是:
- Java 是单继承,类适配器的继承会占用唯一的父类位置;
- 组合比继承更灵活,运行时可以换一个不同的被适配者实例;
- 继承暴露了父类的所有
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(拿铁)
附加选项(可以自由组合):
- 加糖
- 加牛奶
- 加奶泡
- 加香草糖浆
- 加巧克力酱
如果用继承来表达所有可能的组合:
EspressoWithSugarEspressoWithMilkEspressoWithSugarAndMilkEspressoWithSugarAndMilkAndVanillaAmericanoWithSugar- …
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.03.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有对应的装饰器体系(BufferedOutputStream、GZIPOutputStream、DataOutputStream),与输入流体系完全对称。
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 流体系(
BufferedInputStream、GZIPInputStream等)是装饰器模式的经典示范。装饰器与代理结构相似但意图不同:代理控制访问,装饰器增强功能,前者通常由框架透明织入,后者通常由调用方主动选择。
下一篇继续结构型模式中的外观模式(子系统简化入口)、桥接模式(抽象与实现的独立演进)、组合模式(树形结构统一处理)和享元模式(大量细粒度对象的共享):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.Proxy、java.io.FilterInputStream
思考题
- JDK 动态代理要求目标类实现接口,CGLIB 通过继承目标类实现代理。Spring AOP 默认对接口使用 JDK 代理、对类使用 CGLIB 代理。Spring Boot 2.0+ 默认全部使用 CGLIB——这个改变的原因是什么?CGLIB 代理在什么场景下会失败(如
final类/方法)?- 装饰器模式和代理模式在结构上几乎相同(都持有目标对象的引用并委托调用),但意图不同——装饰器增强功能,代理控制访问。Java IO 的
BufferedInputStream(new FileInputStream(...))是典型的装饰器。在实际代码中,你如何区分一个类是装饰器还是代理?是否有明确的设计准则?- Spring AOP 的代理对象在调用
this.method()时不会触发 AOP 增强——因为this引用的是原始对象而非代理对象。这个’自调用’问题是 Spring AOP 最常见的陷阱之一。除了AopContext.currentProxy()和@EnableAspectJAutoProxy(exposeProxy=true)之外,还有哪些解决方案?AspectJ 编译时织入(CTW)为什么不存在这个问题?