启动参数出现 red,容器里的 Bean 仍然是 blue

配置类定义两个同类型 Bean:@Profile("blue") 返回 blue,@Profile("red") 返回 red。普通 JVM 分别带 blue、red 启动,容器里也分别出现对应 Bean。用 blue 生成 Spring AOT 产物后,再给 AOT JVM 传 red,实验仍取得 blue Bean。

日志中有效 profile 是 blue,red,这并不矛盾。环境属性仍可在启动时参与处理,已经生成的 Bean 注册代码却不会重新执行一次任意的配置类发现与条件选择。把“Environment 中出现了某个值”当作“容器根据该值重建了 Bean 集合”,会误判 AOT 应用的装配行为。

实验固定 Framework 6.2.11、Boot 3.5.6 与 JDK 21。final-lab/AotApp 是独立的非 Web 入口,避免把数据库连接和服务器启动引入 AOT 基础实验;它与订单入口共享 Maven 模块和冻结依赖。普通 JVM、生成代码、AOT JVM 均已实际执行,native image 单独标记为 NOT_RUN。

条件解析、生成代码与三种运行产物

Spring AOT 提前执行的是容器装配决策

普通容器启动需要解析配置类、处理导入、识别 Bean 方法、评估条件并注册 BeanDefinition。Spring AOT 将其中可在构建阶段确定的部分转成生成代码。运行时可以直接应用这些注册结果,减少重复发现与分析。

固定版本 ApplicationContextAotGenerator.processAheadOfTime 首先调用 refreshForAotProcessing,再建立初始化代码生成器,收集 BeanFactory 初始化阶段的 AOT 贡献。返回值指向生成的 ApplicationContextInitializer 类型。它描述如何恢复优化后的上下文初始状态,而不是把正在运行的整个 JVM 内存保存成快照。固定版本生成器源码

构建时刷新也不等于完整业务启动。其主要目标是 Bean 定义和可分析元数据;AOT 处理器、相关基础设施及其必要依赖可能实例化,普通业务 Bean 不应被假设为已经全部运行过初始化逻辑。因此,构建阶段能成功生成代码,不能证明连接池取得了连接、远端接口可访问或关闭回调会正确释放资源。

声明类型也会影响分析能力。@Bean 返回一个过于宽泛的 Object,而实际创建某个带注解、代理接口或生命周期能力的具体类型,会让构建分析缺少信息。对于需要 AOT 分析的注册,保留可解析的 BeanDefinition 与具体返回类型,比在运行过程中任意注册已创建单例更容易表达完整语义。AOT 容器约束

本地生成的文件究竟包含什么

本例 Maven 插件显式指定 AotApp 为 mainClass,构建 profile 固定为 blue。执行 spring-boot:process-aot 后,target/spring-aot/main/sources 出现 AotApp__ApplicationContextInitializer、AotApp__BeanFactoryRegistrations 与 AotApp__BeanDefinitions 等源码。

初始化器负责把生成的工厂注册逻辑应用到 GenericApplicationContext。BeanDefinitions 中保留用于创建 blue Bean 的定义;BeanFactoryRegistrations 将这些定义放进 BeanFactory。检查源码可以看到注册路径,但还需要编译并运行相同产物,才能确认这些类真的进入最终应用包并参与启动。

实验先运行普通 jar,再执行 AOT 处理与 package,最后用 -Dspring.aot.enabled=true 运行重新打包的 jar。缺少这个开关时,应用仍可能走普通启动路径,单凭目录里存在生成文件不能证明 AOT JVM 已运行。日志明确打印 AOT_ENABLED=true,同时对 Bean、资源和反射结果进行断言。

生成的 Java、class 与 native metadata 被复制到 evidence/42/local-20261002/generated/;最终 jar 保存在模块 target 中,证据记录其 SHA-256。生成目录是观测对象,修改业务配置时应该重新生成,不能直接手工修生成代码后把它当作源代码修复。

profile 决定 Bean 集合,普通属性仍有运行空间

blue 与 red 是两个互斥的装配选择:它们决定注册哪个 Marker Bean。普通 JVM 每次启动都会重新处理这个选择,因此四次运行中的前两次分别得到 blue、red。AOT 构建时使用 blue,后两次运行则都得到 blue。

后一次 AOT 运行仍传入 --spring.profiles.active=red。Boot 生成的环境处理逻辑保留构建所需 profile,实际日志出现 blue,red。实验只据此说明当前冻结版本和配置组合的观测结果;不能把该字符串推广为所有 Spring AOT 应用都会采用的 profile 合并规则。

同一次运行还传入 --lab.expected=blue,主程序通过 Environment 读取该属性作为断言的期待值。该属性不改变 Bean 是否存在,因此可以在运行时取得。对数据库地址、业务阈值等属性,也应分别检查它们只是改变已存在对象的配置,还是影响条件装配、类型或 Bean 数量。

@ConditionalOnProperty 一旦决定某个 Bean 是否注册,就属于后者。把这类属性留到部署时才决定,而构建阶段没有对应 Bean 定义,运行时不能期待它自动补出整套装配。环境配置需要按“影响对象值”与“影响对象集合”逐项判断,不能统称为配置都被冻结或配置都可变。Boot 的 AOT JVM 运行限制

RuntimeHints 表达动态访问需求

静态分析不能自动知道每次反射、资源查找或动态代理最终会使用哪些目标。RuntimeHints 为这些操作提供元数据。它不是允许任意反射的全局开关,也不是应用运行时替业务代码执行反射的工具。

本例 Hints 注册 lab-aot-message.txt 资源模式,并为 Payload 注册公开构造器和公开方法调用需求。应用实际通过 ClassPathResource 打开资源,通过 getConstructor().newInstance() 创建对象,再反射调用 value 方法。四次 JVM 运行都断言返回 resource-ok 和 reflection-ok。

构建生成的 resource-config.json 与 reflect-config.json 中也分别包含资源名和 Payload 类型。这个检查证明注册器参与生成且相关声明写入了输出;它没有证明这些声明已经覆盖应用中所有动态访问,也没有证明删除 hints 会让普通 JVM 失败。

普通 JVM 本身具备反射与 classpath 资源能力,缺失 native metadata 通常不会让这里的 JVM 演示立刻失败。即使 AOT JVM 成功执行,也不能据此声称 native image 的封闭世界分析已经通过。两种运行方式的限制不同,证据必须分开保存。RuntimeHints 固定接口

三种产物的验收记录

对象 实际操作 已观察结果 未包含的证明
普通 JVM jar blue、red 分别启动 Bean 随 profile 改变,资源和反射成功 AOT 初始化器是否可用
AOT JVM jar 生成、编译、打包后启用 AOT 两次均为 blue,生成元数据存在 native 封闭世界分析与本地机器码行为
native image 当前工具探测 PATH 与选定 JDK 无 native-image,NOT_RUN 构建、启动、资源、反射及性能全部未验收

本例共有 18 条具名检查:四次实际 JVM 运行各检查四项,另检查生成 Java 与 hints 输出两项。没有测量冷启动分布、峰值内存或吞吐量,日志中的单次启动耗时不构成性能比较。生成源代码也不意味着 Java JIT 被禁用;AOT JVM 仍然运行在 JVM 上。

native image 工具缺失是本次明确的验证边界。不能从当前 JVM 结果推断 native 的二进制可运行,更不能把 native 的构建失败预先归因于某个 hint。获得工具后仍需执行实际构建、运行和资源反射检查,保留新的产物与日志。

运行命令与修改练习

从系列实验工程根目录执行:

1
2
export JAVA_HOME=/path/to/jdk-21
python3 final-lab/verify.py 42

脚本先 package 普通 jar 并运行两次,再 process-aot、package,最后运行两次 AOT JVM。每个命令单独保留输出和退出码,合并日志位于 evidence/42/local-20261002/run.txt。AOT 步骤的核心命令可以独立复现:

1
2
3
4
./mvnw -f "$PWD/final-lab/pom.xml" -DskipTests spring-boot:process-aot package
"$JAVA_HOME/bin/java" -Dspring.aot.enabled=true \
-jar final-lab/target/final-lab-1.0-SNAPSHOT.jar \
--spring.profiles.active=red --lab.expected=blue

把 pom 中构建 profile 从 blue 改为 red,清理本模块 target 后重新生成,再把两个 AOT 运行的 lab.expected 改为 red。预期生成代码注册 red,运行期传 blue 也不会把定义重新切成 blue。该修改练习未包含在保存的基线运行中;只改启动参数而不重新构建,是另一种实验。

还可以删除 Hints 中资源注册行后重新生成,检查 resource-config 是否缺少该资源模式。普通 JVM 仍可能读取成功,这正是“JVM 可运行”与“native 元数据足够”之间的差别。此练习不要求凭空宣称 native 失败,缺少实际原生产物时结论应停在生成 metadata 的变化。

参考资料