高级原生镜像主题

嵌套配置属性

Spring 提前编译引擎会自动为配置属性创建反射提示。但是,非内部类的嵌套配置属性**必须**使用 @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 属性将无法绑定。您也可以注解 getter 方法。

使用构造函数绑定时,您必须用 @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;
	}

}

使用记录 (records) 时,您必须用 @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

只要 Spring Boot 可执行 Jar 包含 AOT 生成的资产,就可以将其转换为原生镜像。这样做有很多原因,其中包括

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

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

您可以使用云原生 Buildpacks 或 GraalVM 附带的 native-image 工具将 Spring Boot 可执行 Jar 转换为原生镜像。

您的可执行 Jar 必须包含 AOT 生成的资产,例如生成的类和 JSON 提示文件。

使用 Buildpacks

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

首先,请确保 Docker daemon 已可用(更多详情请参见获取 Docker)。如果您使用的是 Linux,请配置它以允许非 root 用户

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

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

$ pack build --builder paketobuildpacks/builder-jammy-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 处理并构建为 myproject-0.0.1-SNAPSHOT.jar 的 Spring Boot 可执行 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 的方法,该 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 社区通过为尚未提供自身支持的项目提供可达性元数据来提供帮助。Spring 本身不包含第三方库的提示,而是依赖于可达性元数据项目。

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

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