编程进阶网 编程进阶网
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接

杨充

专注编程 · 终身学习者
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接
  • README
  • C语言入门精通

  • Cpp入门到精通

  • Java入门精通

    • README
    • 入门教程

    • 综合案例

    • 专栏博客

      • README
      • JVM内存模型与对象
      • 类加载与双亲委派
      • 垃圾回收与GC调优
      • 异常体系与JVM机制
      • 字节码指令集javap实战
      • JIT编译与去优化机制
      • JVM性能诊断工具链
      • OOM八大现场全景剖析
      • JVM参数调优全景图
      • GraalVM与AOT编译原理
      • HashMap底层哈希设计
      • String不可变与常量池
      • ArrayList与LinkedList源码
      • ConcurrentHashMap并发
      • TreeMap与红黑树原理
      • LinkedHashMap与LRU实现
      • Java数字类型原理
      • Object通用方法的契约
      • 泛型擦除与类型系统
      • 枚举原理与最佳实践
      • 注解原理与编译期处理
      • Lambda与引用底层原理
      • Stream原理与流水线设计
      • Optional设计原理
      • Record密封类与模式
      • 反射机制与动态代理
      • MethodHandle与VarHandle
      • 三大字节码框架对比
      • JavaAgent与Instrumentation机制
      • AOP三种实现路线对比
        • 1. 案例引入
          • 1.1 @Transactional 注解为什么会失效?
          • 1.2 AspectJ 一行注解搞定全部接入点
          • 1.3 我们要回答什么
        • 2. AOP 全景概念体系
          • 2.1 横切关注点的本质
          • 2.2 七大核心术语
          • 2.3 织入的三个时机
          • 2.4 三大路线总览
        • 3. 路线一:JDK 动态代理
          • 3.1 接口代理的边界
          • 3.2 织入时机:运行期生成
          • 3.3 优劣与适用场景
        • 4. 路线二:CGLIB 子类代理
          • 4.1 继承代理的局限
          • 4.2 Spring 6 为什么切到 ByteBuddy
          • 4.3 优劣与适用场景
        • 5. 路线三:AspectJ 真·AOP
          • 5.1 AspectJ 不是代理:它是字节码织入器
          • 5.2 编译期织入 CTW
          • 5.3 加载期织入 LTW
          • 5.4 织入字节码对比
        • 6. Spring AOP 内部选型源码
          • 6.1 DefaultAopProxyFactory 决策流程
          • 6.2 advisor 链的执行机制
          • 6.3 与 AspectJ 的混合使用
        • 7. 三大路线全维度对比
          • 7.1 功能能力矩阵
          • 7.2 性能横评
          • 7.3 侵入性与运维成本
        • 8. 自调用问题与生产决策
          • 8.1 self-invocation 失效的根因
          • 8.2 5 种破解方案
          • 8.3 选型决策树
        • 9. 综合回扣与卷四收官
          • 9.1 案例真相揭晓
          • 9.2 设计哲学回扣
          • 9.3 卷四五篇知识地图
          • 9.4 速查表
      • synchronized与锁升级
      • volatile与JMM内存模型
      • 线程池核心源码设计
      • Thread线程生命周期
      • AQS同步框架源码
      • 并发锁三剑客
      • CAS和Atomic深入分析
      • 五大同步器对比
      • CompletableFuture异步
      • IO模型演进BIO到AIO
      • ByteBuffer与堆外内存
      • 序列化原理与替代方案
      • 文件IO与NIO.2
      • 面向对象的真意
      • JDK设计模式上
      • JDK设计模式下
      • SPI与模块化设计
  • Go入门到精通

  • JavaScript入门

  • CodeX
  • Java入门精通
  • 专栏博客
杨充
2026-06-02
目录

AOP三种实现路线对比

# 30.AOP三种实现路线对比

# 目录介绍

  • 1. 案例引入
    • 1.1 @Transactional 注解为什么会失效?
    • 1.2 AspectJ 一行注解搞定全部接入点
    • 1.3 我们要回答什么
  • 2. AOP 全景概念体系
    • 2.1 横切关注点的本质
    • 2.2 七大核心术语
    • 2.3 织入的三个时机
    • 2.4 三大路线总览
  • 3. 路线一:JDK 动态代理
    • 3.1 接口代理的边界
    • 3.2 织入时机:运行期生成
    • 3.3 优劣与适用场景
  • 4. 路线二:CGLIB 子类代理
    • 4.1 继承代理的局限
    • 4.2 Spring 6 为什么切到 ByteBuddy
    • 4.3 优劣与适用场景
  • 5. 路线三:AspectJ 真·AOP
    • 5.1 AspectJ 不是代理:它是字节码织入器
    • 5.2 编译期织入 CTW
    • 5.3 加载期织入 LTW
    • 5.4 织入字节码对比
  • 6. Spring AOP 内部选型源码
    • 6.1 DefaultAopProxyFactory 决策流程
    • 6.2 advisor 链的执行机制
    • 6.3 与 AspectJ 的混合使用
  • 7. 三大路线全维度对比
    • 7.1 功能能力矩阵
    • 7.2 性能横评
    • 7.3 侵入性与运维成本
  • 8. 自调用问题与生产决策
    • 8.1 self-invocation 失效的根因
    • 8.2 5 种破解方案
    • 8.3 选型决策树
  • 9. 综合回扣与卷四收官
    • 9.1 案例真相揭晓
    • 9.2 设计哲学回扣
    • 9.3 卷四五篇知识地图
    • 9.4 速查表

# 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());
    }
}
1
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;
        }
    }
}
1
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 路线大对比 ← 把上面四篇全部串起来
1
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
1
2
3
4
5

# 2. AOP 全景概念体系

# 2.1 横切关注点的本质

传统 OOP:
    OrderService    UserService    StockService    ProductService
        │                │                │                │
   ┌────┴────┐      ┌────┴────┐      ┌────┴────┐      ┌────┴────┐
   │  日志   │      │  日志   │      │  日志   │      │  日志   │   ←─┐
   │  事务   │      │  事务   │      │  事务   │      │  事务   │   ←─┤ 横切关注点
   │  权限   │      │  权限   │      │  权限   │      │  权限   │   ←─┤ (Cross-Cutting Concerns)
   │  监控   │      │  监控   │      │  监控   │      │  监控   │   ←─┘
   │ 业务A   │      │ 业务B   │      │ 业务C   │      │ 业务D   │
   └─────────┘      └─────────┘      └─────────┘      └─────────┘
   
   问题:每个类都重复写 4 类基础设施代码 → 代码 80% 是"装订线"
1
2
3
4
5
6
7
8
9
10
11
12
AOP 思路:
                     ┌────────────────────────┐
                     │  日志切面               │ ← 写一次
                     │  事务切面               │ ← 写一次
                     │  权限切面               │ ← 写一次
                     │  监控切面               │ ← 写一次
                     └─────────┬──────────────┘
                               │
                          织入(Weaving)
                               │
            ┌──────────────────┼──────────────────┐
            ↓                  ↓                  ↓
       OrderService       UserService        StockService
       (只剩业务A)      (只剩业务B)      (只剩业务C)
1
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
1
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
   ↓   │                       (生成代理类,目标类不变)
   方法被调用
1
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
1
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 链 → 目标方法
1
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);
    }
}
1
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+ 上启动报警告 / 部分场景直接崩
1
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
1
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 字节码塞进原方法
   "没有代理对象,原对象本身就长在了切面之上"
1
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);
}
1
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 运行
1
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>
1
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 拿到的已是织入后的字节码
1
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>
1
2
3
4
5
6
7
8
9

启动:

java -javaagent:aspectjweaver-1.9.20.jar -jar app.jar
1

回扣 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
1
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);           // ← 路线一
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

决策树:

                     有 proxyTargetClass=true?
                      ┌────── 是 ───────┐
                      │                │
                      ↓                ↓
                目标类是接口?      用 CGLIB
                      ↓
                      否
                      │
                ┌─────┴──────┐
                ↓            ↓
              CGLIB       JDK Proxy
                            ↑
                      ┌─────┴──────┐
                      │ 目标类有接口 │
                      └────────────┘
1
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
1
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()
1
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) { ... }
}
1
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
1
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
1
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()
}
1
2
3
4

Spring 注入的是代理对象,但 placeOrder 内的 this 是原对象:

调用方 → proxy.placeOrder()                       ← 代理介入
           ↓
        target.placeOrder() 真实方法
           ↓
        this.saveOrder()                          ← ★ this = target,不是 proxy
           ↓
        target.saveOrder()                        ← ★ 直接执行原方法
                                                    ★ TransactionInterceptor 完全没机会拦截
1
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)
1
2
3
4
5
6
7
8
9
10

实际工程比例(来自我经历的 30+ 项目):

┌─────────────────────────────────────────┐
│ Spring AOP(路线一+二)         ≈ 90%  │  ← 业务系统
│ AspectJ LTW(路线三)           ≈  8%  │  ← 中间件 / APM
│ AspectJ CTW(路线三)           ≈  2%  │  ← 高频交易 / 极致性能
└─────────────────────────────────────────┘
1
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 设计哲学回扣

收官提炼三条工程哲学:

  1. "织入时机"是 AOP 的灵魂:所有关于 AOP 的争论——能否拦 final、能否拦 private、能否解决 self-invocation——本质上都被一个变量决定:字节码什么时候被改。改得越早(编译期),能力越强、运行越快、运维越复杂;改得越晚(运行期),能力越弱、运维越简单、自调用问题越多。这就是工程的本质:没有银弹,只有时间维度上的权衡。下次你看任何"框架是怎么实现的",先问:它在哪个时机做了什么事? 这一个问题能秒杀 80% 的源码理解障碍。

  2. "够用就好"才是真智慧:AspectJ 是技术上的至高存在,但 95% 项目用 Spring AOP 就够了。为什么?因为 Spring AOP 用了 5% 的复杂度解决了 95% 的需求。当年 Rod Johnson 选择"代理 + 责任链"而非完整 AOP,是经过深思熟虑的——他知道大多数 Java 程序员没有义务也没有时间去学一门新的 .aj 语言。这给我们的启示是:做框架要追求"刚刚好",而不是"无所不能"。Mockito 选 ByteBuddy 不选 AspectJ、Lombok 选 APT 不选 LTW、Spring 选 Proxy 不选完整字节码织入——都是同一种克制。

  3. "自调用问题"教会我们:永远区分逻辑与物理:@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 完整生态 ★
1
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 { }
1
2
3
4
5
6

AspectJ 启动速查:

# CTW
mvn compile           # 用 aspectj-maven-plugin 编译

# LTW
java -javaagent:aspectjweaver.jar -jar app.jar
# + META-INF/aop.xml 配置切面与织入范围
1
2
3
4
5
6

Pointcut 表达式速查:

execution(返回类型 包.类.方法(参数))   ← 最常用
@annotation(注解全限定名)            ← 注解驱动
within(包..*)                       ← 限定范围
this(类型)                          ← 代理对象类型
target(类型)                        ← 目标对象类型
args(参数类型)                      ← 参数类型
1
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 已弃)
1
2
3
4
5

三大路线一图速记:

路线一 (JDK Proxy):     运行期 + 实现接口生成 $Proxy0
路线二 (CGLIB/ByteBuddy):运行期 + 继承生成子类
路线三 (AspectJ):        编译期/加载期 + 直接改原类字节码 ★ 真·AOP
1
2
3

🎉 卷四《反射与字节码增强》收官 🎉
至此专栏 4/7 卷完结、累计 34/51 篇——下一篇进入 卷五《并发编程深水区》第 4 篇(专栏第 35 篇):Thread 与线程生命周期源码——从 Thread.start()/run()/join()/interrupt() 的源码真相切入,深入 ThreadLocal 与 InheritableThreadLocal 的设计、ThreadLocalMap 弱引用 Entry 的内存泄漏机制、与第 41 篇虚拟线程的生命周期对照,为后续 AQS、ReentrantLock、CAS 三连击打底。

上次更新: 2026/06/10, 11:13:41
JavaAgent与Instrumentation机制
synchronized与锁升级

← JavaAgent与Instrumentation机制 synchronized与锁升级→

最近更新
01
信号崩溃快速排查
06-15
02
CoreDump破案
06-15
03
perf火焰图实战
06-15
更多文章>
Theme by Vdoing | Copyright © 2019-2026 杨充 | MIT License | 桂ICP备2024034950号 | 桂公网安备45142202000030
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式