原生镜像高级主题

嵌套配置属性

Spring 的提前处理(AOT)引擎会自动为配置属性生成反射提示。 但对于不是内部类的嵌套配置属性,*必须*使用 @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 注解,则 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;
	}

}

使用 record 时,需为参数添加 @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
)
请务必在所有场景下提供 public 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 工具。

假设已在 target 目录下构建出 AOT 处理后的 Spring Boot 可执行 jar(如 myproject-0.0.1-SNAPSHOT.jar),可执行如下命令:

$ 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 --rm -p 8080:8080 docker.io/library/myproject:0.0.1-SNAPSHOT

使用 GraalVM native-image

另一种方式是使用 GraalVM 的 native-image 工具将 AOT 处理后的 Spring Boot 可执行 jar 转为原生可执行文件。 你需要在本地安装 GraalVM,可以在 Liberica Native Image Kit 页面 手动下载,或使用 SDKMAN! 等下载管理器。

假设已在 target 目录下构建出 AOT 处理后的 Spring Boot 可执行 jar(如 myproject-0.0.1-SNAPSHOT.jar),可执行如下命令:

$ 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,仅在需要 reachability metadata 覆盖时才包含。
native-image-cp 参数不支持通配符,需确保所有 jar 均被列出(上述命令用 findtr 实现)。

使用 Tracing Agent

GraalVM 原生镜像 tracing agent 可拦截 JVM 上的反射、资源或代理使用,以生成相关提示。 Spring 通常会自动生成大部分提示,但 tracing agent 可用于快速识别缺失项。

使用 agent 生成原生镜像提示有两种方式:

  • 直接启动应用并手动操作

  • 运行应用测试以覆盖代码路径

第一种方式适用于识别 Spring 未能识别的库或模式所需的提示。

第二种方式更适合可复现的场景,但默认生成的提示会包含测试基础设施所需的内容,部分内容在实际运行时并不需要。 为解决此问题,agent 支持 access-filter 文件,可排除部分数据。

直接启动应用

使用如下命令,附加 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 停止应用。

应用关闭时,tracing agent 会将提示文件写入指定的 config 输出目录。 你可以手动检查这些文件,或将其作为原生镜像构建的输入。 如需作为输入,将其复制到 src/main/resources/META-INF/native-image/ 目录。 下次构建原生镜像时,GraalVM 会自动读取这些文件。

tracing agent 还支持更多高级选项,例如按调用类过滤记录的提示等。 详见 官方文档

自定义提示

如需为反射、资源、序列化、代理等提供自定义提示,可实现 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.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 with GraalVM 页面。 你也可以在 spring-aot-smoke-tests 项目提交问题,该项目用于验证常见应用类型的兼容性。

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