本节目标:把
process-aot在构建期做的事拆成一条可核实的调用链,看清它生成了哪些源码与配置、SpringApplication在 AOT 模式下为什么能少走反射,以及扩展点到底挂在哪里。
适用版本:Spring Boot 4.1.x(Java 21)
9.1 AOT 处理与生成物
实战卷解决的是「怎么把服务跑起来」,本节解决「构建期到底预演了什么」。AOT(Ahead-Of-Time)在 Spring Boot 里不是「编译成机器码」——那是 GraalVM native-image 的活——而是把运行期才做的装配决策,提前到构建期算一遍并固化成代码。理解这一点,后面两节讲反射与原生镜像权衡时才不会混淆「Spring 的 AOT」和「GraalVM 的 AOT」两件事。
本节围绕一个最小应用 LibraryApplication 展开:它扫描到 BookRepository、LoanService 两个组件。后面 9.2 讲这些 bean 在原生镜像里为什么还需要额外声明反射,9.3 讲值不值得上原生镜像。
诚实性提示:本节所有类名、方法名、目录常量均从本机
~/.m2下的spring-boot-4.1.1.jar、spring-context-7.0.9.jar、spring-beans-7.0.9.jar与spring-boot-maven-plugin-4.1.1.jar用javap/unzip核实。本机没有 GraalVMnative-image,因此凡是「构建日志」与「生成的目录树」一律标注为示例输出,不是本机实测。
9.1.1 为什么需要 AOT:封闭世界假设
JVM 是一个开放世界:类可以在运行期被反射加载、方法可以在运行期被代理、资源可以按名字动态读取。GraalVM 原生镜像反过来,假设封闭世界——构建时能静态分析到的类才会进镜像,运行期再想 Class.forName("...") 一个没被分析到的类就会抛 ClassNotFoundException。
Spring 的装配恰恰是反射密集型的:@ComponentScan 靠反射读注解、@Configuration 靠反射调工厂方法、@Autowired 靠反射注入、BeanPostProcessor 靠反射生成代理。如果什么都不做直接丢给 native-image,绝大多数 bean 会因为「不可达」被裁掉。
AOT 处理的思路是:在构建期把容器真正跑一遍(但不产生副作用),把这次运行中发生的每一条 bean 定义、每一次注入、每一个候选构造器,翻译成等价的、纯代码的装配逻辑。运行期不再靠反射扫描,而是执行这些生成的方法。这就是 Spring 的 AOT 与 GraalVM 的 AOT 的分工。
9.1.2 process-aot:构建期的一次「预演」
Maven 侧的处理入口是 spring-boot-maven-plugin 的 process-aot goal。从本机 spring-boot-maven-plugin-4.1.1.jar 的 META-INF/maven/plugin.xml 可以核实到它的元数据:
| 项 | 值(plugin.xml 核实) |
|---|---|
| goal | process-aot |
| 实现类 | org.springframework.boot.maven.ProcessAotMojo |
| 绑定阶段 | prepare-package |
| since | 3.0.0 |
| 依赖解析 | compile+runtime |
| 测试侧对应 goal | process-test-aot → ProcessTestAotMojo,阶段 process-test-classes |
也就是说,一次 mvn package 会在打包之前自动触发 AOT 处理;不需要额外命令。要单独触发可以显式执行:
./mvnw spring-boot:process-aot
输出目录是 Mojo 里写死的常量(ProcessAotMojo 字节码字符串核实):
${project.build.directory}/spring-aot/main/sources
${project.build.directory}/spring-aot/main/resources
${project.build.directory}/spring-aot/main/classes
测试侧的 ProcessTestAotMojo 对应 spring-aot/test/... 三个目录,并由 SpringBootTestAotProcessor 驱动。
4.1 的一处行为变更值得记住:
-DskipTests不再跳过测试的 AOT 处理,要连 AOT 一起跳过得用-Dmaven.test.skip=true。这正是ProcessTestAotMojo字节码里判断的maven.test.skip属性。
9.1.3 处理入口:SpringApplicationAotProcessor 与 ContextAotProcessor
ProcessAotMojo 最终 fork 一个 JVM 去执行处理主类。这个主类在 spring-boot 模块里,叫 org.springframework.boot.SpringApplicationAotProcessor,它的继承链(javap 核实)是:
org.springframework.boot.SpringApplicationAotProcessor
extends org.springframework.context.aot.ContextAotProcessor
extends org.springframework.context.aot.AbstractAotProcessor<ClassName>
关键方法:
SpringApplicationAotProcessor(Class<?>, AbstractAotProcessor.Settings, String[])—— 构造器接收主应用类、groupId/artifactId 等设置、以及传给应用的处理参数。protected GenericApplicationContext prepareApplicationContext(Class<?>)—— 覆写父类抽象方法,把主类包成一个GenericApplicationContext,并注册一个AotProcessorHook来拦截close()。ContextAotProcessor.performAotProcessing(GenericApplicationContext)—— 真正跑装配。ApplicationContextAotGenerator.processAheadOfTime(GenericApplicationContext, GenerationContext)—— 遍历 bean 定义,产出代码。
AbstractAotProcessor.process() 是模板方法,它做了三件可核实的事:
- 处理期间设置系统属性
spring.aot.processing=true(常量AbstractAotProcessor.AOT_PROCESSING,值经javap -c -constants核实为"spring.aot.processing"),处理结束再清掉。这样被处理的容器知道「我现在是在构建期预演,别做真实副作用」。 - 通过
createFileSystemGeneratedFiles()建立输出到磁盘的GeneratedFiles。 - 最后
writeHints(RuntimeHints)把收集到的 hint 写成 native-image 配置。
9.1.4 生成物:源码、native-image 配置、properties
AOT 处理产出三类东西,落在 target/spring-aot/main/ 下:
(1)生成的 Java 源码(sources/)。核心是两类类,命名遵循约定:
| 生成类 | 生成器(jar 核实) | 作用 |
|---|---|---|
<主类>__ApplicationContextInitializer | ApplicationContextInitializationCodeGenerator | 取代运行期的扫描与配置类解析 |
<配置类>__BeanDefinitions | BeanRegistrationsAotContribution / BeanDefinitionsRegistrationGenerator | 每个配置类的 bean 定义与实例供应商 |
BeanRegistrationCodeFragments(spring-beans 核实)定义了生成代码的骨架,它的常量 BEAN_DEFINITION_VARIABLE、INSTANCE_SUPPLIER_VARIABLE 就是生成方法里那两个局部变量名;getTarget(RegisteredBean) 决定每个 bean 的注册代码写进哪个 __BeanDefinitions 类。BeanDefinitionMethodGenerator 则负责把一个 RegisteredBean 翻成一个 @Bean 工厂方法调用。
(2)native-image 配置(resources/)。RuntimeHintsWriter(org.springframework.aot.nativex 包)把收集到的 RuntimeHints 写成 GraalVM 能读的文件,落到:
META-INF/native-image/<groupId>/<artifactId>/reflect-config.json
META-INF/native-image/<groupId>/<artifactId>/resource-config.json
META-INF/native-image/<groupId>/<artifactId>/proxy-config.json
META-INF/native-image/<groupId>/<artifactId>/serialization-config.json
(3)native-image.properties。ContextAotProcessor 里可核实的字符串显示,它会往 META-INF/native-image/ 写一个 native-image.properties,内容由 getDefaultNativeImageArguments(String) 生成。字节码里能看到它至少包含 --no-fallback 与 -H:Class=<主类> 两项——前者要求「要么成功构建原生镜像,要么失败」,不允许回退到 JVM 解释执行;后者告诉 native-image 入口类是谁。
示例输出:构建后
target/spring-aot/main/的大致结构(路径常量已核实,具体文件列表随应用而定):
target/spring-aot/
└── main/
├── sources/
│ └── com/example/library/
│ ├── LibraryApplication__ApplicationContextInitializer.java
│ ├── LibraryApplication__BeanDefinitions.java
│ └── ...
├── resources/
│ └── META-INF/native-image/com.example/library/
│ ├── reflect-config.json
│ ├── resource-config.json
│ └── native-image.properties
└── classes/ # 编译后的生成类,打进最终 jar
9.1.5 生成的装配代码与运行期装配的差异
理解了生成器,再看生成代码就有感觉了。运行期装配靠 ConfigurationClassPostProcessor 反射解析 @Configuration,AOT 装配则把每个 @Bean 方法翻成一个静态的注册调用。形态大致如下:
示例输出(生成代码的具体形态由生成器与 JavaPoet 决定,此处为按
BeanDefinitionMethodGenerator语义整理的示意,非本机实测):
// target/spring-aot/main/sources/com/example/library/
// LibraryApplication__BeanDefinitions.java(示意)
final class LibraryApplication__BeanDefinitions {
// 每个 @Bean 方法对应一个 BeanInstanceSupplier
// 每个 bean 定义对应一次 registerBeanDefinition 调用
static BeanDefinitionRegistrar registerBeanDefinitions() {
// 由 BeanRegistrationsAotContribution 生成的注册逻辑
return null; // 示意,实际返回填充好的 registrar
}
}
与运行期装配的差别可以列成一张表:
| 维度 | 运行期装配(JVM 默认) | AOT 装配(生成物) |
|---|---|---|
| 触发者 | ConfigurationClassPostProcessor | AotApplicationContextInitializer |
| 扫描 | 反射读 classpath 注解 | 构建期扫描结果写死 |
@Bean 调用 | 反射调用配置类方法 | 生成的直接方法调用 |
| 注入 | AutowiredAnnotationBeanPostProcessor 反射注入 | 生成的 BeanInstanceSupplier 装配 |
| 条件注解 | 运行期 Condition 求值 | 构建期求值、结果固化 |
最后一行是理解 AOT 的关键:@Conditional 在构建期就被求值了。这意味着「运行期环境变了、条件本该翻转」的场景在 AOT 下不会发生——条件结果已经写进生成代码。对 profile 相关条件尤其要注意:构建时激活的 profile 决定了生成物,运行期改 profile 不改变已固化的条件分支。这也是为什么有些自动配置在原生镜像里「没生效」,根因不在 hint,而在条件求值时机。
9.1.6 SpringApplication 为什么在 AOT 模式跳过反射
SpringApplication.run() 里有一个分叉点:如果检测到「应该用生成物」,就不再走常规的 refresh() 扫描路径,而是让 AotApplicationContextInitializer 直接把 __BeanDefinitions 注册进来。
判断逻辑在 org.springframework.aot.AotDetector(spring-core 核实):
public abstract class AotDetector {
public static final String AOT_ENABLED = "spring.aot.enabled";
public static boolean useGeneratedArtifacts();
}
useGeneratedArtifacts() 的字节码(javap -c 核实)是「inNativeImage 或 spring.aot.enabled 为真」。其中 inNativeImage 在静态初始化时通过 NativeDetector.inNativeImage(Context.RUN, Context.BUILD) 求得。也就是说:
- 在真正的原生镜像里运行,
NativeDetector.inNativeImage()为真,自动走生成物; - 在 JVM 上想验证 AOT 生成物,可以加
-Dspring.aot.enabled=true强制走生成路径。
如果 AOT 模式开着却找不到生成物,启动会抛 AotInitializerNotFoundException(org.springframework.boot 包,jar 核实),并且有一个专门的 AotInitializerNotFoundFailureAnalyzer 给出人话提示。这解释了「为什么本地跑得好好的 jar,加了 -Dspring.aot.enabled=true 反而起不来」——因为它去找一个并不存在的 __ApplicationContextInitializer。
AotApplicationContextInitializer 的两个静态方法(javap 核实)说明了挂载方式:
static <C extends ConfigurableApplicationContext> AotApplicationContextInitializer<C>
forInitializerClasses(String... initializerClassNames);
static ApplicationContextInitializer<C> instantiateInitializer(String, ClassLoader);
运行期它按类名实例化生成类,不再依赖 @ComponentScan 去反射读注解。
9.1.7 扩展点:两个 *AotProcessor 接口
这里要澄清一个常见误解:Spring 并没有一个叫 @AotProcessor 的注解。AOT 的扩展点是两个接口,靠 META-INF/spring/aot.factories 这个服务文件注册(不是 spring.factories)。从 spring-boot-4.1.1.jar 的 META-INF/spring/aot.factories 可以直接看到三类键:
org.springframework.aot.hint.RuntimeHintsRegistrar=\
org.springframework.boot.ApplicationProperties$ApplicationPropertiesRuntimeHints,\
...
org.springframework.beans.factory.aot.BeanFactoryInitializationAotProcessor=\
org.springframework.boot.context.properties.ConfigurationPropertiesBeanFactoryInitializationAotProcessor,\
...
org.springframework.beans.factory.aot.BeanRegistrationAotProcessor=\
org.springframework.boot.context.properties.ConfigurationPropertiesBeanRegistrationAotProcessor
两个处理接口的签名(javap 核实):
// 针对「单个 bean 定义」:可改注册代码,或把 bean 排除出 AOT 处理
public interface BeanRegistrationAotProcessor {
String IGNORE_REGISTRATION_ATTRIBUTE = "...";
BeanRegistrationAotContribution processAheadOfTime(RegisteredBean registeredBean);
default boolean isBeanExcludedFromAotProcessing();
}
// 针对「整个 bean 工厂」:在所有定义就绪后、实例化前介入
public interface BeanFactoryInitializationAotProcessor {
BeanFactoryInitializationAotContribution processAheadOfTime(ConfigurableListableBeanFactory beanFactory);
}
Spring Boot 自己就用这两个接口:ConfigurationPropertiesBeanRegistrationAotProcessor 为 @ConfigurationProperties 类补上绑定所需的反射 hint;ConfigurationPropertiesBeanFactoryInitializationAotProcessor 则把 ConfigurationPropertiesReflectionHintsContribution 加进最终 hint。
想自定义生成代码(例如给某个 bean 换一套注册片段),实现 BeanRegistrationCodeFragments 并包一层 BeanRegistrationCodeFragmentsDecorator 是标准做法;BeanRegistrationCodeFragments 的 getTarget、generateNewBeanDefinitionCode、generateInstanceSupplierCode 等方法(javap 核实)就是切入点。
9.1.8 构建日志里能看到什么
示例输出(本机无 GraalVM,以下为按 Mojo 行为整理的示意,非本机实测):
[INFO] --- spring-boot:4.1.1:process-aot (default) @ library ---
[INFO] Using auto-detected main class: com.example.library.LibraryApplication
[INFO] Generating AOT source files in /path/target/spring-aot/main/sources
[INFO] Generating AOT resource files in /path/target/spring-aot/main/resources
[INFO] AOT processing completed in 2.3s
要看得更细,可以把 Maven 的日志级别调高(-X)观察 ProcessAotMojo fork 出的子进程命令,那里能看到传给处理主类的 spring-boot.aot.main-class 等参数——这个属性名也是从 ProcessAotMojo 字节码里核实到的。
9.1.9 知道之后能做什么
排障定位。 原生镜像里 bean 没注册、ClassNotFoundException、注入拿到 null,第一站都是 target/spring-aot/main/sources/ 里那个 __BeanDefinitions 类:它写明了每个 bean 的定义从哪来。如果某个 bean 根本没出现在生成代码里,说明它在构建期没被容器「看到」。
强制在 JVM 上验证生成物。 用 -Dspring.aot.enabled=true 在普通 JVM 上跑生成的初始化器,能在不装 GraalVM 的前提下暴露一批 AOT 相关问题(比如某个 hint 缺失导致的反射失败)。这是把「原生镜像构建慢」这个反馈环缩短的关键手段。
写扩展点。 需要为自定义 bean 补生成逻辑,就实现 BeanRegistrationAotProcessor 并注册进 META-INF/spring/aot.factories;需要改整个工厂级别的生成物,用 BeanFactoryInitializationAotProcessor。两者都只在构建期被调用,运行期零成本。
小结
- Spring 的 AOT 是「构建期把装配预演一遍并固化成代码」,与 GraalVM 的
native-image编译是两件事,前者是后者的前置。 process-aot绑定在prepare-package阶段,实现类ProcessAotMojo,产物落在target/spring-aot/main/{sources,resources,classes}。- 入口链是
SpringApplicationAotProcessor→ContextAotProcessor→AbstractAotProcessor,核心生成器是ApplicationContextAotGenerator。 AotDetector.useGeneratedArtifacts()决定是否跳过反射扫描;-Dspring.aot.enabled=true可在 JVM 上强制验证生成物。- 扩展点是
BeanRegistrationAotProcessor与BeanFactoryInitializationAotProcessor两个接口(不存在@AotProcessor注解),经META-INF/spring/aot.factories注册。
下一节进入 hint 层:为什么光有生成代码还不够,反射、资源与动态代理在原生镜像里各自需要怎样声明。
阅读导航:上一节:8.3 方法安全与上下文传播 · 下一节:9.2 反射与资源配置 。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。