1. 概览
在此 Codelab 中,我们将了解 Spring Native 项目,构建使用该项目的应用,并将其部署到 Google Cloud。
我们将介绍其组件、该项目的近期历史记录、一些用例,当然还有在您的项目中使用该项目所需的步骤。
Spring Native 项目目前处于实验阶段,因此需要进行一些特定配置才能开始使用。不过,正如 SpringOne 2021 大会上宣布的那样,Spring Native 将集成到 Spring Framework 6.0 和 Spring Boot 3.0 中,并提供 一流的支持,因此现在是发布前几个月仔细了解该项目的绝佳时机。
虽然即时编译已针对长时间运行的进程等进行了非常好的优化,但在某些用例中,提前编译的应用的性能甚至更好,我们将在 Codelab 中讨论这一点。
在接下来的实验中
- 使用 Cloud Shell
- 启用 Cloud Run API
- 创建和部署 Spring 原生应用
- 将此类应用部署到 Cloud Run
所需条件
- 已有一个 Google Cloud Platform 项目,并且已有一个 活跃的 GCP 结算账号
- 已安装 gcloud CLI,或有权访问 Cloud Shell
- 具备 Java + XML 基本技能
- 常用 Linux 命令的实践知识
调查问卷
您将如何使用本教程?
您如何评价自己在使用 Java 方面的经验水平?
您如何评价自己在使用 Google Cloud 服务方面的经验水平?
2. 背景
Spring Native 项目利用多种技术为开发者提供原生应用性能。
如需全面了解 Spring Native,最好先了解其中一些组件技术、它们为我们提供的功能以及它们如何协同工作。
AOT 编译
当开发者在编译时间正常运行 javac 时,我们的 .java 源代码会被编译为以字节码编写的 .class 文件。此字节码仅供 Java 虚拟机理解,因此 JVM 必须在其他机器上解释此代码,我们才能运行代码。
此过程使我们能够获得 Java 的标志性可移植性,即“一次编写,随处运行”,但与运行原生代码相比,此过程的开销很大。
幸运的是,JVM 的大多数实现都使用即时编译来降低这种解释开销。这是通过统计函数的调用次数来实现的,如果调用次数足够多,超过了阈值(默认值为 10,000),则会在运行时将其编译为原生代码,以防止进一步的昂贵解释。
提前编译采用相反的方法,即在编译时将所有可访问的代码编译为原生可执行文件。这牺牲了可移植性,换取了运行时内存效率和其他性能提升。

这当然是一种权衡,并不总是值得。不过,AOT 编译在某些用例中可以发挥出优势,例如:
- 启动时间非常重要的短期应用
- 内存高度受限的环境,其中 JIT 的开销可能过高
有趣的是,AOT 编译是在 JDK 9 中作为 实验性功能 引入的,但此实现维护成本高昂,并且从未真正流行起来,因此在 Java 17 中被 悄悄移除,开发者只需使用 GraalVM 即可。
GraalVM
GraalVM 是一种经过高度优化的开源 JDK 分发,具有极快的启动时间、AOT 原生映像编译和多语言功能,可让开发者将多种语言混合到单个应用中。
GraalVM 正在积极开发中,不断获得新功能并改进现有功能,因此我鼓励开发者密切关注。
近期的一些里程碑包括:
- 新的用户友好型原生映像构建输出 ( 2021-01-18)
- Java 17 支持 ( 2022-01-18)
- 默认启用多层编译,以缩短多语言编译时间 ( 2021-04-20)
Spring Native
简而言之,Spring Native 可让您使用 GraalVM 的 native-image 编译器将 Spring 应用转换为原生可执行文件。
此过程涉及在编译时间对应用执行静态分析,以查找应用中可从入口点访问的所有方法。
这实际上创建了应用的“封闭世界”概念,其中所有代码都假定在编译时已知,并且不允许在运行时加载任何新代码。
请务必注意,原生映像生成是一个内存密集型过程,比编译常规应用花费的时间更长,并且对 Java 的某些方面施加了 限制。
在某些情况下,应用无需进行任何代码更改即可使用 Spring Native。不过,某些情况需要特定的原生配置才能正常运行。在这些情况下,Spring Native 通常会提供 原生提示 来简化此过程。
3. 设置/准备工作
在开始实现 Spring Native 之前,我们需要先创建并部署应用,以建立性能基准,以便稍后与原生版本进行比较。
1. 创建项目
我们将首先从 start.spring.io 获取应用:
curl https://start.spring.io/starter.zip -d dependencies=web \
-d javaVersion=11 \
-d bootVersion=2.6.4 -o io-native-starter.zip
此起始应用使用 Spring Boot 2.6.4,这是 spring-native 项目在撰写本文时支持的最新版本。
请注意,自 GraalVM 21.0.3 发布以来,您也可以将 Java 17 用于此示例。在本教程中,我们将继续使用 Java 11,以最大限度地减少涉及的配置。
在命令行中获取 zip 文件后,我们可以为项目创建一个子目录,并在其中解压缩文件夹:
mkdir spring-native cd spring-native unzip ../io-native-starter.zip
**2. 代码变更
打开项目后,我们将快速添加一个生命迹象,并在高效运转后展示 Spring Native 性能。
修改 DemoApplication.java 以匹配以下内容:
package com.example.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.event.EventListener;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.time.Duration;
import java.time.Instant;
@RestController
@SpringBootApplication
public class DemoApplication {
private static Instant startTime;
private static Instant readyTime;
public static void main(String[] args) {
startTime = Instant.now();
SpringApplication.run(DemoApplication.class, args);
}
@GetMapping("/")
public String index() {
return "Time between start and ApplicationReadyEvent: "
+ Duration.between(startTime, readyTime).toMillis()
+ "ms";
}
@EventListener(ApplicationReadyEvent.class)
public void ready() {
readyTime = Instant.now();
}
}
此时,我们的基准应用已准备就绪,因此您可以随意构建映像并在本地高效运转它,以便在将其转换为原生应用之前了解启动时间。
如需构建映像,请执行以下操作:
mvn spring-boot:build-image
您还可以使用 docker images demo 来了解基准映像的大小:
如需运行应用,请执行以下操作:
docker run --rm -p 8080:8080 demo:0.0.1-SNAPSHOT
3. 部署基准应用
现在我们有了应用,接下来我们将部署它并记录时间,以便稍后与原生应用的启动时间进行比较。
根据您构建的应用类型,有多种不同的托管 方式。
不过,由于我们的示例是一个非常简单、直接的 Web 应用,因此我们可以保持简单,并依赖于 Cloud Run。
如果您是在自己的机器上进行操作,请确保已安装并更新 gcloud CLI 工具。
如果您使用的是 Cloud Shell,则所有这些都将得到妥善处理,您只需在源目录中运行以下命令即可:
gcloud run deploy
4. 应用配置
1. 配置 Maven 制品库
由于此项目仍处于实验阶段,因此我们必须配置应用,使其能够找到实验性工件,这些工件在 Maven 的中央制品库中不可用。
这将涉及向 pom.xml 添加以下元素,您可以在所选编辑器中执行此操作。
将以下 repositories 和 pluginRepositories 部分添加到我们的 pom 中:
<repositories>
<repository>
<id>spring-release</id>
<name>Spring release</name>
<url>https://repo.spring.io/release</url>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>spring-release</id>
<name>Spring release</name>
<url>https://repo.spring.io/release</url>
</pluginRepository>
</pluginRepositories>
2. 添加依赖项
接下来,添加 spring-native 依赖项,这是将 Spring 应用作为原生映像运行所必需的。注意:如果您使用的是 Gradle,则无需执行此步骤
<dependencies>
<!-- ... -->
<dependency>
<groupId>org.springframework.experimental</groupId>
<artifactId>spring-native</artifactId>
<version>0.11.2</version>
</dependency>
</dependencies>
3. 添加/启用插件
现在添加 AOT 插件,以提高原生映像兼容性和占用空间(了解详情):
<plugins>
<!-- ... -->
<plugin>
<groupId>org.springframework.experimental</groupId>
<artifactId>spring-aot-maven-plugin</artifactId>
<version>0.11.2</version>
<executions>
<execution>
<id>generate</id>
<goals>
<goal>generate</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
现在,我们将更新 spring-boot-maven-plugin 以启用原生映像支持,并使用 paketo 构建器构建原生映像:
<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>
请注意,微型构建器映像只是几个选项之一。对于我们的用例来说,这是一个不错的选择,因为它只有很少的额外库和实用程序,这有助于最大限度地减少攻击面。
例如,如果您要构建一个需要访问某些常见 C 库的应用,或者您还不确定应用的要求,那么 完整构建器 可能更适合。
5. 构建和运行原生应用
一切就绪后,我们应该能够构建映像并运行原生编译的应用。
在运行构建之前,请注意以下几点:
- 这比常规构建(几分钟)花费的时间更长

- 此构建流程可能会占用大量内存(几 GB)

- 此构建流程需要 Docker 守护程序可访问
- 虽然在此示例中,我们手动完成了整个过程,但您也可以配置构建阶段以 自动触发原生构建配置文件。
如需构建映像,请执行以下操作:
mvn spring-boot:build-image
构建完成后,我们就可以看到原生应用的实际运行情况了!
如需运行应用,请执行以下操作:
docker run --rm -p 8080:8080 demo:0.0.1-SNAPSHOT
此时,我们能够很好地了解原生应用等式的两方面。
我们在编译时间牺牲了一些时间和额外的内存用量,但作为交换,我们获得了一个启动速度更快、内存消耗显著减少(取决于工作负载)的应用。
如果我们运行 docker images demo 来比较原生映像与原始映像的大小,可以看到显著的缩减:

我们还应注意,在更复杂的应用场景中,还需要进行其他修改,以告知 AOT 编译器应用在运行时将执行的操作。因此,某些可预测的工作负载(例如批量作业)可能非常适合此方法,而其他工作负载可能需要更多工作。
6. 部署原生应用
如需将应用部署到 Cloud Run,我们需要将原生映像放入软件包管理器(如 Artifact Registry)中。
1. 准备 Docker 制品库
我们可以先创建一个制品库来开始此过程:
gcloud artifacts repositories create native-image-docker-repo --repository-format=docker \
--location=us-central1 --description="Repository for our native images"
接下来,我们需要确保已通过 身份验证,以便推送到 新注册表。
gcloud CLI 可以大大简化此过程:
gcloud auth configure-docker us-central1-docker.pkg.dev
**2. 将映像推送到 Artifact Registry
接下来,我们将标记映像:
export PROJECT_ID=$(gcloud config list --format 'value(core.project)')
docker tag demo:0.0.1-SNAPSHOT \
us-central1-docker.pkg.dev/$PROJECT_ID/native-image-docker-repo/quickstart-image:tag2
然后,我们可以使用 docker push 将其发送到 Artifact Registry:
docker push us-central1-docker.pkg.dev/$PROJECT_ID/native-image-docker-repo/quickstart-image:tag2
3. 部署到 Cloud Run
我们现在可以将存储在 Artifact Registry 中的映像部署到 Cloud Run:
gcloud run deploy --image us-central1-docker.pkg.dev/$PROJECT_ID/native-image-docker-repo/quickstart-image:tag2
由于我们已将应用构建并部署为原生映像,因此可以放心,我们的应用在运行时会充分利用基础架构费用。
您可以自行比较基准应用与此新原生应用的启动时间!

7. 总结/清理
恭喜您在 Google Cloud 上构建和部署 Spring 原生应用!
希望本教程能鼓励您更加熟悉 Spring Native 项目,并在将来满足您的需求时记住它。
可选:清理和/或停用服务
无论您是为此 Codelab 创建了 Google Cloud 项目,还是重复使用现有项目,请注意避免因我们使用的资源而产生不必要的费用。
8. 其他资源
虽然 Spring Native 项目目前是一个新的实验性项目,但已有大量优质资源可帮助早期采用者排查问题并参与其中:
其他资源
以下是在线资源,可能与本教程相关:
许可
此作品已获得 Creative Commons Attribution 2.0 通用许可授权。