使用 Java 进行 InnerLoop 开发 - SpringBoot

1. 概览

本实验演示了一些特性和功能,这些特性和功能旨在简化在容器化环境中开发 Java 应用的软件工程师的开发工作流。典型的容器开发要求用户了解容器和容器构建流程的详细信息。此外,开发者通常需要中断他们的流程,离开 IDE,以在远程环境中测试和调试其应用。借助本教程中提到的工具和技术,开发者无需离开 IDE 即可高效使用容器化应用。

学习内容

在本实验中,您将学习在 GCP 中使用容器进行开发的方法,包括:

  • 设置和要求
  • 创建新的 Java 起始应用
  • 开发过程介绍
  • 开发简单的 CRUD 静态服务
  • 清理

2. 设置和要求

自定进度的环境设置

  1. 登录 Google Cloud 控制台,然后创建一个新项目或重复使用现有项目。如果您还没有 Gmail 或 Google Workspace 账号,则必须创建一个

b35bf95b8bf3d5d8.png

a99b7ace416376c4.png

bd84a6d3004737c5.png

  • 项目名称是此项目参与者的显示名称。它是 Google API 尚未使用的字符串,您可以随时对其进行更新。
  • 项目 ID 在所有 Google Cloud 项目中必须是唯一的,并且不可变(一经设置便无法更改)。Cloud Console 会自动生成一个唯一字符串;通常情况下,您无需关注该字符串。在大多数 Codelab 中,您都需要引用项目 ID(它通常标识为 PROJECT_ID),因此如果您不喜欢某个 ID,请再生成一个随机 ID,还可以尝试自己创建一个,并确认是否可用。然后,项目创建后,ID 会处于“冻结”状态。
  • 第三个值是一些 API 使用的项目编号。如需详细了解所有这三个值,请参阅文档
  1. 接下来,您需要在 Cloud Console 中启用结算功能,才能使用 Cloud 资源/API。运行此 Codelab 应该不会产生太多的费用(如果有费用的话)。要关闭资源以避免产生超出本教程范围的费用,请按照此 Codelab 末尾提供的任何“清理”说明操作。Google Cloud 的新用户符合参与 $300 USD 免费试用计划的条件。

启动 Cloudshell 编辑器

本实验旨在与 Google Cloud Shell Editor 搭配使用,并经过测试。要访问该编辑器,请按以下步骤操作:

  1. 通过 https://console.cloud.google.com 访问您的 Google 项目。
  2. 点击右上角的 Cloud Shell 编辑器图标

8560cc8d45e8c112

  1. 窗口底部会打开一个新窗格
  2. 点击“打开编辑器”按钮

9e504cb98a6a8005

  1. 编辑器将打开,右侧为探索器,中心区域为编辑器
  2. 屏幕底部还应提供一个终端窗格
  3. 如果终端未打开,请使用 `ctrl+` 的组合键打开新的终端窗口

设置 gcloud

在 Cloud Shell 中,设置项目 ID 以及要将应用部署到的区域。将它们保存为 PROJECT_IDREGION 变量。

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 数据库中的数据。下面的设置脚本会为您准备此基础架构。预配过程将需要 10 分钟以上。在设置处理期间,您可以继续执行接下来的几个步骤。

./setup.sh

3. 创建新的 Java 起始应用

在本部分中,您将使用 Spring.io 提供的示例应用从头开始创建一个新的 Java Spring Boot 应用。

克隆示例应用

  1. 创建起始应用
curl  https://start.spring.io/starter.zip -d dependencies=web -d type=maven-project -d javaVersion=11 -d packageName=com.example.springboot -o sample-app.zip
  1. 解压缩应用
unzip sample-app.zip -d sample-app
  1. 切换到 sample-app 目录,然后在 Cloud Shell IDE 工作区中打开该文件夹
cd sample-app && cloudshell workspace .

添加 Spring-boot-devtools 和吉布

如需启用 Spring Boot 开发者工具,请在编辑器中从资源管理器中找到并打开 pom.xml。接下来,将以下代码粘贴到显示 <description>Spring Boot Demo 项目</description>的说明行之后。

  1. 在 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>
  1. 在 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>

如果系统提示更改 build 文件,请选择 Always

447a90338f51931f

生成清单

Skaffold 提供集成工具以简化容器开发。在此步骤中,您将初始化 skaffold,它会自动创建基础 Kubernetes YAML 文件。该过程会尝试识别包含容器映像定义的目录(例如 Dockerfile),然后为每个目录创建部署和服务清单。

执行以下命令以开始该过程。

  1. 在终端中执行以下命令
skaffold init --generate-manifests
  1. 当系统提示时:
  • 使用箭头将光标移至 Jib Maven Plugin
  • 按空格键即可选择相应选项。
  • 按 Enter 键即可继续
  1. 输入 8080 作为端口
  2. 输入 y 以保存配置

向工作区可视化图表添加了 skaffold.yamldeployment.yaml 两个文件

更新应用名称

配置中包含的默认值目前与应用的名称不匹配。更新文件以引用您的应用名称,而不是默认值。

  1. 更改 Skaffold 配置中的条目
  • 打开skaffold.yaml
  • 选择当前设为“pom-xml-image”的映像名称
  • 右键点击并选择“更改所有出现次数”
  • 输入新名称 demo-app
  1. 更改 Kubernetes 配置中的条目
  • 打开 deployment.yaml 文件
  • 选择当前设为“pom-xml-image”的映像名称
  • 右键点击并选择“更改所有出现次数”
  • 输入新名称 demo-app

启用热同步

为了优化热重载体验,您需要使用 Jib 提供的同步功能。在此步骤中,您将配置 Skaffold 以在构建流程中使用该功能。

请注意,“同步”命令您在 Skaffold 配置中配置的配置文件利用了 Spring“sync”您在上一步中配置的配置文件,其中启用了对 spring-dev-tools 的支持。

  1. 更新 Skaffold 配置

在 skaffold.yaml 文件中,将该文件的整个构建部分替换为以下规范。请勿更改文件的其他部分。

skaffold.yaml

build:
  artifacts:
  - image: demo-app
    jib:
      project: com.example:demo
      type: maven
      args: 
      - --no-transfer-progress
      - -Psync
      fromImage: gcr.io/distroless/java: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 还可以为基于容器的开发提供传统的调试和热同步功能,从而增强您的开发流程。

将容器部署到 Kubernetes

  1. 在 Cloud Shell Editor 底部的窗格中,选择 Cloud Code 

fdc797a769040839.png

  1. 在顶部显示的面板中,选择“在 Kubernetes 上调试”。如果出现提示,请选择“Yes”以使用当前的 Kubernetes 上下文。

cfce0d11ef307087.png

  1. 首次运行此命令时,屏幕顶部会显示一条提示,询问您是否需要当前的 Kubernetes 上下文,请选择“是”接受并使用当前上下文。

817ee33b5b412ff8

  1. 接下来,系统会显示一条提示,询问要使用哪个容器注册表。按 Enter 键可接受提供的默认值

eb4469aed97a25f6.png

  1. 选择下部窗格中的“Output”(输出)标签页可查看进度和通知

f95b620569ba96c5.png

  1. 选择“Kubernetes:运行/调试 - 详细”查看右侧渠道下拉菜单中的 其他详细信息和实时从容器流式传输的日志

94acdcdda6d2108

  1. 选择“Kubernetes: Run/Debug”返回简化的视图下拉菜单
  2. 构建和测试完成后,“Output”(输出)标签页会显示 Resource deployment/demo-app status completed successfully,并列出了一个网址:“Forwarded 网址 from service demo-app: http://localhost:8080”
  3. 在 Cloud Code 终端中,将鼠标悬停在输出中的网址 (http://localhost:8080) 上,然后在显示的工具提示中选择“打开网页预览”。

响应将是:

Hello from your local environment!

利用断点

  1. 打开位于 /src/main/java/com/example/springboot/HelloController.java 的 HelloController.java 应用
  2. 找到显示 return String.format("Hello from your %s environment!", target); 的根路径的 return 语句
  3. 点击行号左侧的空白处,为该行添加断点。系统会显示一个红色指示器,指明断点已设置
  4. 重新加载浏览器,并注意调试程序会在断点处停止进程,并允许您调查在 GKE 中远程运行的应用的可变沙状态
  5. 点击“变量”部分,直到找到“目标”变量。
  6. 观察到当前值为“local”
  7. 双击变量名称“target”在弹出式窗口中,将值更改为其他值,例如“Cloud”
  8. 点击调试控制台中的“继续”按钮
  9. 在浏览器中查看响应,浏览器现在会显示您刚刚输入的更新值。

热重载

  1. 更改语句以返回不同的值,例如“Hello from %s Code”
  2. 文件会自动保存并同步到 GKE 中的远程容器中
  3. 刷新浏览器以查看更新后的结果。
  4. 点击调试工具栏 a13d42d726213e6c.png 中的红色方块,停止调试会话

5. 开发简单的 CRUD 静态服务

至此,您的应用已完全针对容器化开发进行了配置,并且您已完成 Cloud Code 的基本开发工作流。在以下部分中,您将通过添加连接到 Google Cloud 中的代管式数据库的 REST 服务端点,练习所学知识。

配置依赖项

应用代码使用数据库来保留其余服务数据。通过在 pom.xl 中添加以下代码,确保依赖项可用。

  1. 打开 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>

对其余服务进行编码

Quote.java

在 /src/main/java/com/example/springboot/ 中创建一个名为 Quote.java 的文件,并复制以下代码。这定义了应用中使用的 Quote 对象的 Entity 模型。

package com.example.springboot;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.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) {
        try {
            quoteRepository.deleteById(id);
            return new ResponseEntity<>(HttpStatus.NO_CONTENT);
        } catch (RuntimeException e) {
            System.out.println(e.getMessage());
            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

部署并验证应用

  1. 在 Cloud Shell 编辑器底部的窗格中,选择“Cloud Code”,然后选择屏幕顶部的“在 Kubernetes 上调试”。
  2. 构建和测试完成后,“Output”(输出)标签页会显示 Resource deployment/demo-app status completed successfully,并列出了一个网址:“Forwarded 网址 from service demo-app: http://localhost:8080”
  3. 查看随机报价

在 cloudshell 终端中,针对 random-quote 端点多次运行以下命令。观察重复调用返回不同报价

curl -v 127.0.0.1:8080/random-quote
  1. 添加报价

使用下列命令创建一个新报价(id=6),并观察回显请求

curl -v -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 127.0.0.1:8080/quotes
  1. 删除报价

现在,删除您刚刚使用 delete 方法添加的引用,并观察 HTTP/1.1 204 响应代码。

curl -v -X DELETE 127.0.0.1:8080/quotes/6
  1. 服务器错误

在条目被删除后再次运行最后一个请求,从而遇到错误状态

curl -v -X DELETE 127.0.0.1:8080/quotes/6

请注意,响应会返回 HTTP:500 Internal Server Error

调试应用

在上一部分中,当您尝试删除数据库中不存在的条目时,发现应用中存在错误状态。在本部分中,您将设置一个断点来查找问题。DELETE 操作中发生该错误,因此您将使用 QuoteController 类。

  1. 打开 src.main.java.com.example.springboot.QuoteController.java
  2. 查找 deleteQuote() 方法
  3. 找到从数据库中删除商品所在的行:quoteRepository.deleteById(id);
  4. 点击行号左侧的空白处,在该行上设置断点。
  5. 系统会显示一个红色指示器,指示已设置断点
  6. 再次运行 delete 命令
curl -v -X DELETE 127.0.0.1:8080/quotes/6
  1. 点击左侧列中的图标切换回调试视图
  2. 您会发现,在 QuoteController 类中停止的调试行。
  3. 在调试程序中,点击 step over 图标 b814d39b2e5f3d9e.png 并观察系统会抛出异常
  4. 您会发现一个非常普通的 RuntimeException was caught.。这会向客户端返回一个内部服务器错误 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

更新代码

代码不正确,应重构异常块,以捕获 EmptyResultDataAccessException 异常并发回 HTTP 404 找不到状态代码。

更正错误。

  1. 在调试会话仍在运行的情况下,按“continue”(继续)完成请求按钮。
  2. 接下来,将以下代码块添加到代码中:
       } catch (EmptyResultDataAccessException e){
            return new ResponseEntity<HttpStatus>(HttpStatus.NOT_FOUND);
        }

该方法应如下所示:

    public ResponseEntity<HttpStatus> deleteQuote(@PathVariable("id") Integer id) {
        try {
            quoteRepository.deleteById(id);
            return new ResponseEntity<>(HttpStatus.NO_CONTENT);
        } catch(EmptyResultDataAccessException e){
            return new ResponseEntity<HttpStatus>(HttpStatus.NOT_FOUND);
        } catch (RuntimeException e) {
            System.out.println(e.getMessage());
            return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
        }
    }
  1. 重新运行删除命令
curl -v -X DELETE 127.0.0.1:8080/quotes/6
  1. 逐步执行调试程序,并观察已捕获 EmptyResultDataAccessException,并向调用方返回“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
  1. 点击调试工具栏 a13d42d726213e6c.png 中的红色方块,停止调试会话

6. 清理

恭喜!在本实验中,您从头开始创建了一个新的 Java 应用,并将其配置为与容器高效运行。然后,您按照传统应用堆栈中的相同开发者流程,将应用部署并调试到远程 GKE 集群。

在完成实验后进行清理:

  1. 删除实验中使用的文件
cd ~ && rm -rf container-developer-workshop
  1. 删除项目以移除所有相关基础架构和资源