AOP三种实现路线对比
# 30.AOP三种实现路线对比
# 目录介绍
- 1. 案例引入
- 2. AOP 全景概念体系
- 3. 路线一:JDK 动态代理
- 4. 路线二:CGLIB 子类代理
- 5. 路线三:AspectJ 真·AOP
- 6. Spring AOP 内部选型源码
- 7. 三大路线全维度对比
- 8. 自调用问题与生产决策
- 9. 综合回扣与卷四收官
# 1. 案例引入
# 1.1 @Transactional 注解为什么会失效?
某次线上事故复盘——一个看似正确的 Spring 服务,事务完全没生效:
@Service
public class OrderService {
public void placeOrder(Order order) {
saveOrder(order); // ★ 调用本类方法
deductStock(order); // ★ 调用本类方法
}
@Transactional(rollbackFor = Exception.class)
public void saveOrder(Order order) {
orderMapper.insert(order);
if (order.getAmount().compareTo(BigDecimal.ZERO) <= 0) {
throw new IllegalArgumentException("invalid amount"); // 期望回滚
}
}
@Transactional(rollbackFor = Exception.class)
public void deductStock(Order order) {
stockMapper.deduct(order.getProductId(), order.getQuantity());
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
线上故障:金额非法时,订单已经入库且没有回滚——事务注解形同虚设。
疑惑:
@Transactional不是只要加上就生效吗?为什么变成"装饰品"?- 为什么把
saveOrder抽到另一个 Service 类里调用就立刻生效? - 同一个团队的另一个项目里用 AspectJ 注解,完全没有这个坑——为什么?
# 1.2 AspectJ 一行注解搞定全部接入点
同样的需求换 AspectJ 实现:
@Aspect
public class TransactionAspect {
@Around("@annotation(org.springframework.transaction.annotation.Transactional)")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
TransactionStatus tx = txManager.getTransaction(...);
try {
Object ret = pjp.proceed();
txManager.commit(tx);
return ret;
} catch (Throwable t) {
txManager.rollback(tx);
throw t;
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
配置编译期织入(aspectj-maven-plugin)后:
- 不管在哪个类调用、不管是不是 this 调用、不管是 private 还是 public——只要打了
@Transactional就生效 - 启动后没有代理对象——业务对象就是真身,方法调用是直接调用
疑惑:为什么 AspectJ 没有自调用失效问题?它和 Spring AOP 到底差在哪一层?
# 1.3 我们要回答什么
第 34 篇是卷四第 5 篇——卷四收官篇。承接 33 篇 Java Agent / 32 篇字节码框架 / 07 篇反射代理的钩子,把"动态修改运行时行为"的最后一块拼图——AOP 三大实现路线——讲透:
卷四五篇知识递进:
07 篇 → 反射 + JDK Proxy + CGLIB 基础
31 篇 → MethodHandle/VarHandle 现代继任者
32 篇 → ASM/Javassist/ByteBuddy 字节码框架
33 篇 → Java Agent + Instrumentation
34 篇 → AOP 路线大对比 ← 把上面四篇全部串起来
2
3
4
5
6
带着这个目标回答 5 个核心问题:
追问 ①:AOP 究竟是什么?三种实现的本质差别? → §2、§3、§4、§5
追问 ②:为什么 Spring 6 抛弃 CGLIB 改用 ByteBuddy? → §4.2
追问 ③:AspectJ CTW vs LTW 工程上怎么选? → §5.2、§5.3
追问 ④:Spring AOP 如何决定用 JDK 还是 CGLIB? → §6.1
追问 ⑤:@Transactional 自调用失效根因?破解方案? → §8
2
3
4
5
# 2. AOP 全景概念体系
# 2.1 横切关注点的本质
传统 OOP:
OrderService UserService StockService ProductService
│ │ │ │
┌────┴────┐ ┌────┴────┐ ┌────┴────┐ ┌────┴────┐
│ 日志 │ │ 日志 │ │ 日志 │ │ 日志 │ ←─┐
│ 事务 │ │ 事务 │ │ 事务 │ │ 事务 │ ←─┤ 横切关注点
│ 权限 │ │ 权限 │ │ 权限 │ │ 权限 │ ←─┤ (Cross-Cutting Concerns)
│ 监控 │ │ 监控 │ │ 监控 │ │ 监控 │ ←─┘
│ 业务A │ │ 业务B │ │ 业务C │ │ 业务D │
└─────────┘ └─────────┘ └─────────┘ └─────────┘
问题:每个类都重复写 4 类基础设施代码 → 代码 80% 是"装订线"
2
3
4
5
6
7
8
9
10
11
12
AOP 思路:
┌────────────────────────┐
│ 日志切面 │ ← 写一次
│ 事务切面 │ ← 写一次
│ 权限切面 │ ← 写一次
│ 监控切面 │ ← 写一次
└─────────┬──────────────┘
│
织入(Weaving)
│
┌──────────────────┼──────────────────┐
↓ ↓ ↓
OrderService UserService StockService
(只剩业务A) (只剩业务B) (只剩业务C)
2
3
4
5
6
7
8
9
10
11
12
13
14
AOP 解决的问题不是"代码复用"——而是"基础设施代码与业务代码的物理分离"。这是 Java EE 时代最重要的工程思想之一。
# 2.2 七大核心术语
| 术语 | 英文 | 解释 | 例子 |
|---|---|---|---|
| 切面 | Aspect | 横切逻辑的模块化 | TransactionAspect 类 |
| 连接点 | Join Point | 程序执行的某个点 | 某个方法调用、字段访问、异常抛出 |
| 切点 | Pointcut | 描述哪些 Join Point 被拦截 | execution(* com.x.service..*(..)) |
| 通知 | Advice | 在 Join Point 执行的代码 | @Before / @After / @Around |
| 目标对象 | Target | 被拦截的对象 | OrderService 实例 |
| 代理对象 | Proxy | 织入切面后的对象 | OrderService$$EnhancerBySpringCGLIB |
| 织入 | Weaving | 把 Advice 接入 Join Point 的过程 | 编译期/加载期/运行期完成 |
Pointcut 表达式速查:
execution(* com.x.service.*Service.*(..)) ← 所有 Service 的所有方法
@annotation(o.s.tx.annotation.Transactional)← 加了 @Transactional 的方法
within(com.x.service..*) ← 服务包下所有类
this(com.x.OrderService) ← 代理对象是 OrderService
target(com.x.OrderService) ← 目标对象是 OrderService
args(java.lang.String) ← 方法参数是 String
2
3
4
5
6
# 2.3 织入的三个时机
这是 AOP 路线选择最核心的维度——见过的 90% 同学都没真正理解。
源码 .java
│
│ ┌── 编译期织入(CTW) ←── AspectJ ajc 路线三 §5.2
↓ │ (源码 → 字节码时直接改)
字节码 .class
│
│ ┌── 加载期织入(LTW) ←── AspectJ + Java Agent 路线三 §5.3
↓ │ (类加载时 Transformer 改)
JVM 内已加载的类
│
│ ┌── 运行期织入(RTW) ←── JDK Proxy / CGLIB 路线一/二 §3、§4
↓ │ (生成代理类,目标类不变)
方法被调用
2
3
4
5
6
7
8
9
10
11
12
13
重要洞察:
- 运行期织入只能在"代理对象"上生效——因此 self-invocation(this 调用)穿透代理直达原对象,注解失效
- 编译/加载期织入直接改原类字节码——业务对象本身就是被织入后的对象,没有"代理"概念,自调用问题天然不存在
# 2.4 三大路线总览
| 路线 | 织入时机 | 实现机制 | 代表 |
|---|---|---|---|
| 路线一:JDK 动态代理 | 运行期 | 实现接口生成代理类 | Spring AOP(有接口时) |
| 路线二:CGLIB / ByteBuddy | 运行期 | 继承生成子类 | Spring AOP(无接口时) |
| 路线三:AspectJ | 编译期/加载期 | 直接改字节码 | 真正的 AOP 框架 |
约定:业内说"动态代理 = 路线一+二","AOP = 路线一+二+三"。Spring AOP 只是 AOP 的一个子集——它只用了路线一和二。
# 3. 路线一:JDK 动态代理
第 07 篇已经详细讲过
Proxy.newProxyInstance字节码生成机制。本节聚焦它在 AOP 路线中的定位与边界——避免重复。
# 3.1 接口代理的边界
JDK 动态代理的硬约束:目标类必须实现接口。
public interface OrderService { void placeOrder(Order o); }
@Service
public class OrderServiceImpl implements OrderService {
@Override public void placeOrder(Order o) { ... }
}
// 注入时必须用接口
@Autowired private OrderService orderService; // ✅ 收到的是 $Proxy0
@Autowired private OrderServiceImpl impl; // ❌ ClassCastException
2
3
4
5
6
7
8
9
10
为什么这样设计:JDK Proxy 生成的 $Proxy0 类继承 Proxy 类(已被占用,不能再继承目标类),只能通过实现接口"长得像"目标类。
# 3.2 织入时机:运行期生成
Spring 容器启动
│
↓
扫描 OrderServiceImpl,识别匹配的 advisor
│
↓
ProxyFactory.getProxy()
│
↓
JdkDynamicAopProxy.getProxy()
│
↓
Proxy.newProxyInstance(loader, interfaces, this) ← 运行期生成 $Proxy0
│
↓
$Proxy0 注入到容器(替代 OrderServiceImpl 实例)
│
↓
方法调用 → $Proxy0.placeOrder() → invoke(...) → advisor 链 → 目标方法
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 3.3 优劣与适用场景
| 维度 | JDK 动态代理 |
|---|---|
| ✅ 优势 | JDK 内置无依赖、生成代理快、JDK 8+ 性能与 CGLIB 接近 |
| ❌ 劣势 | 必须有接口、不能代理 final 方法、不能拦截构造器/静态方法 |
| 🎯 适用 | 接口设计良好的服务层(DDD/六边形架构最爱) |
# 4. 路线二:CGLIB 子类代理
第 07 篇已经讲过 CGLIB 的 FastClass 机制和与 JDK Proxy 的对比。本节聚焦Spring 6 弃用 CGLIB 改用 ByteBuddy 的工程根因——这是网上极少有人讲透的点。
# 4.1 继承代理的局限
// CGLIB 生成代理(伪代码)
public class OrderServiceImpl$$EnhancerByCGLIB$$abc extends OrderServiceImpl {
private MethodInterceptor callback;
public void placeOrder(Order o) {
callback.intercept(this, METHOD_placeOrder, args, METHOD_PROXY_placeOrder);
}
}
2
3
4
5
6
7
8
继承的天然限制:
- ❌ 不能代理
final类(无法继承) - ❌ 不能代理
final方法(无法重写) - ❌ 不能代理
private方法(不可见) - ❌ 不能代理静态方法(继承无关)
- ⚠️ 构造器会被调用两次(父类构造器 + 子类构造器)——曾导致大量数据库连接泄漏事故
# 4.2 Spring 6 为什么切到 ByteBuddy
CGLIB 在 Java 9+ 后水土不服——这是 Spring 6 切换到 ByteBuddy 的根本原因。
问题链路:
Java 9 (JEP 261):
引入 Java Module System (JPMS)
对 sun.misc.Unsafe 等内部 API 加访问限制
Java 11:
正式弃用 Nashorn / 收紧 sun.* / 加严反射访问
Java 17 (JEP 403):
Strongly Encapsulate JDK Internals
--add-opens 才能使用 sun.misc.Unsafe
CGLIB 依赖的关键 API:
- sun.misc.Unsafe.defineClass(...) ← 直接生成类
- sun.misc.Unsafe.defineAnonymousClass() ← JDK 17 已移除
- 反射访问 ClassLoader.defineClass ← 受限
结果:传统 CGLIB 在 JDK 17+ 上启动报警告 / 部分场景直接崩
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Spring 6 / Spring Boot 3 的应对:
Spring 5.x 及以前:
org.springframework.cglib (内嵌 CGLIB 3.x,老 ASM)
↓ 调用 sun.misc.Unsafe
↓ Java 17 报警告
Spring 6.x:
底层切换到 ByteBuddy(第 32 篇详细讲过)
↓ 使用 java.lang.invoke.MethodHandles.Lookup#defineClass
↓ JDK 9+ 标准 API、Java 17/21 完美兼容
↓ 性能略优于 CGLIB
2
3
4
5
6
7
8
9
10
对开发者影响:
- API 层无变化(仍是
@EnableAspectJAutoProxy(proxyTargetClass=true)) - 类名从
xxx$$EnhancerByCGLIB$$xxx→xxx$$SpringCGLIB$$0(看起来还叫 CGLIB 但底层已是 ByteBuddy) - 调试栈帧时一脸懵——这是 Spring 升级的隐藏破坏点
# 4.3 优劣与适用场景
| 维度 | CGLIB(Spring 6 = ByteBuddy) |
|---|---|
| ✅ 优势 | 不需要接口、性能略优于 JDK Proxy(FastClass) |
| ❌ 劣势 | 不能代理 final、不能代理 private、构造器被调用两次 |
| 🎯 适用 | 没有接口的 Service / Controller 类(Spring Boot 默认场景) |
# 5. 路线三:AspectJ 真·AOP
这是本篇最关键章节——也是 90% Java 程序员"知道 Spring AOP 但不知道 AspectJ"的盲区。
# 5.1 AspectJ 不是代理:它是字节码织入器
Spring AOP 的本质:
生成代理对象 → 拦截调用 → 调原方法
"原对象没有任何变化,是代理对象在干活"
AspectJ 的本质:
直接修改原类字节码 → 把 Advice 字节码塞进原方法
"没有代理对象,原对象本身就长在了切面之上"
2
3
4
5
6
7
举例——以下 OrderService 加上 AspectJ @Around 后,.class 反编译出来:
// 织入前
public void placeOrder(Order o) {
orderMapper.insert(o);
}
// 织入后 .class(伪反编译)
public void placeOrder(Order o) {
Object[] args = new Object[]{o};
JoinPoint jp = Factory.makeJP(ajc$tjp_0, this, args);
// ★★★ Advice 直接被复制进了方法体 ★★★
Object ret = TransactionAspect.aspectOf().around(jp, new AjcClosure1(args));
// closure 内部才是原始 orderMapper.insert(o);
}
2
3
4
5
6
7
8
9
10
11
12
13
没有代理类、没有 InvocationHandler、没有 MethodInterceptor——切面逻辑直接住进了原方法的字节码。
# 5.2 编译期织入 CTW
CTW = Compile-Time Weaving。
工具链:
.aj 切面文件 + .java 业务代码
↓
ajc 编译器 (AspectJ Compiler) ← 替代 javac
↓
.class(已织入)
↓
正常 java -jar 运行
2
3
4
5
6
7
Maven 集成:
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>aspectj-maven-plugin</artifactId>
<configuration>
<complianceLevel>17</complianceLevel>
<source>17</source>
<target>17</target>
<aspectLibraries>
<aspectLibrary>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
</aspectLibrary>
</aspectLibraries>
</configuration>
<executions>
<execution><goals><goal>compile</goal></goals></execution>
</executions>
</plugin>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
优劣:
- ✅ 性能最高(运行期零开销,只是普通方法调用)
- ✅ 可拦截构造器、静态方法、字段访问、
private方法 - ❌ 必须用 ajc 编译,IDE 与构建工具配置繁琐
- ❌ 依赖方的 .class 已固化,无法对其织入(除非有源码)
# 5.3 加载期织入 LTW
LTW = Load-Time Weaving——这就是第 33 篇 Java Agent 的应用场景。
工具链:
正常 javac 编译 .java → 普通 .class
↓
java -javaagent:aspectjweaver.jar -jar app.jar ← 关键
↓
ClassLoader 加载类时,aspectjweaver Agent 拦截
↓
读取 META-INF/aop.xml 配置
↓
ClassFileTransformer.transform() 改字节码
↓
JVM 拿到的已是织入后的字节码
2
3
4
5
6
7
8
9
10
11
aop.xml 配置:
<!-- META-INF/aop.xml -->
<aspectj>
<aspects>
<aspect name="com.example.TransactionAspect"/>
</aspects>
<weaver options="-verbose -showWeaveInfo">
<include within="com.example..*"/>
</weaver>
</aspectj>
2
3
4
5
6
7
8
9
启动:
java -javaagent:aspectjweaver-1.9.20.jar -jar app.jar
回扣 33 篇:aspectjweaver.jar 的 MANIFEST.MF 含 Premain-Class: org.aspectj.weaver.loadtime.Agent——它就是一个标准的 premain Java Agent,注册了一个 ClassFileTransformer,在每个类加载时根据 aop.xml 决定是否织入。
CTW vs LTW 选型:
| 维度 | CTW(编译期) | LTW(加载期) |
|---|---|---|
| 性能开销 | 启动后 0 开销 | 类加载时有解析/织入开销 |
| 编译工具 | 需 ajc 替代 javac | 普通 javac 即可 |
| 切面替换 | 需要重新编译 | 改 aop.xml 即可(不需要改代码) |
| 第三方 jar 织入 | ❌(除非源码可改) | ✅(加载时拦截) |
| 适用 | 自有代码 + 极致性能 | 中间件赋能 + 灵活配置 |
# 5.4 织入字节码对比
同一个 placeOrder 方法,三种路线下 .class 字节码差异:
原始字节码(无 AOP):
placeOrder(Order):
aload_0
aload_1
invokespecial orderMapper.insert
return
JDK Proxy 后(路线一):
原 OrderServiceImpl 字节码完全不变 ★
只是新增一个 $Proxy0 类(运行期生成)
CGLIB 后(路线二):
原 OrderServiceImpl 字节码完全不变 ★
只是新增一个 OrderServiceImpl$$EnhancerByCGLIB 子类
AspectJ 后(路线三):
★★★ OrderServiceImpl 字节码本身被改 ★★★
placeOrder(Order):
new JoinPoint 对象
aload_0
invokestatic TransactionAspect.aspectOf
invokevirtual around(JoinPoint, Closure)
return
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
这是 AOP 三大路线最本质的差异——前两个不动原类、第三个改原类。一切性能、能力、自调用问题都从这里推导。
# 6. Spring AOP 内部选型源码
# 6.1 DefaultAopProxyFactory 决策流程
Spring AOP 在 DefaultAopProxyFactory 里决定用 JDK 还是 CGLIB(Spring 6 = ByteBuddy):
// org.springframework.aop.framework.DefaultAopProxyFactory(精简)
@Override
public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException {
if (!NativeDetector.inNativeImage() &&
(config.isOptimize() || // ← 几乎不用
config.isProxyTargetClass() || // ← @EnableAspectJAutoProxy(proxyTargetClass=true)
hasNoUserSuppliedProxyInterfaces(config))) {// ← 目标类没接口
Class<?> targetClass = config.getTargetClass();
// 即便强制 CGLIB,如果"接口本身就是 Spring 内部代理接口",仍走 JDK
if (targetClass.isInterface() || Proxy.isProxyClass(targetClass)
|| ClassUtils.isLambdaClass(targetClass)) {
return new JdkDynamicAopProxy(config);
}
return new ObjenesisCglibAopProxy(config); // ← 路线二
}
return new JdkDynamicAopProxy(config); // ← 路线一
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
决策树:
有 proxyTargetClass=true?
┌────── 是 ───────┐
│ │
↓ ↓
目标类是接口? 用 CGLIB
↓
否
│
┌─────┴──────┐
↓ ↓
CGLIB JDK Proxy
↑
┌─────┴──────┐
│ 目标类有接口 │
└────────────┘
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Spring Boot 2.x+ 默认值:
spring:
aop:
proxy-target-class: true # 默认 true → 默认 CGLIB
2
3
# 6.2 advisor 链的执行机制
无论 JDK Proxy 还是 CGLIB,Spring 都使用统一的 ReflectiveMethodInvocation 走 advisor 责任链:
proxy.placeOrder(order)
↓
JdkDynamicAopProxy.invoke() / CglibAopProxy.intercept()
↓
List<MethodInterceptor> chain = config.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass);
↓
new ReflectiveMethodInvocation(proxy, target, method, args, chain).proceed();
↓
责任链:
ExposeInvocationInterceptor ← 把 MethodInvocation 放 ThreadLocal
TransactionInterceptor.invoke() ← 开事务
chain.next().proceed()
↓
AnotherAroundInterceptor ← 比如日志切面
↓
target.placeOrder(order) ← 调用真实方法
↑
← 异常向上抛 / 返回值向上传
TransactionInterceptor 决定 commit/rollback
← 最终回到 proxy.invoke()
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
核心设计模式:经典责任链 + 拦截器——这个模式在 Servlet Filter / Netty Pipeline / OkHttp / MyBatis Plugin 中无处不在。
# 6.3 与 AspectJ 的混合使用
工程中最常见的"伪 AspectJ"——@AspectJ 注解 + Spring AOP 引擎:
@Aspect ← 注解来自 AspectJ
@Component
public class LogAspect {
@Around("execution(* com.x..*Service.*(..))")
public Object log(ProceedingJoinPoint pjp) { ... }
}
2
3
4
5
6
真相:上面这段代码仍然是 Spring AOP(路线一/二)——只是用了 AspectJ 的注解语法和 Pointcut 表达式。真正的 AspectJ 必须配 ajc 编译或 LTW Agent。区分:
看 pom 依赖:
spring-aop + aspectjweaver(只用注解解析) → Spring AOP
spring-aop + aspectjweaver(注解 + LTW Agent) → AspectJ LTW
aspectj-maven-plugin + aspectjrt → AspectJ CTW
2
3
4
# 7. 三大路线全维度对比
# 7.1 功能能力矩阵
| 能力 | JDK Proxy | CGLIB/ByteBuddy | AspectJ |
|---|---|---|---|
| 拦截 public 方法 | ✅ | ✅ | ✅ |
| 拦截 protected 方法 | ❌ | ✅ | ✅ |
| 拦截 private 方法 | ❌ | ❌ | ✅ |
| 拦截 final 方法 | ❌ | ❌ | ✅ |
| 拦截 final 类 | ✅(如有接口) | ❌ | ✅ |
| 拦截 static 方法 | ❌ | ❌ | ✅ |
| 拦截构造器 | ❌ | ❌ | ✅ |
| 拦截字段访问 | ❌ | ❌ | ✅ |
| 解决 self-invocation | ❌ | ❌ | ✅ |
| 拦截第三方 jar 类 | ❌ | ❌ | ✅(LTW) |
结论:AspectJ 是真·AOP,能力是 Spring AOP 的超集。Spring AOP 是简化版,因为运行期代理只能拦截"经过代理对象的调用"。
# 7.2 性能横评
来自 [Mark Reinhold 团队 + spring-projects/spring-aop benchmark](数据为相对值):
基准:直接方法调用 = 1.00x
JDK 动态代理 (JDK 17 + Spring 6) : 1.10x ~ 1.15x
CGLIB / ByteBuddy (Spring 6) : 1.05x ~ 1.10x
AspectJ CTW : 1.00x ~ 1.02x ← 几乎零开销
AspectJ LTW : 1.00x ~ 1.02x ← 启动慢,运行同 CTW
2
3
4
5
6
AspectJ 性能为什么最好:
- 编译/加载期已织入完成 → 运行期就是普通方法调用
- 没有 InvocationHandler/MethodInterceptor 的额外栈帧
- JIT 可以正常内联(代理对象会阻止内联)
# 7.3 侵入性与运维成本
| 维度 | Spring AOP(路线一+二) | AspectJ CTW | AspectJ LTW |
|---|---|---|---|
| 学习成本 | ★ 低 | ★★★ 高(要学 .aj/Pointcut) | ★★ 中 |
| 构建侵入 | 无 | 需替换 ajc 编译 | 无 |
| 启动参数 | 无 | 无 | 必须加 -javaagent |
| 调试难度 | 中(栈帧有 $Proxy) | 低 | 中(agent 增强) |
| 部署复杂度 | 低 | 低 | 中(多了 Agent jar) |
取舍口诀:Spring AOP 是"最大公约数",AspectJ 是"上限"。
# 8. 自调用问题与生产决策
# 8.1 self-invocation 失效的根因
回到 §1.1 的故障:
public void placeOrder(Order order) {
saveOrder(order); // ← this.saveOrder()
deductStock(order); // ← this.deductStock()
}
2
3
4
Spring 注入的是代理对象,但 placeOrder 内的 this 是原对象:
调用方 → proxy.placeOrder() ← 代理介入
↓
target.placeOrder() 真实方法
↓
this.saveOrder() ← ★ this = target,不是 proxy
↓
target.saveOrder() ← ★ 直接执行原方法
★ TransactionInterceptor 完全没机会拦截
2
3
4
5
6
7
8
根因:JDK Proxy / CGLIB 都是"代理对象拦截"——this 引用绕过代理直达原对象,注解失效。
# 8.2 5 种破解方案
| 方案 | 做法 | 评价 |
|---|---|---|
| 方案一:注入自己 | @Autowired private OrderService self; self.saveOrder() | 简单但有循环依赖味道 |
| 方案二:AopContext | ((OrderService)AopContext.currentProxy()).saveOrder() | 需 @EnableAspectJAutoProxy(exposeProxy=true)、ThreadLocal 开销 |
| 方案三:拆类 | 把 saveOrder 抽到独立 Service | 工程最干净 |
| 方案四:编程式事务 | transactionTemplate.execute(...) | 灵活但啰嗦 |
| 方案五:AspectJ | 切到 AspectJ CTW/LTW | 一劳永逸但学习成本 |
生产推荐:先方案三(80% 场景适用)→ 中间件团队推方案五(彻底解决)。
# 8.3 选型决策树
项目使用 AOP 的场景?
│
┌───────────────┼─────────────────┐
↓ ↓ ↓
仅业务侧切面 需要切第三方 极致性能要求
(日志/事务) 类/private (低延迟系统)
│ │ │
↓ ↓ ↓
Spring AOP AspectJ LTW AspectJ CTW
(够用90%场景) (中间件首选) (金融/HFT)
2
3
4
5
6
7
8
9
10
实际工程比例(来自我经历的 30+ 项目):
┌─────────────────────────────────────────┐
│ Spring AOP(路线一+二) ≈ 90% │ ← 业务系统
│ AspectJ LTW(路线三) ≈ 8% │ ← 中间件 / APM
│ AspectJ CTW(路线三) ≈ 2% │ ← 高频交易 / 极致性能
└─────────────────────────────────────────┘
2
3
4
5
# 9. 综合回扣与卷四收官
# 9.1 案例真相揭晓
① §1.1 @Transactional 失效真相:Spring 容器注入的 OrderService 是 CGLIB 代理对象,但 placeOrder 内调用 this.saveOrder() 时,this 引用的是原对象而非代理对象——TransactionInterceptor 没有机会介入(§8.1)。根因是 Spring AOP 走的是路线二(运行期代理)——只有"经过代理对象的方法调用"才会被拦截。生产破解 5 选 1(§8.2)。
② §1.2 AspectJ 不会失效真相:AspectJ 走的是路线三(编译/加载期织入)——切面字节码直接被塞进 placeOrder 方法体(§5.4),原对象本身就是被织入后的对象,没有"代理 vs 原对象"的二元结构。无论哪种调用方式(this / 外部 / 反射),方法字节码里都已经有事务管理的逻辑。这是 AspectJ 称为"真·AOP"的根本原因。
③ 5 大追问全部作答:
| 追问 | 答案 | 章节 |
|---|---|---|
| ① AOP 三种实现的本质差别 | 织入时机:运行期/加载期/编译期 | §2.3、§5.4 |
| ② Spring 6 弃 CGLIB | sun.misc.Unsafe 受限 + ByteBuddy 用标准 API | §4.2 |
| ③ CTW vs LTW 选型 | 自有代码用 CTW,第三方/中间件用 LTW | §5.3 |
| ④ Spring AOP JDK vs CGLIB | DefaultAopProxyFactory 三条件判断 | §6.1 |
| ⑤ self-invocation 根因 | 运行期代理 + this 引用绕过代理 | §8.1 |
# 9.2 设计哲学回扣
收官提炼三条工程哲学:
"织入时机"是 AOP 的灵魂:所有关于 AOP 的争论——能否拦 final、能否拦 private、能否解决 self-invocation——本质上都被一个变量决定:字节码什么时候被改。改得越早(编译期),能力越强、运行越快、运维越复杂;改得越晚(运行期),能力越弱、运维越简单、自调用问题越多。这就是工程的本质:没有银弹,只有时间维度上的权衡。下次你看任何"框架是怎么实现的",先问:它在哪个时机做了什么事? 这一个问题能秒杀 80% 的源码理解障碍。
"够用就好"才是真智慧:AspectJ 是技术上的至高存在,但 95% 项目用 Spring AOP 就够了。为什么?因为 Spring AOP 用了 5% 的复杂度解决了 95% 的需求。当年 Rod Johnson 选择"代理 + 责任链"而非完整 AOP,是经过深思熟虑的——他知道大多数 Java 程序员没有义务也没有时间去学一门新的 .aj 语言。这给我们的启示是:做框架要追求"刚刚好",而不是"无所不能"。Mockito 选 ByteBuddy 不选 AspectJ、Lombok 选 APT 不选 LTW、Spring 选 Proxy 不选完整字节码织入——都是同一种克制。
"自调用问题"教会我们:永远区分逻辑与物理:
@Transactional失效的根因是程序员把"逻辑层面的方法调用"和"物理层面的对象引用"混淆了。在 OOP 思维里this.saveOrder()和proxy.saveOrder()是同一回事;在 AOP 实现层面,二者天差地别。这种"逻辑-物理不一致"在工程里到处都是:Java 内存模型的可见性(你以为 a=1 写完了,CPU 缓存里还没刷)、HTTP 的端到端语义(你以为成功了,TCP 层还在重传)、数据库的事务隔离(你以为读到了最新值,MVCC 给你看的是快照)。优秀工程师的标志,是能在心里同时持有"逻辑模型"和"物理实现"两层抽象,并知道它们什么时候会撕裂。
# 9.3 卷四五篇知识地图
卷四:反射与字节码增强(5 篇)
│
┌───────────────────────────────┼───────────────────────────────┐
│ │ │
理论基础 现代化继任 应用整合
│ │ │
↓ ↓ ↓
07.反射 & 动态代理 31.MethodHandle 33.Java Agent
│ \ & VarHandle │
│ \ │ │
│ \ ↓ ↓
│ 32.字节码框架 ───────────────────────── 34.AOP 路线对比
│ (ASM/Javassist/ (本篇 = 收官)
│ ByteBuddy)
│ │
└───────────────────────────────────────────────────────┘
★ 闭环:从字节码到 AOP 完整生态 ★
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
卷四回扣:
- 07 篇打底(反射 + 代理基础)
- 31 篇衔接现代(MethodHandle 替代反射)
- 32 篇拓宽工具(三大字节码框架)
- 33 篇打通生态(Agent + Instrumentation)
- 34 篇收官升华(AOP 路线对比)
至此 JVM 层面"动态修改运行时行为"的整条技术栈打通——从最底层的 Class 文件结构(13 篇)→ 反射调用(07)→ 字节码生成(32)→ Agent 钩子(33)→ AOP 应用(34),形成完整闭环。
# 9.4 速查表
Spring AOP 配置速查:
@SpringBootApplication
@EnableAspectJAutoProxy(
proxyTargetClass = true, // true=CGLIB / false=JDK Proxy(默认 true)
exposeProxy = true // true=可用 AopContext.currentProxy()
)
public class App { }
2
3
4
5
6
AspectJ 启动速查:
# CTW
mvn compile # 用 aspectj-maven-plugin 编译
# LTW
java -javaagent:aspectjweaver.jar -jar app.jar
# + META-INF/aop.xml 配置切面与织入范围
2
3
4
5
6
Pointcut 表达式速查:
execution(返回类型 包.类.方法(参数)) ← 最常用
@annotation(注解全限定名) ← 注解驱动
within(包..*) ← 限定范围
this(类型) ← 代理对象类型
target(类型) ← 目标对象类型
args(参数类型) ← 参数类型
2
3
4
5
6
生产场景选型速查:
业务系统的日志/事务/权限 → Spring AOP(CGLIB 默认)
中间件做 APM/链路追踪 → AspectJ LTW + Java Agent
低延迟系统(金融/HFT) → AspectJ CTW
self-invocation 救急 → 注入自己 / AopContext / 拆类
JDK 17+ Spring 升级 → 必须 Spring 6+(CGLIB 已弃)
2
3
4
5
三大路线一图速记:
路线一 (JDK Proxy): 运行期 + 实现接口生成 $Proxy0
路线二 (CGLIB/ByteBuddy):运行期 + 继承生成子类
路线三 (AspectJ): 编译期/加载期 + 直接改原类字节码 ★ 真·AOP
2
3
🎉 卷四《反射与字节码增强》收官 🎉
至此专栏 4/7 卷完结、累计 34/51 篇——下一篇进入 卷五《并发编程深水区》第 4 篇(专栏第 35 篇):Thread 与线程生命周期源码——从 Thread.start()/run()/join()/interrupt() 的源码真相切入,深入 ThreadLocal 与 InheritableThreadLocal 的设计、ThreadLocalMap 弱引用 Entry 的内存泄漏机制、与第 41 篇虚拟线程的生命周期对照,为后续 AQS、ReentrantLock、CAS 三连击打底。