跳到主要内容

高级原生镜像主题

DeepSeek V3 中英对照 Advanced Native Images Topics

嵌套配置属性

反射提示由 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;
}

}
java

其中 Nested 是:

public class Nested {

private int number;

// getters / setters...

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

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

}
java

上面的示例生成了 my.properties.namemy.properties.nested.number 的配置属性。如果在 nested 字段上没有使用 @NestedConfigurationProperty 注解,my.properties.nested.number 属性在原生镜像中将无法绑定。你也可以在 getter 方法上添加该注解。

在使用构造函数绑定时,你需要使用 [@NestedConfigurationProperty](https://docs.spring.io/spring-boot/3.4.2/api/java/org/springframework/boot/context/properties/NestedConfigurationProperty.html) 注解来标记字段:

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;
}

}
java

在使用记录(records)时,必须使用 [@NestedConfigurationProperty](https://docs.spring.io/spring-boot/3.4.2/api/java/org/springframework/boot/context/properties/NestedConfigurationProperty.html) 注解来标注参数:

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) {

}
java

在使用 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
)
kotlin
备注

请在所有情况下使用公共的 getter 和 setter 方法,否则属性将无法绑定。

转换 Spring Boot 可执行 Jar

可以将 Spring Boot 可执行 jar 文件 转换为原生镜像,只要该 jar 文件包含 AOT 生成的资源。这在很多情况下都非常有用,包括:

  • 你可以保留你的常规 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-jammy-java-tiny \
--path target/myproject-0.0.1-SNAPSHOT.jar \
--env 'BP_NATIVE_IMAGE=true' \
my-application:0.0.1-SNAPSHOT
shell
备注

你不需要在本地安装 GraalVM 就可以通过这种方式生成镜像。

一旦 pack 完成,你可以使用 docker run 启动应用程序:

$ docker run --rm -p 8080:8080 docker.io/library/myproject:0.0.1-SNAPSHOT
shell

使用 GraalVM native-image

另一个将经过 AOT 处理的 Spring Boot 可执行 jar 文件转换为本地可执行文件的方法是使用 GraalVM 的 native-image 工具。为此,你需要在机器上安装 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 ../
shell
备注

这些命令适用于 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
shell

现在,你可以执行想要获取提示的代码路径,然后使用 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);
}

}
java

然后,你可以在任何 @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);
}

}
java

静态提供提示

如果你愿意,可以在一个或多个 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 维基中的 Spring Boot with GraalVM 页面。你也可以在 GitHub 上向 spring-aot-smoke-tests 项目贡献问题,该项目用于确认常见的应用程序类型是否按预期工作。

如果你发现某个库与 GraalVM 不兼容,请在 reachability metadata 项目 上提交一个问题。