高级原生镜像主题

嵌套配置属性

Spring Ahead-of-Time 引擎会自动为配置属性创建反射提示。但是,不是内部类的嵌套配置属性**必须**使用@NestedConfigurationProperty注解,否则它们将不会被检测到,并且不可绑定。

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.NestedConfigurationProperty;

@ConfigurationProperties(prefix = "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注解,则my.properties.nested.number属性在原生镜像中将不可绑定。

使用构造函数绑定时,必须使用@NestedConfigurationProperty注解字段

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.NestedConfigurationProperty;

@ConfigurationProperties(prefix = "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(prefix = "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(prefix = "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 守护进程可用(有关更多详细信息,请参阅 获取 Docker)。如果您使用的是 Linux,请 将其配置为允许非 root 用户

您还需要按照 buildpacks.io 上的安装指南 安装 pack

假设 AOT 处理过的 Spring Boot 可执行 JAR 构建为 myproject-0.0.1-SNAPSHOT.jar 并位于 target 目录中,请运行

$ pack build --builder paketobuildpacks/builder-jammy-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 来执行此操作)。

使用跟踪代理

GraalVM 原生镜像 跟踪代理 允许您拦截 JVM 上的反射、资源或代理使用,以便生成相关的提示。Spring 应该会自动生成大多数这些提示,但跟踪代理可用于快速识别缺失的条目。

在使用代理为原生镜像生成提示时,有两种方法:

  • 直接启动应用程序并运行它。

  • 运行应用程序测试以运行应用程序。

第一个选项对于识别 Spring 未识别的库或模式时缺少的提示很有用。

第二个选项对于可重复设置听起来更吸引人,但默认情况下,生成的提示将包括测试基础设施所需的任何内容。当应用程序实际运行时,其中一些将是不必要的。为了解决这个问题,代理支持一个访问过滤器文件,该文件将导致某些数据从生成的输出中排除。

直接启动应用程序

使用以下命令启动带有附加的原生镜像跟踪代理的应用程序:

$ 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 方法的数据时。但是,当您直接使用 WebClientRestClientRestTemplate 时,您可能需要使用 @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.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 原生镜像是一项不断发展的技术,并非所有库都提供支持。GraalVM 社区正在通过为尚未提供自身可达性元数据的项目提供 可达性元数据 来提供帮助。Spring 本身不包含针对第三方库的提示,而是依赖于可达性元数据项目。

如果您在为 Spring Boot 应用程序生成原生镜像时遇到问题,请查看 Spring Boot wiki 的 Spring Boot 与 GraalVM 页面。您也可以在 GitHub 上为 spring-aot-smoke-tests 项目贡献问题,该项目用于确认常见的应用程序类型按预期工作。

如果您发现某个库无法与 GraalVM 一起使用,请在 可达性元数据项目 上提出问题。