1. 概览
本实验演示了一些特性和功能,这些特性和功能旨在简化在容器化环境中开发 Java 应用的软件工程师的开发工作流。典型的容器开发要求用户了解容器和容器构建流程的详细信息。此外,开发者通常需要中断他们的流程,离开 IDE,以在远程环境中测试和调试其应用。借助本教程中提到的工具和技术,开发者无需离开 IDE 即可高效使用容器化应用。
学习内容
在本实验中,您将学习在 GCP 中使用容器进行开发的方法,包括:
- 使用 Cloud Workstations 进行 InnerLoop 开发
- 创建新的 Java 起始应用
- 开发过程介绍
- 开发简单的 CRUD 静态服务
- 在 GKE 集群上调试应用
- 将应用连接到 CloudSQL 数据库
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 Editor 搭配使用,并经过测试。要访问该编辑器,请按以下步骤操作:
- 通过 https://console.cloud.google.com 访问您的 Google 项目。
- 点击右上角的 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,并访问存储在 CloudSQL 数据库中的数据。下面的设置脚本会为您准备此基础架构。预配过程将需要 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
验证“Configurations”(配置)部分下的结果。转变为“就绪”状态需要 2 分钟。
在控制台中打开 Cloud Workstations 并创建新实例。
将名称更改为“my-workstation
”,然后选择现有配置:codeoss-java
。
验证“Workstations”部分下的结果。
启动工作站
启动和启动工作站。
通过点击地址栏中的图标允许第三方 Cookie。
点击“网站无法访问?”。
点击“允许 Cookie”。
工作站启动后,您会看到 Code OSS IDE 启动。点击“标为已完成”在“使用入门”页面一是工作站 IDE
3. 创建新的 Java 起始应用
在本部分中,您将使用 Spring.io 提供的示例应用从头开始创建一个新的 Java Spring Boot 应用。打开一个新的终端。
克隆示例应用
- 创建起始应用
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 和吉布
如需启用 Spring Boot 开发者工具,请在编辑器中从资源管理器中找到并打开 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 配置中配置的配置文件利用了 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 图标,然后选择“Sign in to Google Cloud”:
点击“继续登录”。
在终端中检查输出并打开链接:
使用您的 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”创建的新实例
- 选择“Container Repository”。
- 选择下部窗格中的“Output”(输出)标签页可查看进度和通知
- 选择“Kubernetes:运行/调试 - 详细”查看右侧渠道下拉菜单中的 其他详细信息和实时从容器流式传输的日志
等待应用完成部署。
- 在 Cloud 控制台中查看 GKE 上已部署的应用。
- 选择“Kubernetes: Run/Debug”返回简化的视图“输出”标签页的下拉菜单中
- 构建和测试完成后,“Output”(输出)标签页会显示
Resource deployment/demo-app status completed successfully
,并列出了一个网址:“Forwarded 网址 from service demo-app: http://localhost:8080” - 在 Cloud Code 终端中,将鼠标悬停在输出中的网址 (http://localhost:8080) 上,然后在显示的工具提示中选择“Follow link”(访问链接)。
系统随即会打开新标签页,并显示以下输出:
利用断点
- 打开位于
/src/main/java/com/example/springboot/HelloController.java
的HelloController.java
应用 - 找到显示
return String.format("Hello from your %s environment!", target);
的根路径的 return 语句 - 点击行号左侧的空白处,为该行添加断点。系统会显示一个红色指示器,指明断点已设置
- 重新加载浏览器,并注意调试程序会在断点停止进程,并允许您调查在 GKE 中远程运行的应用变量和状态
- 点击“变量”部分,直到找到“目标”变量。
- 观察到当前值为“local”
- 双击变量名称“target”在弹出式窗口中
请将值更改为“Cloud Workstations”
- 点击调试控制台中的“继续”按钮
- 在浏览器中查看响应,浏览器现在会显示您刚刚输入的更新值。
- 点击行号左侧的红色指示器即可移除断点。这可防止您的代码在这一行的这段代码中停止执行。
热重载
- 更改语句以返回不同的值,例如“Hello from %s Code”
- 文件会自动保存并同步到 GKE 中的远程容器中
- 刷新浏览器以查看更新后的结果。
- 点击调试工具栏中的红色方块,停止调试会话
选择“是,每次运行后清理”。
5. 开发简单的 CRUD 静态服务
至此,您的应用已完全针对容器化开发进行了配置,并且您已完成 Cloud Code 的基本开发工作流。在以下部分中,您将通过添加连接到 Google Cloud 中的代管式数据库的 REST 服务端点,练习所学内容。
配置依赖项
应用代码使用数据库来保留其余服务数据。通过在 pom.xl 中添加以下代码,确保依赖项可用。
- 打开
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 对象的 Entity 模型。
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
文件中添加以下内容可让应用连接到 CloudSQL 实例。
- TARGET - 配置此变量以指示执行应用的环境
- SPRING_PROFILES_ACTIVE - 显示有效的 Spring 配置文件,该配置文件将配置为
cloud-dev
- DB_HOST - 数据库的专用 IP。在创建数据库实例时或通过点击 Google Cloud 控制台导航菜单中的
SQL
时,系统已记下该 IP - 请更改值! - 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 编辑器底部的窗格中,选择“Cloud Code”,然后选择屏幕顶部的“在 Kubernetes 上调试”。
- 构建和测试完成后,“Output”(输出)标签页会显示
Resource deployment/demo-app status completed successfully
,并列出了一个网址:“Forwarded 网址 from service 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
- 删除报价
现在,删除您刚刚使用 delete 方法添加的引用,并观察 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 not found”状态代码。
更正错误。
- 在调试会话仍在运行的情况下,按“continue”(继续)完成请求按钮。
- 接下来,将
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 进行 InnerLoop 开发
- 创建新的 Java 起始应用
- 开发过程介绍
- 开发简单的 CRUD REST 服务
- 在 GKE 集群上调试应用
- 将应用连接到 CloudSQL 数据库