跳到主要内容
版本:3.5.10

高级原生镜像主题

QWen Max 中英对照 Advanced Native Images Topics

嵌套配置属性

Spring 的预先(ahead-of-time)引擎会自动为配置属性创建反射提示(reflection hints)。然而,对于不是内部类的嵌套配置属性,必须使用 @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 是:

public class Nested {

private int number;

// getters / setters...

public int getNumber() {
return this.number;
}

public void setNumber(int number) {
this.number = number;
}

}

上面的示例会生成 my.properties.namemy.properties.nested.number 的配置属性。如果 nested 字段上没有 @NestedConfigurationProperty 注解,那么在原生镜像(native image)中 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;
}

}

使用记录类(records)时,你必须使用 @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 转换为原生镜像。这样做有很多好处,包括:

  • 你可以保留常规的 JVM 流水线,并在 CI/CD 平台上将 JVM 应用程序转换为原生镜像。

  • 由于 native-image 不支持交叉编译,你可以保留一个与操作系统无关的部署制品,稍后再将其转换为不同操作系统的架构。

你可以使用 Cloud Native Buildpacks 或随 GraalVM 一起提供的 native-image 工具,将 Spring Boot 可执行 jar 转换为原生镜像。

备注

你的可执行 JAR 必须包含 AOT 生成的资源,例如生成的类和 JSON 提示文件。

使用 Buildpacks

Spring Boot 应用通常通过 Maven(mvn spring-boot:build-image)或 Gradle(gradle bootBuildImage)集成来使用 Cloud Native Buildpacks。不过,你也可以使用 pack 将经过 AOT 处理的 Spring Boot 可执行 jar 转换为原生容器镜像。

首先,请确保 Docker daemon 可用(更多详情请参见 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 文件(上面的命令使用了 findtr 来实现这一点)。

使用 Tracing Agent

GraalVM native image 的 tracing agent 允许你在 JVM 上拦截反射、资源或代理的使用,以生成相关的提示(hints)。Spring 应该能自动生成大部分此类提示,但 tracing agent 可用于快速识别缺失的条目。

在使用 agent 为 native image 生成 hints 时,有几种方法:

  • 直接启动应用程序并进行操作。

  • 运行应用程序测试以对应用程序进行操作。

第一个选项在识别 Spring 未识别的库或模式所缺失的提示时很有用。

第二种选项对于可重复的设置来说听起来更有吸引力,但默认情况下,生成的 hints 会包含测试基础设施所需的所有内容。其中一些在应用程序实际运行时是不必要的。为了解决这个问题,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 停止应用程序。

在应用程序关闭时,native image tracing agent 会将 hint 文件写入指定的配置输出目录。你可以手动检查这些文件,也可以将它们作为 native image 构建过程的输入。要将其用作输入,请将这些文件复制到 src/main/resources/META-INF/native-image/ 目录中。下次构建 native image 时,GraalVM 会考虑这些文件。

有一些更高级的选项可以在 native image tracing agent 上进行设置,例如按调用方类(caller classes)过滤记录的提示(hints)等。更多内容,请参阅官方文档

自定义提示

如果你需要为反射、资源、序列化、代理使用等提供自己的提示(hints),可以使用 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](https://docs.spring.io/spring-framework/docs/6.2.x/javadoc-api/org/springframework/context/annotation/Configuration.html) 类(例如使用 [@SpringBootApplication](https://docs.spring.io/spring-boot/3.5.10/api/java/org/springframework/boot/autoconfigure/SpringBootApplication.html) 注解的应用程序类)上使用 [@ImportRuntimeHints](https://docs.spring.io/spring-framework/docs/6.2.x/javadoc-api/org/springframework/context/annotation/ImportRuntimeHints.html) 来激活这些提示。

如果你有需要绑定的类(主要在序列化或反序列化 JSON 时需要),可以在任意 bean 上使用 @RegisterReflectionForBinding。大多数提示(hints)会被自动推断出来,例如在 @RestController 方法中接收或返回数据时。但当你直接使用 WebClientRestClientRestTemplate 时,可能需要显式使用 @RegisterReflectionForBinding

测试自定义提示

可以使用 RuntimeHintsPredicates API 来测试你的提示(hints)。该 API 提供了一些方法,用于构建一个 Predicate,该 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 hint 文件中静态地提供自定义 hints。这些文件应放置在 src/main/resources/ 目录下的 META-INF/native-image/*/*/ 目录中。在 AOT 处理期间生成的 hints 会被写入名为 META-INF/native-image/{groupId}/{artifactId}/ 的目录中。请将你的静态 hint 文件放置在一个不与该位置冲突的目录中,例如 META-INF/native-image/{groupId}/{artifactId}-additional-hints/

已知限制

GraalVM 原生镜像是一项不断发展的技术,并非所有库都提供支持。GraalVM 社区通过为尚未自带支持的项目提供 reachability metadata 来给予帮助。Spring 本身不包含第三方库的提示(hints),而是依赖于 reachability metadata 项目。

如果你在为 Spring Boot 应用程序生成原生镜像时遇到问题,请查看 Spring Boot Wiki 中的 [Spring Boot with GraalVM](https://github.com/spring-projects/s Spring-boot/wiki/Spring-Boot-with-GraalVM) 页面。你也可以向 GitHub 上的 spring-aot-smoke-tests 项目提交问题,该项目用于确认常见的应用程序类型是否按预期正常工作。

如果你发现某个库无法与 GraalVM 配合使用,请在 reachability metadata 项目 中提交 issue。