高级原生镜像主题
嵌套配置属性
Spring 提前编译引擎会自动为配置属性创建反射提示。然而,对于非内部类的嵌套配置属性,必须使用 @NestedConfigurationProperty 注解,否则它们将无法被检测到,也无法进行绑定。
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.NestedConfigurationProperty;
@ConfigurationProperties("my.properties")
public class MyProperties {
private String name;
@NestedConfigurationProperty
private final Nested nested = new Nested();
// getters / setters...
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
public Nested getNested() {
return this.nested;
}
}
其中 Nested 是:
- Java
- Kotlin
public class Nested {
private int number;
// getters / setters...
public int getNumber() {
return this.number;
}
public void setNumber(int number) {
this.number = number;
}
}
class Nested {
}
上面的示例为 my.properties.name 和 my.properties.nested.number 生成了配置属性。如果在 nested 字段上没有 @NestedConfigurationProperty 注解,则 my.properties.nested.number 属性在原生镜像中将无法绑定。你也可以对 getter 方法进行注解。
使用构造函数绑定时,必须使用 @NestedConfigurationProperty 注解来标注字段:
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.NestedConfigurationProperty;
@ConfigurationProperties("my.properties")
public class MyPropertiesCtor {
private final String name;
@NestedConfigurationProperty
private final Nested nested;
public MyPropertiesCtor(String name, Nested nested) {
this.name = name;
this.nested = nested;
}
// getters / setters...
public String getName() {
return this.name;
}
public Nested getNested() {
return this.nested;
}
}
使用记录类时,你必须使用 @NestedConfigurationProperty 注解该参数:
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.NestedConfigurationProperty;
@ConfigurationProperties("my.properties")
public record MyPropertiesRecord(String name, @NestedConfigurationProperty Nested nested) {
}
在使用 Kotlin 时,你需要使用 @NestedConfigurationProperty 注解标注数据类的参数:
import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.boot.context.properties.NestedConfigurationProperty
@ConfigurationProperties("my.properties")
data class MyPropertiesKotlin(
val name: String,
@NestedConfigurationProperty val nested: Nested
)
请在所有情况下使用公共的 getter 和 setter,否则属性将无法绑定。
转换 Spring Boot 可执行 Jar
只要 jar 包含 AOT 生成的资源,就可以将 Spring Boot 可执行 jar 转换为原生镜像。这可能出于多种原因非常有用,包括:
-
你可以在 CI/CD 平台上保留常规的 JVM 流水线,并将 JVM 应用转换为原生镜像。
-
由于
native-image不支持交叉编译,你可以保留一个与操作系统无关的部署产物,稍后再将其转换为不同的操作系统架构。
你可以使用 Cloud Native Buildpacks 将 Spring Boot 可执行 jar 转换为原生镜像,或者使用随 GraalVM 附带的 native-image 工具。
您的可执行 jar 必须包含 AOT 生成的资源,例如生成的类和 JSON 提示文件。
使用 Buildpacks
Spring Boot 应用程序通常通过 Maven(mvn spring-boot:build-image)或 Gradle(gradle bootBuildImage)集成使用 Cloud Native Buildpacks。不过,你也可以使用 pack 将经过 AOT 处理的 Spring Boot 可执行 jar 转换为原生容器镜像。
你必须使用至少 JDK 25 来构建你的应用程序,因为 Buildpacks 使用的 GraalVM native-image 版本与编译时所用的 Java 版本相同。
首先,请确保 Docker 守护进程可用(更多详情请参见 Get Docker)。如果你使用的是 Linux,请配置 Docker 以允许非 root 用户操作。
你还需要按照 buildpacks.io 上的安装指南 安装 pack。
假设一个经过 AOT 处理的 Spring Boot 可执行 jar 文件(构建为 myproject-0.0.1-SNAPSHOT.jar)位于 target 目录中,运行:
$ pack build --builder paketobuildpacks/builder-noble-java-tiny \
--path target/myproject-0.0.1-SNAPSHOT.jar \
--env 'BP_NATIVE_IMAGE=true' \
my-application:0.0.1-SNAPSHOT
您无需本地安装 GraalVM 即可通过此方式生成镜像。
一旦 pack 完成,你可以使用 docker run 启动应用程序:
$ docker run --rm -p 8080:8080 docker.io/library/myproject:0.0.1-SNAPSHOT
使用 GraalVM native-image
将经过 AOT 处理的 Spring Boot 可执行 jar 转换为原生可执行文件的另一种选择是使用 GraalVM 的 native-image 工具。为此,您需要在机器上安装 GraalVM 发行版。您可以手动从 Liberica Native Image Kit 页面 下载,或者使用 SDKMAN! 等下载管理器。
假设一个经过 AOT 处理的 Spring Boot 可执行 jar 文件(构建为 myproject-0.0.1-SNAPSHOT.jar)位于 target 目录中,运行:
$ rm -rf target/native
$ mkdir -p target/native
$ cd target/native
$ jar -xvf ../myproject-0.0.1-SNAPSHOT.jar
$ native-image -H:Name=myproject @META-INF/native-image/argfile -cp .:BOOT-INF/classes:`find BOOT-INF/lib | tr '\n' ':'`
$ mv myproject ../
这些命令适用于 Linux 或 macOS 机器,但你需要针对 Windows 进行调整。
@META-INF/native-image/argfile 可能不会被打包到你的 jar 中。只有在需要覆盖可达性元数据时才会包含它。
native-image 的 -cp 标志不接受通配符。你需要确保列出所有 jar 文件(上述命令使用 find 和 tr 来实现这一点)。
使用 Tracing Agent
GraalVM 原生镜像的 tracing agent 可以拦截 JVM 上的反射、资源或代理使用情况,从而生成相关的提示。Spring 应该能自动生成大部分此类提示,但 tracing agent 可用于快速识别缺失的条目。
当使用 agent 为 native image 生成 hints 时,有几种方法:
-
直接启动应用程序并进行操作。
-
运行应用程序测试以对应用程序进行操作。
第一种选项在 Spring 无法识别某个库或模式时,有助于识别缺失的提示。
第二种选项对于可重复的设置来说更具吸引力,但默认情况下,生成的提示将包含测试基础设施所需的所有内容。其中一些在应用程序实际运行时是不必要的。为了解决这个问题,agent 支持一个 access-filter 文件,该文件会使某些数据从生成的输出中被排除。
直接启动应用程序
使用以下命令启动应用程序,并附加 native image tracing agent:
$ java -Dspring.aot.enabled=true \
-agentlib:native-image-agent=config-output-dir=/path/to/config-dir/ \
-jar target/myproject-0.0.1-SNAPSHOT.jar
现在你可以执行你希望获得提示的代码路径,然后使用 ctrl-c 停止应用程序。
在应用程序关闭时,原生镜像追踪代理会将提示文件写入指定的配置输出目录。你可以手动检查这些文件,也可以将它们作为原生镜像构建过程的输入。要将它们用作输入,请将它们复制到 src/main/resources/META-INF/native-image/ 目录中。下次构建原生镜像时,GraalVM 会将这些文件纳入考虑。
在原生镜像追踪代理上,还可以设置更高级的选项,例如根据调用者类过滤记录的提示等。更多详情,请参阅官方文档。
自定义提示
如果你需要为反射、资源、序列化、代理使用等提供自己的提示,可以使用 RuntimeHintsRegistrar API。创建一个实现 RuntimeHintsRegistrar 接口的类,然后对提供的 RuntimeHints 实例进行相应的调用:
import java.lang.reflect.Method;
import org.springframework.aot.hint.ExecutableMode;
import org.springframework.aot.hint.RuntimeHints;
import org.springframework.aot.hint.RuntimeHintsRegistrar;
import org.springframework.util.ReflectionUtils;
public class MyRuntimeHints implements RuntimeHintsRegistrar {
@Override
public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
// Register method for reflection
Method method = ReflectionUtils.findMethod(MyClass.class, "sayHello", String.class);
hints.reflection().registerMethod(method, ExecutableMode.INVOKE);
// Register resources
hints.resources().registerPattern("my-resource.txt");
// Register serialization
hints.serialization().registerType(MySerializableClass.class);
// Register proxy
hints.proxies().registerJdkProxy(MyInterface.class);
}
}
然后,你可以在任何 @Configuration 类(例如你的 @SpringBootApplication 注解的应用程序类)上使用 @ImportRuntimeHints 来激活这些提示。
如果你有需要绑定的类(通常在序列化或反序列化 JSON 时需要),可以在任意 Bean 上使用 @RegisterReflectionForBinding。大多数提示会自动推断,例如在 @RestController 方法中接收或返回数据时。但当你直接使用 WebClient、RestClient 或 RestTemplate 时,可能需要使用 @RegisterReflectionForBinding。
测试自定义提示
RuntimeHintsPredicates API 可用于测试你的提示。该 API 提供了构建 Predicate 的方法,可用于测试 RuntimeHints 实例。
如果你使用 AssertJ,你的测试将如下所示:
import org.junit.jupiter.api.Test;
import org.springframework.aot.hint.RuntimeHints;
import org.springframework.aot.hint.predicate.RuntimeHintsPredicates;
import org.springframework.boot.docs.packaging.nativeimage.advanced.customhints.MyRuntimeHints;
import static org.assertj.core.api.Assertions.assertThat;
class MyRuntimeHintsTests {
@Test
void shouldRegisterHints() {
RuntimeHints hints = new RuntimeHints();
new MyRuntimeHints().registerHints(hints, getClass().getClassLoader());
assertThat(RuntimeHintsPredicates.resource().forResource("my-resource.txt")).accepts(hints);
}
}
静态提供提示
如果你愿意,也可以在一个或多个 GraalVM JSON 提示文件中静态提供自定义提示。这些文件应放置在 src/main/resources/ 目录下的 META-INF/native-image/*/*/ 目录中。AOT 处理期间生成的提示 会被写入名为 META-INF/native-image/{groupId}/{artifactId}/ 的目录。请将你的静态提示文件放置在一个不与此位置冲突的目录中,例如 META-INF/native-image/{groupId}/{artifactId}-additional-hints/。
已知限制
GraalVM 原生镜像是一项不断发展的技术,并非所有库都提供支持。GraalVM 社区正在通过为尚未自带支持的项目提供 reachability metadata 来提供帮助。Spring 本身不包含第三方库的提示,而是依赖于 reachability metadata 项目。
如果你在为 Spring Boot 应用程序生成原生镜像时遇到问题,请查看 Spring Boot Wiki 的 Spring Boot with GraalVM 页面。你也可以向 GitHub 上的 spring-aot-smoke-tests 项目提交问题,该项目用于确认常见应用程序类型是否按预期工作。
如果你发现某个库无法与 GraalVM 一起使用,请在 reachability metadata 项目 上提交 issue。