Spring 里有两块知识经常被分开背:AOP 动态代理,以及 Spring Boot fat jar 为什么能直接 java -jar 启动。一个属于运行期增强,一个属于启动和类加载。它们看似无关,底层都绕不开 JVM 类、字节码和 ClassLoader。
整理时可以按两条线看:AOP 是“方法调用前后怎么插逻辑”;fat jar 是“嵌套 jar 里的类怎么被加载起来”。
AOP 本质是在调用链上加一层
AOP 不是魔法,它的核心是拦截方法调用,在调用前、调用后、异常时插入额外逻辑。
典型场景包括:
- 事务。
- 日志。
- 权限。
- 监控埋点。
- 缓存。
如果每个方法都手写这些横切逻辑,业务代码会变脏。AOP 把这些公共逻辑抽出来,通过代理对象统一处理。
JDK 动态代理和 CGLib 的差别
Spring AOP 常见两种代理方式:
| 方式 | 基础 | 适合场景 | 限制 |
| — | — | — | — |
| JDK 动态代理 | 接口 | 目标类实现了接口 | 必须通过接口代理 |
| CGLib | 生成子类 | 没有接口也能代理 | final 类/方法不能代理 |
JDK 动态代理核心是 Proxy.newProxyInstance 和 InvocationHandler。它生成一个实现同样接口的代理类,调用接口方法时进入 invoke。
CGLib 则是生成目标类的子类,重写非 final 方法,在方法里走 MethodInterceptor。所以 final 类和 final 方法不能被它覆盖。
工程里不用把两者神化。记住一句话:有接口时 JDK 代理很自然;没有接口时 CGLib 通过继承生成代理子类。
为什么有时 AOP 不生效
AOP 不生效常见不是框架坏了,而是调用没有经过代理对象。
比如同一个类里方法 A 调方法 B:
public void a() {
b();
}
@Transactional
public void b() {
}
这里 a() 内部直接调用 this.b(),没有经过 Spring 容器里的代理对象,事务就可能不生效。
其他常见原因:
- 方法不是 public。
- 类或方法是 final。
- 对象不是 Spring 容器管理的 Bean。
- 注解加在了错误位置。
- 切点表达式没匹配上。
排查 AOP 时,先确认调用对象是不是代理对象,再看代理方式和切点。
LTW 是另一种织入方式
除了运行期动态代理,还有类加载期织入,也就是 LTW。它基于 java.lang.instrument,通过 -javaagent 在类加载时改写字节码。
核心接口是:
ClassFileTransformer.transform(...)
每次 JVM 加载类时,Transformer 都有机会拿到原始字节码并返回修改后的字节码。
LTW 的能力比普通动态代理更强,可以影响 final、private、构造方法等位置。但代价也更高:排查复杂、侵入启动参数、心智负担更重。日常 Spring AOP 默认用动态代理,只有少数特殊场景才需要 AspectJ 或 LTW。
Spring Boot fat jar 里有什么
Spring Boot 可执行 jar 和普通 jar 不太一样。典型结构是:
xxx.jar
├── BOOT-INF/classes/
├── BOOT-INF/lib/
├── META-INF/MANIFEST.MF
└── org/springframework/boot/loader/
其中:
BOOT-INF/classes/放业务代码和配置。BOOT-INF/lib/放依赖 jar。spring-boot-loader负责启动和加载嵌套 jar。
普通 Java ClassLoader 不直接支持 jar 里再嵌套一堆 jar 作为 classpath。Spring Boot 的 loader 就是为了解决这个问题。
MANIFEST 里的两个入口
Spring Boot fat jar 的 MANIFEST.MF 里通常有两个关键字段:
Main-Class: org.springframework.boot.loader.JarLauncher
Start-Class: com.example.YourApplication
Main-Class 不是业务启动类,而是 Spring Boot 的 JarLauncher。真正的业务启动类放在 Start-Class。
启动流程大概是:
java -jar app.jar进入JarLauncher.main。JarLauncher创建自己的 ClassLoader。- 它加载
BOOT-INF/classes/和BOOT-INF/lib/*.jar。 - 它读取
Start-Class。 - 反射调用业务启动类的
main方法。 - 业务代码进入
SpringApplication.run。
理解到这一步,就知道为什么 fat jar 不是单纯把所有依赖复制进去,而是带了一套启动加载机制。
SpringApplication.run 做了什么
进入业务 main 后,SpringApplication.run 会做一系列初始化:
- 判断 Web 应用类型。
- 加载 initializer 和 listener。
- 创建 ApplicationContext。
- 执行 refresh。
- 启动嵌入式容器。
- 注册 shutdown hook。
Web 项目最终会启动 Tomcat、Jetty、Undertow 或 Netty 等容器。非 Web 项目则只创建普通 ApplicationContext。
两条线放在一起看
AOP 和 fat jar 启动都说明一件事:Spring 很多能力不是凭空来的,而是建立在 JVM 机制之上。
AOP 依赖代理、字节码或类加载期织入;fat jar 依赖自定义启动器和 ClassLoader。知道这些边界后,排查问题会更有方向。
事务不生效,先看有没有经过代理;jar 启动找不到类,先看 BOOT-INF、MANIFEST 和 ClassLoader。抓住底层机制,Spring 的很多“自动”行为就没那么玄了。



