1. 概览
此实验演示了旨在简化软件工程师在容器化环境中开发 Java 应用的开发工作流程的功能和特性。典型的容器开发需要用户了解容器和容器构建流程的详细信息。此外,开发者通常必须中断工作流程,离开 IDE 以在远程环境中测试和调试应用。借助本教程中提到的工具和技术,开发者无需离开 IDE 即可高效处理容器化应用。
学习内容
在本实验中,您将学习在 GCP 中使用容器进行开发的各种方法,包括:
- 使用 Cloud Workstations 进行内部循环开发
- 创建新的 Java 初始应用
- 开发流程概览
- 开发简单的 CRUD REST 服务
- 在 GKE 集群上调试应用
- 将应用连接到 Cloud SQL 数据库

2. 设置和要求
自定进度的环境设置
- 登录 Google Cloud 控制台,然后创建一个新项目或重复使用现有项目。如果您还没有 Gmail 或 Google Workspace 账号,则必须创建一个。



- 项目名称是此项目参与者的显示名称。它是 Google API 尚未使用的字符串。您可以随时更新。
- 项目 ID 在所有 Google Cloud 项目中是唯一的,并且是不可变的(一经设置便无法更改)。Cloud 控制台会自动生成一个唯一字符串;通常情况下,您无需关注该字符串。在大多数 Codelab 中,您都需要引用项目 ID(通常用
PROJECT_ID标识)。如果您不喜欢生成的 ID,可以再随机生成一个 ID。或者,您也可以尝试自己的项目 ID,看看是否可用。完成此步骤后便无法更改该 ID,并且此 ID 在项目期间会一直保留。 - 此外,还有第三个值,即部分 API 使用的项目编号,供您参考。如需详细了解所有这三个值,请参阅文档。
- 接下来,您需要在 Cloud 控制台中启用结算功能,以便使用 Cloud 资源/API。运行此 Codelab 应该不会产生太多的费用(如果有费用的话)。若要关闭资源以避免产生超出本教程范围的结算费用,您可以删除自己创建的资源或删除整个项目。Google Cloud 的新用户符合参与 $300 USD 免费试用计划的条件。
启动 Cloudshell 编辑器
本实验经过精心设计和测试,可与 Google Cloud Shell 编辑器搭配使用。如需访问编辑器,请执行以下操作:
- 访问您的 Google 项目:https://console.cloud.google.com。
- 点击右上角的 Cloud Shell 编辑器图标

- 系统会在窗口底部打开一个新窗格
- 点击“打开编辑器”按钮

- 编辑器将打开,右侧显示资源管理器,中央区域显示编辑器
- 屏幕底部还应显示一个终端窗格
- 如果终端未打开,请使用 `ctrl+`` 组合键打开新的终端窗口
设置 gcloud
在 Cloud Shell 中,设置项目 ID 以及要将应用部署到的区域。将它们保存为 PROJECT_ID 和 REGION 变量。
export REGION=us-central1
export PROJECT_ID=$(gcloud config get-value project)
export PROJECT_NUMBER=$(gcloud projects describe $PROJECT_ID --format='value(projectNumber)')
克隆源代码
本实验的源代码位于 GitHub 上 GoogleCloudPlatform 中的 container-developer-workshop 中。使用以下命令克隆该代码库,然后切换到该目录。
git clone https://github.com/GoogleCloudPlatform/container-developer-workshop.git
cd container-developer-workshop/labs/spring-boot
预配本实验中使用的基础设施
在本实验中,您将向 GKE 部署代码,并访问存储在 Cloud SQL 数据库中的数据。下面的设置脚本会为您准备此基础架构。预配过程将需要 25 分钟以上。等待脚本运行完毕,然后再继续执行下一部分。
./setup_with_cw.sh &
Cloud Workstations 集群
在 Cloud 控制台中打开 Cloud Workstations。等待集群处于 READY 状态。
创建工作站配置
如果您的 Cloud Shell 会话已断开连接,请点击“重新连接”,然后运行 gcloud CLI 命令来设置项目 ID。在运行命令之前,请将下面的示例项目 ID 替换为您的 Qwiklabs 项目 ID。
gcloud config set project qwiklabs-gcp-project-id
在终端中运行以下脚本,以创建 Cloud Workstations 配置。
cd ~/container-developer-workshop/labs/spring-boot
./workstation_config_setup.sh
验证“配置”部分下的结果。需要 2 分钟才能转换为 READY 状态。

在控制台中打开 Cloud Workstations 并创建新实例。

将名称更改为 my-workstation,然后选择现有配置:codeoss-java。

验证“工作站”部分下的结果。

启动工作站
启动并发布工作站。

点击地址栏中的相应图标,允许使用第三方 Cookie。

点击“网站无法正常运行?”。

点击“允许使用 Cookie”。

工作站启动后,您会看到 Code OSS IDE 启动。在工作站 IDE 的“开始使用”页面上,点击“标记为完成”

3. 创建新的 Java 初始应用
在此部分中,您将从头开始创建一个新的 Java Spring Boot 应用,并使用 spring.io 提供的示例应用。打开新终端。

克隆示例应用
- 创建起始应用
curl https://start.spring.io/starter.zip -d dependencies=web -d type=maven-project -d javaVersion=17 -d packageName=com.example.springboot -o sample-app.zip
如果您看到此消息,请点击“允许”按钮,以便您将内容复制并粘贴到工作站中。

- 解压缩应用
unzip sample-app.zip -d sample-app
- 打开“sample-app”文件夹
cd sample-app && code-oss-cloud-workstations -r --folder-uri="$PWD"
添加 spring-boot-devtools 和 Jib
如需启用 Spring Boot DevTools,请在编辑器中从探索器找到并打开 pom.xml。接下来,将以下代码粘贴到描述行(即 <description>Demo project for Spring Boot</description>)之后
- 在 pom.xml 中添加 spring-boot-devtools
打开项目根目录中的 pom.xml。在 Description 条目后添加以下配置。
pom.xml
<!-- Spring profiles-->
<profiles>
<profile>
<id>sync</id>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
</dependency>
</dependencies>
</profile>
</profiles>
- 在 pom.xml 中启用 jib-maven-plugin
Jib 是 Google 的一款开源 Java 容器化工具,可让 Java 开发者使用他们熟悉的 Java 工具来构建容器。Jib 是一款快速简单的容器映像构建工具,可处理将应用打包到容器映像中的所有步骤。它不需要您编写 Dockerfile 或安装 Docker,并且直接集成到 Maven 和 Gradle 中。
在 pom.xml 文件中向下滚动,然后更新 Build 部分以包含 Jib 插件。构建部分完成后应与以下内容一致。
pom.xml
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<!-- Jib Plugin-->
<plugin>
<groupId>com.google.cloud.tools</groupId>
<artifactId>jib-maven-plugin</artifactId>
<version>3.2.0</version>
</plugin>
<!-- Maven Resources Plugin-->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-resources-plugin</artifactId>
<version>3.1.0</version>
</plugin>
</plugins>
</build>
生成清单
Skaffold 提供集成式工具来简化容器开发。在此步骤中,您将初始化 Skaffold,它会自动创建基本的 Kubernetes YAML 文件。该流程会尝试识别包含容器映像定义(例如 Dockerfile)的目录,然后为每个目录创建部署和服务清单。
在终端中执行以下命令以开始该流程。

- 在终端中执行以下命令
skaffold init --generate-manifests
- 当系统提示时:
- 使用箭头键将光标移动到
Jib Maven Plugin - 按空格键即可选择相应选项。
- 按 Enter 键继续
- 输入 8080 作为端口
- 输入 y 以保存配置
向工作区添加了两个文件 skaffold.yaml 和 deployment.yaml
Skaffold 输出:

更新应用名称
配置中包含的默认值目前与您的应用名称不一致。更新文件以引用您的应用名称,而不是默认值。
- 更改 Skaffold 配置中的条目
- 打开“
skaffold.yaml” - 选择当前设置为
pom-xml-image的图片名称 - 右键点击,然后选择“更改所有匹配项”
- 以
demo-app格式输入新名称
- 更改 Kubernetes 配置中的条目
- 打开
deployment.yaml文件 - 选择当前设置为
pom-xml-image的图片名称 - 右键点击,然后选择“更改所有匹配项”
- 以
demo-app格式输入新名称
启用自动同步模式
为了实现优化的热重载体验,您将使用 Jib 提供的同步功能。在此步骤中,您将配置 Skaffold 以在构建流程中利用该功能。
请注意,您在 Skaffold 配置中配置的“sync”配置文件利用了您在上一步中配置的 Spring“sync”配置文件,您在其中启用了对 spring-dev-tools 的支持。
- 更新 Skaffold 配置
在 skaffold.yaml 文件中,将文件的整个 build 部分替换为以下规范。请勿更改文件的其他部分。
skaffold.yaml
build:
artifacts:
- image: demo-app
jib:
project: com.example:demo
type: maven
args:
- --no-transfer-progress
- -Psync
fromImage: gcr.io/distroless/java17-debian11:debug
sync:
auto: true
添加默认路由
在 /src/main/java/com/example/springboot/ 文件夹中创建一个名为 HelloController.java 的文件。

将以下内容粘贴到文件中,以创建默认的 HTTP 路由。
HelloController.java
package com.example.springboot;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.beans.factory.annotation.Value;
@RestController
public class HelloController {
@Value("${target:local}")
String target;
@GetMapping("/")
public String hello()
{
return String.format("Hello from your %s environment!", target);
}
}
4. 开发流程概览
在本部分中,您将使用 Cloud Code 插件完成几个步骤,以了解基本流程并验证初始应用的配置和设置。
Cloud Code 与 Skaffold 集成,可简化您的开发流程。在后续步骤中部署到 GKE 时,Cloud Code 和 Skaffold 会自动构建您的容器映像,将其推送到 Container Registry,然后将您的应用部署到 GKE。这会在后台进行,不会让开发者流程察觉到相关细节。Cloud Code 还通过为基于容器的开发提供传统的调试和热同步功能来增强您的开发流程。
登录 Google Cloud
点击 Cloud Code 图标,然后选择“登录 Google Cloud”:

点击“Proceed to sign in”(继续登录)。

在终端中查看输出,然后打开链接:

使用您的 Qwiklabs 学生凭据登录。

选择“允许”:

复制验证码,然后返回到工作站标签页。

粘贴验证码,然后按 Enter 键。

添加 Kubernetes 集群
- 添加集群

- 选择 Google Kubernetes Engine:

- 选择项目。

- 选择在初始设置中创建的“quote-cluster”。


使用 gcloud CLI 设置当前项目 ID
从 Qwiklabs 页面复制本实验的项目 ID。

运行 gcloud CLI 命令以设置项目 ID。在运行命令之前,请替换示例项目 ID。
gcloud config set project qwiklabs-gcp-project-id
示例输出:

在 Kubernetes 上调试
- 在左侧窗格底部选择 Cloud Code。

- 在“开发会话”下方显示的面板中,选择“在 Kubernetes 上调试”。
如果看不到该选项,请向下滚动。

- 选择“是”以使用当前上下文。

- 选择在初始设置期间创建的“quote-cluster”。

- 选择“容器代码库”。

- 选择下部窗格中的“输出”标签页,查看进度和通知
- 在右侧的渠道下拉菜单中选择“Kubernetes:运行/调试 - 详细”,以查看其他详细信息和从容器实时传输的日志

等待应用部署完成。

- 在 Cloud 控制台中查看已部署到 GKE 的应用。

- 如需返回简化视图,请从“输出”标签页的下拉菜单中选择“Kubernetes:运行/调试”。
- 构建和测试完成后,“输出”标签页将显示
Resource deployment/demo-app status completed successfully,并列出以下网址:“Forwarded 网址 from service demo-app: http://localhost:8080” - 在 Cloud Code 终端中,将鼠标悬停在输出中的网址 (http://localhost:8080) 上,然后在显示的工具提示中选择“打开链接”。

系统会打开一个新标签页,您将看到以下输出内容:

利用断点
- 打开位于
/src/main/java/com/example/springboot/HelloController.java的HelloController.java应用 - 找到根路径的 return 语句,该语句的内容为
return String.format("Hello from your %s environment!", target); - 点击行号左侧的空白处,为该行添加一个断点。系统会显示一个红色指示器,表明已设置断点

- 重新加载浏览器,并注意调试程序会在断点处停止进程,以便您调查在 GKE 中远程运行的应用的变量和状态

- 向下点击进入“变量”部分,直到找到“目标”变量。
- 观察当前值为“local”

- 双击变量名称“target”,然后在弹出式窗口中,
将值更改为“Cloud Workstations”

- 点击调试控制面板中的“继续”按钮

- 在浏览器中查看响应,其中现在显示了您刚刚输入的更新后的值。

- 点击行号左侧的红色指示器,移除断点。这样可防止您的代码在您继续完成本实验时在此行停止执行。
热重载
- 更改语句以返回不同的值,例如“Hello from %s Code”
- 文件会自动保存并同步到 GKE 中的远程容器
- 刷新浏览器即可查看更新后的结果。
- 点击调试工具栏中的红色方块,停止调试会话

选择“是,每次运行后都清理”。

5. 开发简单的 CRUD REST 服务
至此,您的应用已完全配置好,可以进行容器化开发,并且您已通过 Cloud Code 完成了基本开发工作流程。在以下部分中,您将练习所学知识,添加连接到 Google Cloud 中托管式数据库的 REST 服务端点。
配置依赖项
应用代码使用数据库来持久保留 REST 服务数据。通过在 pom.xml 中添加以下内容来确保依赖项可用
- 打开
pom.xml文件,并将以下内容添加到配置的依赖项部分
pom.xml
<!-- Database dependencies-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-core</artifactId>
</dependency>
<dependency>
<groupId>javax.persistence</groupId>
<artifactId>javax.persistence-api</artifactId>
<version>2.2</version>
</dependency>
代码 REST 服务
Quote.java
在 /src/main/java/com/example/springboot/ 中创建一个名为 Quote.java 的文件,并将以下代码复制到其中。这定义了应用中使用的 Quote 对象的实体模型。
package com.example.springboot;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.util.Objects;
@Entity
@Table(name = "quotes")
public class Quote
{
@Id
@Column(name = "id")
private Integer id;
@Column(name="quote")
private String quote;
@Column(name="author")
private String author;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getQuote() {
return quote;
}
public void setQuote(String quote) {
this.quote = quote;
}
public String getAuthor() {
return author;
}
public void setAuthor(String author) {
this.author = author;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
Quote quote1 = (Quote) o;
return Objects.equals(id, quote1.id) &&
Objects.equals(quote, quote1.quote) &&
Objects.equals(author, quote1.author);
}
@Override
public int hashCode() {
return Objects.hash(id, quote, author);
}
}
QuoteRepository.java
在 src/main/java/com/example/springboot 中创建一个名为 QuoteRepository.java 的文件,并将以下代码复制到其中
package com.example.springboot;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
public interface QuoteRepository extends JpaRepository<Quote,Integer> {
@Query( nativeQuery = true, value =
"SELECT id,quote,author FROM quotes ORDER BY RANDOM() LIMIT 1")
Quote findRandomQuote();
}
此代码使用 JPA 来持久保留数据。该类扩展了 Spring JPARepository 接口,并允许创建自定义代码。在您添加的代码中,有一个 findRandomQuote 自定义方法。
QuoteController.java
为了公开服务的端点,QuoteController 类将提供此功能。
在 src/main/java/com/example/springboot 中创建一个名为 QuoteController.java 的文件,并将以下内容复制到其中
package com.example.springboot;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class QuoteController {
private final QuoteRepository quoteRepository;
public QuoteController(QuoteRepository quoteRepository) {
this.quoteRepository = quoteRepository;
}
@GetMapping("/random-quote")
public Quote randomQuote()
{
return quoteRepository.findRandomQuote();
}
@GetMapping("/quotes")
public ResponseEntity<List<Quote>> allQuotes()
{
try {
List<Quote> quotes = new ArrayList<Quote>();
quoteRepository.findAll().forEach(quotes::add);
if (quotes.size()==0 || quotes.isEmpty())
return new ResponseEntity<List<Quote>>(HttpStatus.NO_CONTENT);
return new ResponseEntity<List<Quote>>(quotes, HttpStatus.OK);
} catch (Exception e) {
System.out.println(e.getMessage());
return new ResponseEntity<List<Quote>>(HttpStatus.INTERNAL_SERVER_ERROR);
}
}
@PostMapping("/quotes")
public ResponseEntity<Quote> createQuote(@RequestBody Quote quote) {
try {
Quote saved = quoteRepository.save(quote);
return new ResponseEntity<Quote>(saved, HttpStatus.CREATED);
} catch (Exception e) {
System.out.println(e.getMessage());
return new ResponseEntity<Quote>(HttpStatus.INTERNAL_SERVER_ERROR);
}
}
@PutMapping("/quotes/{id}")
public ResponseEntity<Quote> updateQuote(@PathVariable("id") Integer id, @RequestBody Quote quote) {
try {
Optional<Quote> existingQuote = quoteRepository.findById(id);
if(existingQuote.isPresent()){
Quote updatedQuote = existingQuote.get();
updatedQuote.setAuthor(quote.getAuthor());
updatedQuote.setQuote(quote.getQuote());
return new ResponseEntity<Quote>(updatedQuote, HttpStatus.OK);
} else {
return new ResponseEntity<Quote>(HttpStatus.NOT_FOUND);
}
} catch (Exception e) {
System.out.println(e.getMessage());
return new ResponseEntity<Quote>(HttpStatus.INTERNAL_SERVER_ERROR);
}
}
@DeleteMapping("/quotes/{id}")
public ResponseEntity<HttpStatus> deleteQuote(@PathVariable("id") Integer id) {
Optional<Quote> quote = quoteRepository.findById(id);
if (quote.isPresent()) {
quoteRepository.deleteById(id);
return new ResponseEntity<>(HttpStatus.NO_CONTENT);
} else {
return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
}
}
}
添加数据库配置
application.yaml
为服务访问的后端数据库添加配置。修改(或创建,如果不存在)src/main/resources 下名为 application.yaml 的文件,并为后端添加参数化 Spring 配置。
target: local
spring:
config:
activate:
on-profile: cloud-dev
datasource:
url: 'jdbc:postgresql://${DB_HOST:127.0.0.1}/${DB_NAME:quote_db}'
username: '${DB_USER:user}'
password: '${DB_PASS:password}'
jpa:
properties:
hibernate:
jdbc:
lob:
non_contextual_creation: true
dialect: org.hibernate.dialect.PostgreSQLDialect
hibernate:
ddl-auto: update
添加数据库迁移
在 src/main/resources 下创建文件夹 db/migration
创建 SQL 文件:V1__create_quotes_table.sql
将以下内容粘贴到文件中
V1__create_quotes_table.sql
CREATE TABLE quotes(
id INTEGER PRIMARY KEY,
quote VARCHAR(1024),
author VARCHAR(256)
);
INSERT INTO quotes (id,quote,author) VALUES (1,'Never, never, never give up','Winston Churchill');
INSERT INTO quotes (id,quote,author) VALUES (2,'While there''s life, there''s hope','Marcus Tullius Cicero');
INSERT INTO quotes (id,quote,author) VALUES (3,'Failure is success in progress','Anonymous');
INSERT INTO quotes (id,quote,author) VALUES (4,'Success demands singleness of purpose','Vincent Lombardi');
INSERT INTO quotes (id,quote,author) VALUES (5,'The shortest answer is doing','Lord Herbert');
Kubernetes 配置
对 deployment.yaml 文件进行以下添加操作后,应用便可连接到 Cloud SQL 实例。
- TARGET - 配置变量以指示应用所执行的环境
- SPRING_PROFILES_ACTIVE - 显示有效的 Spring 配置文件,该配置文件将配置为
cloud-dev - DB_HOST - 数据库的私有 IP,在创建数据库实例时已记录,或者通过点击 Google Cloud 控制台导航菜单中的
SQL找到 - 请更改该值! - DB_USER 和 DB_PASS - 在 CloudSQL 实例配置中设置,以 Secret 形式存储在 GCP 中
使用以下内容更新您的 deployment.yaml。
deployment.yaml
apiVersion: v1
kind: Service
metadata:
name: demo-app
labels:
app: demo-app
spec:
ports:
- port: 8080
protocol: TCP
clusterIP: None
selector:
app: demo-app
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: demo-app
labels:
app: demo-app
spec:
replicas: 1
selector:
matchLabels:
app: demo-app
template:
metadata:
labels:
app: demo-app
spec:
containers:
- name: demo-app
image: demo-app
env:
- name: PORT
value: "8080"
- name: TARGET
value: "Local Dev - CloudSQL Database - K8s Cluster"
- name: SPRING_PROFILES_ACTIVE
value: cloud-dev
- name: DB_HOST
value: ${DB_INSTANCE_IP}
- name: DB_PORT
value: "5432"
- name: DB_USER
valueFrom:
secretKeyRef:
name: gke-cloud-sql-secrets
key: username
- name: DB_PASS
valueFrom:
secretKeyRef:
name: gke-cloud-sql-secrets
key: password
- name: DB_NAME
valueFrom:
secretKeyRef:
name: gke-cloud-sql-secrets
key: database
在终端中运行以下命令,将 DB_HOST 值替换为数据库的地址:
export DB_INSTANCE_IP=$(gcloud sql instances describe quote-db-instance \
--format=json | jq \
--raw-output ".ipAddresses[].ipAddress")
envsubst < deployment.yaml > deployment.new && mv deployment.new deployment.yaml
打开 deployment.yaml,并验证 DB_HOST 值是否已更新为实例 IP。

部署和验证应用
- 在 Cloud Shell Editor 底部的窗格中,选择 Cloud Code,然后选择屏幕顶部的“在 Kubernetes 上调试”。

- 构建和测试完成后,“输出”标签页将显示
Resource deployment/demo-app status completed successfully,并列出网址:“Forwarded 网址 from service demo-app: http://localhost: 8080”(来自服务 demo-app 的转发网址:http://localhost:8080)。请注意,有时端口可能会有所不同,例如 8081。如果需要,请设置相应的值。在终端中设置网址的值
export URL=localhost:8080
- 查看随机名言
在终端中,针对 random-quote 端点多次运行以下命令。观察到重复调用返回不同的报价
curl $URL/random-quote | jq
- 添加报价
使用下列命令创建 ID 为 6 的新报价,并观察系统回显的请求
curl -H 'Content-Type: application/json' -d '{"id":"6","author":"Henry David Thoreau","quote":"Go confidently in the direction of your dreams! Live the life you have imagined"}' -X POST $URL/quotes
- 删除报价
现在,使用删除方法删除您刚刚添加的引用,并观察 HTTP/1.1 204 响应代码。
curl -v -X DELETE $URL/quotes/6
- 服务器错误
在条目已被删除后再次运行最后一个请求,体验错误状态
curl -v -X DELETE $URL/quotes/6
请注意,响应会返回一个 HTTP:500 Internal Server Error。
调试应用
在上一部分中,您尝试删除数据库中不存在的条目时,发现应用处于错误状态。在本部分中,您将设置一个断点来定位问题。该错误发生在 DELETE 操作中,因此您将使用 QuoteController 类。
- 打开“
src/main/java/com/example/springboot/QuoteController.java” - 找到
deleteQuote()方法 - 找到以下行:
Optional<Quote> quote = quoteRepository.findById(id); - 点击行号左侧的空白处,在该行上设置断点。
- 系统会显示一个红色指示图标,表明已设置断点
- 再次运行
delete命令
curl -v -X DELETE $URL/quotes/6
- 点击左侧列中的图标,切换回调试视图
- 观察到调试行在 QuoteController 类中停止。
- 在调试器中,点击
step over图标
- 请注意,代码会向客户端返回内部服务器错误 HTTP 500,这并不理想。
Trying 127.0.0.1:8080... * Connected to 127.0.0.1 (127.0.0.1) port 8080 (#0) > DELETE /quotes/6 HTTP/1.1 > Host: 127.0.0.1:8080 > User-Agent: curl/7.74.0 > Accept: */* > * Mark bundle as not supporting multiuse < HTTP/1.1 500 < Content-Length: 0 < Date: < * Connection #0 to host 127.0.0.1 left intact
更新代码
此代码不正确,应重构 else 块以发送回 HTTP 404 未找到状态代码。
更正错误。
- 在调试会话仍在运行时,按调试控制面板中的“继续”按钮完成请求。
- 接下来,将
else块更改为以下代码:
else {
return new ResponseEntity<HttpStatus>(HttpStatus.NOT_FOUND);
}
该方法应如下所示
@DeleteMapping("/quotes/{id}")
public ResponseEntity<HttpStatus> deleteQuote(@PathVariable("id") Integer id) {
Optional<Quote> quote = quoteRepository.findById(id);
if (quote.isPresent()) {
quoteRepository.deleteById(id);
return new ResponseEntity<>(HttpStatus.NO_CONTENT);
} else {
return new ResponseEntity<HttpStatus>(HttpStatus.NOT_FOUND);
}
}
- 重新运行删除命令
curl -v -X DELETE $URL/quotes/6
- 逐步调试,并观察返回给调用方的 HTTP 404 Not Found。
Trying 127.0.0.1:8080... * Connected to 127.0.0.1 (127.0.0.1) port 8080 (#0) > DELETE /quotes/6 HTTP/1.1 > Host: 127.0.0.1:8080 > User-Agent: curl/7.74.0 > Accept: */* > * Mark bundle as not supporting multiuse < HTTP/1.1 404 < Content-Length: 0 < Date: < * Connection #0 to host 127.0.0.1 left intact
- 点击调试工具栏中的红色方块,停止调试会话


6. 恭喜
恭喜!在本实验中,您从头开始创建了一个新的 Java 应用,并将其配置为可与容器有效搭配使用。然后,您按照传统应用堆栈中相同的开发者流程,将应用部署到远程 GKE 集群并进行调试。
您学到的内容
- 使用 Cloud Workstations 进行内部循环开发
- 创建新的 Java 初始应用
- 开发流程概览
- 开发简单的 CRUD REST 服务
- 在 GKE 集群上调试应用
- 将应用连接到 Cloud SQL 数据库
