🧑 博主简介:CSDN博客专家,历代文学网(PC端可以访问:历代文学,移动端可微信小程序搜索“历代文学”)总架构师,
15年
工作经验,精通Java编程
,高并发设计
,Springboot和微服务
,熟悉Linux
,ESXI虚拟化
以及云原生Docker和K8s
,热衷于探索科技的边界,并将理论知识转化为实际应用。保持对新技术的好奇心,乐于分享所学,希望通过我的实践经历和见解,启发他人的创新思维。在这里,我希望能与志同道合的朋友交流探讨,共同进步,一起在技术的世界里不断学习成长。
技术合作请加本人wx(注明来自csdn):foreast_sea
1. 概述
在本文中,我们将了解原生镜像,以及如何从 Spring Boot 应用程序和 GraalVM 的原生镜像构建器创建原生镜像。我们指的是 Spring Boot 3,但我们将在本文末尾解决与 Spring Boot 2 的差异。
2. 原生镜像
本机映像是一种将 Java 代码构建为独立可执行文件的技术。此可执行文件包括应用程序类、来自其依赖项的类、运行时库类以及来自 JDK 的静态链接本机代码。JVM 已打包到本机映像中,因此目标系统上不需要任何 Java 运行时环境,但构建工件取决于平台。因此,我们需要为每个支持的目标系统构建一个版本,当我们使用 Docker 等容器技术时,这会更容易,我们可以将容器构建为可以部署到任何 Docker 运行时的目标系统。
2.1. GraalVM 和原生镜像构建器
通用递归应用程序和算法语言虚拟机 (Graal VM) 是为 Java 和其他 JVM 语言编写的高性能 JDK 发行版,同时支持 JavaScript、Ruby、Python 和其他几种语言。它提供了一个 Native Image 构建器 – 一种从 Java 应用程序构建原生代码并将其与 VM 一起打包成独立可执行文件的工具。它由 Spring Boot Maven 和 Gradle Plugin 官方支持,但有一些例外(最糟糕的是 Mockito 目前不支持原生测试)。
2.2. 特殊功能
在构建原生镜像时,我们遇到了两个典型特征。
预先 (AOT) 编译是将高级 Java 代码编译为本机可执行代码的过程。通常,这是由 JVM 的 Just-in-time 编译器 (JIT) 在运行时完成的,它允许在执行应用程序时进行观察和优化。在 AOT 编译的情况下,此优势将丢失。
通常,在 AOT 编译之前,可以选择有一个单独的步骤,称为 AOT 处理,即从代码中收集元数据并将其提供给 AOT 编译器。划分为这两个步骤是有意义的,因为 AOT 处理可以是特定于框架的,而 AOT 编译器更通用。下图给出了一个概述:
Java 平台的另一个特点是,只需将 JAR 放入 Classpath 中,即可在目标系统上实现可扩展性。由于启动时的反射和注释扫描,我们在应用程序中获得了扩展行为。
遗憾的是,这会减慢启动时间,并且不会带来任何好处,尤其是对于云原生应用程序,其中甚至服务器运行时和 Java 基类也被打包到 JAR 中。因此,我们省去了这个功能,然后可以使用 Closed World Optimization 构建应用程序。
这两项功能都减少了运行时需要执行的工作量。
2.3. 优势
本机映像具有各种优势,例如即时启动和减少内存消耗。它们可以打包到轻量级容器映像中,以便更快、更高效地部署,并且它们减少了攻击面。
2.4. 限制
由于 Closed World Optimization,在编写应用程序代码和使用框架时,我们必须注意一些限制。不久:
- 类初始值设定项可以在构建时执行,以实现更快的启动和更好的峰值性能。但我们必须意识到,这可能会破坏代码中的一些假设,例如,当加载一个必须在构建时可用的文件时。
- 反射和动态代理在运行时成本高昂,因此在 Closed World 假设下在构建时进行了优化。在构建时执行时,我们可以在类初始化器中不受限制地使用它。任何其他用法都必须向 AOT 编译器公布,Native Image 构建器会尝试通过执行静态代码分析来访问该编译器。如果失败,我们必须提供此信息,例如,通过配置文件。
- 这同样适用于所有基于反射的技术,例如 JNI 和 Serialization。
- 此外,本机映像生成器还提供了自己的本机接口,该接口比 JNI 简单得多,开销也较低。
- 对于本机映像构建,字节码在运行时不再可用,因此无法使用针对 JVMTI 的工具进行调试和监控。然后,我们必须使用本机调试器和监控工具。
关于 Spring Boot,我们必须意识到,运行时不再完全支持配置文件、条件 bean 和 .enable 属性等功能。如果我们使用 profiles,则必须在构建时指定它们。
3. 基本设置
在构建原生镜像之前,我们必须安装这些工具。
3.1. GraalVM 和原生镜像
首先,我们按照安装说明安装当前版本的 GraalVM 和原生映像构建器。(Spring Boot 需要 22.3 版本)我们应该确保安装目录可以通过 GRAALVM_HOME 环境变量获得,并且 “<GRAALVM_HOME>/bin” 已添加到 PATH 变量中。
3.2. 原生编译器
在构建过程中,Native Image 构建器会调用特定于平台的原生编译器。因此,我们需要这个原生编译器,按照我们平台的 “Prerequisite” 说明进行操作。这将使构建平台相关。我们必须知道,只能在特定于平台的命令行中运行构建。例如,使用 Git Bash 在 Windows 上运行构建将不起作用。我们需要改用 Windows 命令行。
3.3. Docker
作为先决条件,我们将确保安装 Docker,稍后需要运行本机映像。Spring Boot Maven 和 Gradle 插件使用 Paketo Tiny Builder 构建容器。
4. 使用 Spring Boot 配置和构建项目
将本机构建功能与 Spring Boot 一起使用非常简单。例如,通过使用 Spring Initializr 并添加应用程序代码来创建我们的项目。然后,要使用 GraalVM 的原生映像构建器构建原生映像,我们需要使用 GraalVM 本身提供的 Maven 或 Gradle 插件来扩展我们的构建。
4.1. Maven 浏览器
Spring Boot Maven 插件的目标是 AOT 处理(即,不是 AOT 编译自身,而是为 AOT 编译器收集元数据,例如,在代码中注册反射的使用)和构建可与 Docker 一起运行的 OCI 映像。我们可以直接调用这些目标:
mvn spring-boot:process-aot
mvn spring-boot:process-test-aot
mvn spring-boot:build-image
我们不需要这样做,因为 Spring Boot 父 POM 定义了一个将这些目标绑定到构建的本机配置文件。我们需要使用此激活的配置文件进行构建:
mvn clean package -Pnative
如果我们还想执行本机测试,则可以激活第二个配置文件:
mvn clean package -Pnative,nativeTest
如果我们想要构建原生镜像,就必须添加 native-maven-plugin 的相应目标。因此,我们也可以定义一个原生配置文件。因为这个插件是由父 POM 管理的,所以我们可以保留版本号:
<profiles><profile><id>native</id><build><plugins><plugin><groupId>org.graalvm.buildtools</groupId><artifactId>native-maven-plugin</artifactId><executions><execution><id>build-native</id><goals><goal>compile-no-fork</goal></goals><phase>package</phase></execution></executions></plugin></plugins></build></profile>
</profiles>
目前,本机测试执行不支持 Mockito。因此,我们可以排除 Mocking 测试,或者通过将以下内容添加到我们的 POM 中来跳过本机测试:
<build><pluginManagement><plugins><plugin><groupId>org.graalvm.buildtools</groupId><artifactId>native-maven-plugin</artifactId><configuration><skipNativeTests>true</skipNativeTests></configuration></plugin></plugins></pluginManagement>
</build>
4.2. 在没有父 POM 的情况下使用 Spring Boot
如果我们不能从 Spring Boot Parent POM 继承,而是将其用作导入范围的依赖项,则必须自己配置插件和配置文件。然后,我们必须将以下内容添加到我们的 POM 中:
<build><pluginManagement><plugins><plugin><groupId>org.graalvm.buildtools</groupId><artifactId>native-maven-plugin</artifactId><version>${native-build-tools-plugin.version}</version><extensions>true</extensions></plugin></plugins></pluginManagement>
</build>
<profiles><profile><id>native</id><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId><configuration><image><builder>paketobuildpacks/builder:tiny</builder><env><BP_NATIVE_IMAGE>true</BP_NATIVE_IMAGE></env></image></configuration><executions><execution><id>process-aot</id><goals><goal>process-aot</goal></goals></execution></executions></plugin><plugin><groupId>org.graalvm.buildtools</groupId><artifactId>native-maven-plugin</artifactId><configuration><classesDirectory>${project.build.outputDirectory}</classesDirectory><metadataRepository><enabled>true</enabled></metadataRepository><requiredVersion>22.3</requiredVersion></configuration><executions><execution><id>add-reachability-metadata</id><goals><goal>add-reachability-metadata</goal></goals></execution></executions></plugin></plugins></build></profile><profile><id>nativeTest</id><dependencies><dependency><groupId>org.junit.platform</groupId><artifactId>junit-platform-launcher</artifactId><scope>test</scope></dependency></dependencies><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId><executions><execution><id>process-test-aot</id><goals><goal>process-test-aot</goal></goals></execution></executions></plugin><plugin><groupId>org.graalvm.buildtools</groupId><artifactId>native-maven-plugin</artifactId><configuration><classesDirectory>${project.build.outputDirectory}</classesDirectory><metadataRepository><enabled>true</enabled></metadataRepository><requiredVersion>22.3</requiredVersion></configuration><executions><execution><id>native-test</id><goals><goal>test</goal></goals></execution></executions></plugin></plugins></build></profile>
</profiles>
<properties><native-build-tools-plugin.version>0.9.17</native-build-tools-plugin.version>
</properties>
4.3. 格拉德尔
Spring Boot Gradle 插件提供用于 AOT 处理的任务(即,不是 AOT 编译自身,而是为 AOT 编译器收集元数据,例如,在代码中注册反射的使用情况)和构建可与 Docker 一起运行的 OCI 映像:
gradle processAot
gradle processTestAot
gradle bootBuildImage
如果我们想构建原生镜像,我们必须添加 Gradle 插件来构建 GraalVM 原生镜像:
plugins {// ...id 'org.graalvm.buildtools.native' version '0.9.17'
}
然后,我们可以通过调用
gradle nativeTest
gradle nativeCompile
目前,本机测试执行不支持 Mockito。因此,我们可以通过配置 graalvmNative 扩展来排除 Mocking 测试或跳过原生测试,如下所示:
graalvmNative {testSupport = false
}
5. 扩展本机映像生成配置
如前所述,我们必须为 AOT 编译器注册反射、类路径扫描、动态代理等的每次用法。因为 Spring 的内置原生支持是一个非常年轻的功能,目前并不是所有的 Spring 模块都有内置支持,所以我们目前需要自己添加这个。这可以通过手动创建 build configuration 来完成。尽管如此,使用 Spring Boot 提供的接口还是更容易,这样 Maven 和 Gradle 插件都可以在 AOT 处理期间使用我们的代码来生成构建配置。
指定其他本机配置的一种可能性是 Native Hints。那么,让我们看看目前缺少的内置支持的两个示例,以及如何将其添加到我们的应用程序中以使其正常工作。
5.1. 示例:Jackson 的 PropertyNamingStrategy
在 MVC Web 应用程序中,REST 控制器方法的每个返回值都由 Jackson 序列化,并自动将每个属性命名为 JSON 元素。我们可以通过在应用程序属性文件中配置 Jackson 的 PropertyNamingStrategy 来全局影响名称映射:
spring.jacksonproperty-naming-strategy=SNAKE_CASE
SNAKE_CASE 是 PropertyNamingStrategies 类型的静态成员的名称。不幸的是,此成员通过反射解决了。因此,AOT 编译器需要知道这一点,否则,我们将收到一条错误消息:
Caused by: java.lang.IllegalArgumentException: Constant named 'SNAKE_CASE' not foundat org.springframework.util.Assert.notNull(Assert.java:219) ~[na:na]at org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration$Jackson2ObjectMapperBuilderCustomizerConfiguration$StandardJackson2ObjectMapperBuilderCustomizer.configurePropertyNamingStrategyField(JacksonAutoConfiguration.java:287) ~[spring-features.exe:na]
为此,我们可以通过如下简单的方式实现和注册 RuntimeHintsRegistrar:
@Configuration
@ImportRuntimeHints(JacksonRuntimeHints.PropertyNamingStrategyRegistrar.class)
public class JacksonRuntimeHints {static class PropertyNamingStrategyRegistrar implements RuntimeHintsRegistrar {@Overridepublic void registerHints(RuntimeHints hints, ClassLoader classLoader) {try {hints.reflection().registerField(PropertyNamingStrategies.class.getDeclaredField("SNAKE_CASE"));} catch (NoSuchFieldException e) {// ...}}}}
注意:从 3.0.0-RC2 版本开始,在 Spring Boot 中解决此问题的拉取请求已经合并,因此它可以与 Spring Boot 3 一起开箱即用。
5.2. 示例:GraphQL 架构文件
如果我们想实现 GraphQL API,我们需要创建一个架构文件并将其定位在“classpath:/graphql/*.graphqls”下,Springs GraphQL 自动配置会自动检测到它。这是通过 Classpath scanning 以及集成的 GraphiQL 测试客户端的欢迎页面完成的。因此,要在本机可执行文件中正常工作,AOT 编译器需要了解这一点。我们可以用同样的方式注册它:
@ImportRuntimeHints(GraphQlRuntimeHints.GraphQlResourcesRegistrar.class)
@Configuration
public class GraphQlRuntimeHints {static class GraphQlResourcesRegistrar implements RuntimeHintsRegistrar {@Overridepublic void registerHints(RuntimeHints hints, ClassLoader classLoader) {hints.resources().registerPattern("graphql/**/").registerPattern("graphiql/index.html");}}}
Spring GraphQL 团队已经在努力解决这个问题,因此我们可能会在未来版本中内置它。
6. 编写测试
要测试 RuntimeHintsRegistrar 实现,我们甚至不需要运行 Spring Boot 测试,我们可以创建一个简单的 JUnit 测试,如下所示:
@Test
void shouldRegisterSnakeCasePropertyNamingStrategy() {// arrangefinal var hints = new RuntimeHints();final var expectSnakeCaseHint = RuntimeHintsPredicates.reflection().onField(PropertyNamingStrategies.class, "SNAKE_CASE");// actnew JacksonRuntimeHints.PropertyNamingStrategyRegistrar().registerHints(hints, getClass().getClassLoader());// assertassertThat(expectSnakeCaseHint).accepts(hints);
}
如果我们想通过集成测试来测试它,我们可以检查 Jackson ObjectMapper 是否具有正确的配置:
@SpringBootTest
class JacksonAutoConfigurationIntegrationTest {@AutowiredObjectMapper mapper;@Testvoid shouldUseSnakeCasePropertyNamingStrategy() {assertThat(mapper.getPropertyNamingStrategy()).isSameAs(PropertyNamingStrategies.SNAKE_CASE);}}
要使用 native 模式对其进行测试,我们必须运行一个 native test:
# Maven
mvn clean package -Pnative,nativeTest
# Gradle
gradle nativeTest
如果我们需要为 Spring Boot Tests 提供特定于测试的 AOT 支持,我们可以使用 AotTestExecutionListener 接口实现TestRuntimeHintsRegistrar或TestExecutionListener。我们可以在官方文档中找到详细信息。
7. Spring Boot 2
Spring 6 和 Spring Boot 3 在原生镜像构建方面迈出了一大步。但是对于之前的主要版本,这也是可能的。我们只需要知道目前还没有内置支持,即有一个补充的 Spring Native 计划来处理这个主题。因此,我们必须在我们的项目中手动包含和配置它。对于 AOT 处理,有一个单独的 Maven 和 Gradle 插件,该插件未合并到 Spring Boot 插件中。当然,集成库提供的原生支持程度不如现在(将来会更多)。
7.1. Spring 原生依赖
首先,我们必须为 Spring Native 添加 Maven 依赖项:
<dependency><groupId>org.springframework.experimental</groupId><artifactId>spring-native</artifactId><version>0.12.1</version>
</dependency>
但是,对于 Gradle 项目,Spring Native 是由 Spring AOT 插件自动添加的。
需要注意的是,每个 Spring Native 版本只支持一个特定的 Spring Boot 版本——比如 Spring Native 0.12.1 只支持 Spring Boot 2.7.1。因此,我们应该确保在pom.xml中使用兼容的 Spring Boot Maven 依赖项。
7.2. 构建包
要构建 OCI 映像,我们需要显式配置构建包。
使用 Maven,我们需要使用 Paketo Java buildpacks 的带有本机映像配置的 spring-boot-maven-plugin:
<build><pluginManagement><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId><configuration><image><builder>paketobuildpacks/builder:tiny</builder><env><BP_NATIVE_IMAGE>true</BP_NATIVE_IMAGE></env></image></configuration></plugin></plugins></pluginManagement>
</build>
在这里,我们将使用各种可用构建器(如 base 和 full)中的小型构建器来构建原生镜像。此外,我们还通过向 BP_NATIVE_IMAGE 环境变量提供 true 值来启用 buildpack。
同样,在使用 Gradle 时,我们可以将 tiny 构建器以及 BP_NATIVE_IMAGE 环境变量添加到 build.gradle 文件中:
bootBuildImage {builder = "paketobuildpacks/builder:tiny"environment = ["BP_NATIVE_IMAGE" : "true"]
}
7.3. Spring AOT 插件
接下来,我们需要添加 Spring AOT 插件,该插件执行提前转换,有助于改善本机映像的占用空间和兼容性。
因此,让我们将最新的 spring-aot-maven-plugin Maven 依赖项添加到我们的 pom.xml中:
<plugin><groupId>org.springframework.experimental</groupId><artifactId>spring-aot-maven-plugin</artifactId><version>0.12.1</version><executions><execution><id>generate</id><goals><goal>generate</goal></goals></execution></executions>
</plugin>
同样,对于 Gradle 项目,我们可以在 build.gradle 文件中添加最新的 org.springframework.experimental.aot 依赖项:
plugins {id 'org.springframework.experimental.aot' version '0.10.0'
}
此外,正如我们之前提到的,这会自动将 Spring Native 依赖项添加到 Gradle 项目中。
Spring AOT 插件提供了几个选项来确定源生成。例如,removeYamlSupport 和 removeJmxSupport 等选项分别删除 Spring Boot Yaml 和 Spring Boot JMX 支持。
7.4. 构建并运行镜像
就是这样!我们已准备好使用 Maven 命令构建 Spring Boot 项目的本机映像:
$ mvn spring-boot:build-image
7.5. 原生镜像构建
接下来,我们将添加一个名为 native 的配置文件,该配置文件支持一些插件,例如 native-maven-plugin 和 spring-boot-maven-plugin:
<profiles><profile><id>native</id><build><plugins><plugin><groupId>org.graalvm.buildtools</groupId><artifactId>native-maven-plugin</artifactId><version>0.9.17</version><executions><execution><id>build-native</id><goals><goal>build</goal></goals><phase>package</phase></execution></executions></plugin><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId><configuration><classifier>exec</classifier></configuration></plugin></plugins></build></profile>
</profiles>
此配置文件将在打包阶段从构建中调用 native-image 编译器。
但是,在使用 Gradle 时,我们会将最新的 org.graalvm.buildtools.native 插件添加到 build.gradle 文件中:
plugins {id 'org.graalvm.buildtools.native' version '0.9.17'
}
就是这样!我们已准备好通过在 Maven package 命令中提供本机配置文件来构建本机映像:
mvn clean package -Pnative
8. 总结
在本教程中,我们探索了使用 Spring Boot 和 GraalVM 的原生构建工具构建原生镜像。我们了解了 Spring 的内置原生支持。所有代码实现都可以在 GitHub 上找到(Spring Boot 2 示例)。