Spring AOP 和 Fat Jar 启动怎么理解:代理、织入和 JarLauncher

3次阅读
没有评论

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.newProxyInstanceInvocationHandler。它生成一个实现同样接口的代理类,调用接口方法时进入 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

启动流程大概是:

  1. java -jar app.jar 进入 JarLauncher.main
  2. JarLauncher 创建自己的 ClassLoader。
  3. 它加载 BOOT-INF/classes/BOOT-INF/lib/*.jar
  4. 它读取 Start-Class
  5. 反射调用业务启动类的 main 方法。
  6. 业务代码进入 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 的很多“自动”行为就没那么玄了。

正文完
 0
bdspAdmin
版权声明:本站原创文章,由 bdspAdmin 于2026-06-30发表,共计2331字。
转载说明:除特殊说明外本站文章皆由CC-4.0协议发布,转载请注明出处。
评论(没有评论)