Java を使用したインナーループ開発 - SpringBoot

1. 概要

このラボでは、コンテナ化された環境で Java アプリケーションを開発するソフトウェア エンジニア向けに、開発ワークフローを効率化するための特長と機能を紹介します。一般的なコンテナ開発では、ユーザーがコンテナの詳細とコンテナのビルドプロセスを理解する必要があります。さらに、デベロッパーは通常、作業の中断を余儀なくされ、IDE から離れてリモート環境でアプリケーションのテストやデバッグを行う必要もあります。このチュートリアルで説明するツールとテクノロジーを使用すると、デベロッパーは IDE を離れることなく、コンテナ化されたアプリケーションを効果的に操作できます。

学習内容

このラボでは、GCP でコンテナを使用して開発するための次のような方法について学びます。

  • 設定と要件
  • 新しい Java スターター アプリケーションを作成する
  • 開発プロセスの説明
  • シンプルな CRUD REST サービスを開発する
  • クリーンアップ

2. 設定と要件

セルフペース型の環境設定

  1. Google Cloud Console にログインして、プロジェクトを新規作成するか、既存のプロジェクトを再利用します。Gmail アカウントも Google Workspace アカウントもまだお持ちでない場合は、アカウントを作成してください。

b35bf95b8bf3d5d8.png

a99b7ace416376c4.png

bd84a6d3004737c5.png

  • プロジェクト名は、このプロジェクトの参加者に表示される名称です。Google API では使用されない文字列で、いつでも更新できます。
  • プロジェクト ID は、すべての Google Cloud プロジェクトにおいて一意でなければならず、不変です(設定後は変更できません)。Cloud Console により一意の文字列が自動生成されます(通常は内容を意識する必要はありません)。ほとんどの Codelab では、プロジェクト ID を参照する必要があります(通常、プロジェクト ID は「PROJECT_ID」の形式です)。好みの文字列でない場合は、別のランダムな ID を生成するか、独自の ID を試用して利用可能であるかどうかを確認することができます。プロジェクトの作成後、ID は「フリーズ」されます。
  • 3 つ目の値として、一部の API が使用するプロジェクト番号があります。これら 3 つの値について詳しくは、こちらのドキュメントをご覧ください。
  1. 次に、Cloud のリソースや API を使用するために、Cloud Console で課金を有効にする必要があります。この Codelab の操作をすべて行って、費用が生じたとしても、少額です。このチュートリアルを終了した後に課金が発生しないようにリソースをシャットダウンするには、Codelab の最後にある「クリーンアップ」の手順を行います。Google Cloud の新規ユーザーは、300 米ドル分の無料トライアル プログラムをご利用いただけます。

Cloudshell エディタを起動する

このラボは、Google Cloud Shell エディタで使用するように設計、テストされています。エディタにアクセスするには、

  1. https://console.cloud.google.com から Google プロジェクトにアクセスします。
  2. 右上にある Cloud Shell エディタのアイコンをクリックします。

8560cc8d45e8c112.png

  1. ウィンドウの下部に新しいペインが開きます
  2. [エディタを開く] ボタンをクリックします。

9e504cb98a6a8005.png

  1. エディタが開き、右側にエクスプローラ、中央部分にエディタが表示されます
  2. 画面の下部にターミナル ペインも表示されます。
  3. ターミナルが開いていない場合は、`Ctrl+` キーの組み合わせを使用して新しいターミナル ウィンドウを開きます。

gcloud を設定する

Cloud Shell で、プロジェクト ID とアプリケーションのデプロイ先のリージョンを設定します。これらを PROJECT_ID 変数と REGION 変数として保存します。

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 DevTools を有効にするには、エディタでエクスプローラから pom.xml を見つけて開きます。次に、<description>Spring Boot のデモ プロジェクト</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>

ビルドファイルの変更に関するプロンプトが表示されたら、[Always] を選択します。

447a90338f51931f.png

マニフェストを生成する

Skaffold には、コンテナ開発を簡素化する統合ツールが用意されています。このステップでは、ベース Kubernetes YAML ファイルを自動的に作成する skaffold を初期化します。このプロセスでは、Dockerfile などのコンテナ イメージ定義を含むディレクトリを特定し、それぞれに対して Deployment と Service のマニフェストを作成します。

以下のコマンドを実行してプロセスを開始します。

  1. ターミナルで次のコマンドを実行します。
skaffold init --generate-manifests
  1. プロンプトが表示されたら、次の操作を行います。
  • 矢印を使用して Jib Maven Plugin にカーソルを移動します。
  • Space キーを押してオプションを選択します。
  • Enter キーを押して続行
  1. ポートに「8080」と入力します。
  2. y」と入力して構成を保存します。

ワークスペースのビジュアリゼーションに skaffold.yamldeployment.yaml の 2 つのファイルが追加されます。

アプリ名を更新

現在、構成に含まれるデフォルト値は、アプリケーションの名前と一致していません。デフォルト値ではなく、アプリケーション名を参照するようにファイルを更新します。

  1. Skaffold 構成のエントリを変更する
  • skaffold.yaml を開きます。
  • 現在 pom-xml-image として設定されているイメージ名を選択します
  • 右クリックして [すべてのオカレンスを変更] を選択します。
  • 新しい名前として「demo-app」と入力します。
  1. Kubernetes 構成のエントリを変更する
  • deployment.yaml ファイルを開く
  • 現在 pom-xml-image として設定されているイメージ名を選択します
  • 右クリックして [すべてのオカレンスを変更] を選択します。
  • 新しい名前として「demo-app」と入力します。

ホット同期を有効にする

ホットリロードのエクスペリエンスを最適化するには、Jib が提供する同期機能を使用します。このステップでは、ビルドプロセスでその機能を使用するように Skaffold を構成します。

「同期」はskaffold 構成で構成しているプロファイルは、Spring の「同期」を活用します。前のステップで構成し、spring-dev-tools のサポートを有効にしたプロファイル。

  1. 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/java:debug
    sync:
      auto: true

デフォルト ルートの追加

HelloController.java というファイルを /src/main/java/com/example/springboot/ に作成します。

次の内容をファイルに貼り付けて、デフォルトの 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 に push し、アプリケーションを GKE にデプロイします。この処理はバックグラウンドで行われ、デベロッパー フローでは詳細が抽象化されます。また、Cloud Code は、コンテナベースの開発に従来のデバッグ機能とホットシンク機能を提供することで開発プロセスを強化します。

Kubernetes へのデプロイ

  1. Cloud Shell エディタの下部にあるペインで、[Cloud Code] を選択します。

fdc797a769040839.png

  1. 上部に表示されるパネルで、[Debug on Kubernetes] を選択します。プロンプトが表示されたら、[はい] を選択して現在の Kubernetes コンテキストを使用します。

cfce0d11ef307087.png

  1. コマンドを初めて実行すると、画面の上部にプロンプトが表示され、現在の Kubernetes コンテキストが必要かどうかを尋ね、[はい] を選択します。現在のコンテキストを受け入れて使用します。

817ee33b5b412ff8.png

  1. 次に、使用する Container Registry を尋ねるプロンプトが表示されます。Enter キーを押して、指定されたデフォルト値を受け入れます。

eb4469aed97a25f6.png

  1. 下部のペインで [Output] タブを選択すると、進行状況と通知が表示されます。

f95b620569ba96c5.png

  1. [Kubernetes: Run/Debug - Detailed] を選択します。右側のチャネル プルダウンから、追加の詳細情報や、コンテナからライブ ストリーミングされるログを確認できます。

94acdcdda6d2108.png

  1. [Kubernetes: Run/Debug] を選択すると、簡素化されたビューに戻ります。プルダウンから
  2. ビルドとテストが完了すると、[Output] タブに Resource deployment/demo-app status completed successfully と表示され、URL として「Forwarded URL from service demo-app: http://localhost:8080」が表示されます。
  3. Cloud Code ターミナルで、出力の URL(http://localhost:8080)にカーソルを合わせ、表示されたツールチップで [Open Web Preview] を選択します。

レスポンスは次のようになります。

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 でリモートで実行されているアプリケーションの Sand 状態を調査できるようになります。
  5. [変数] 欄をクリックして、[ターゲット] を探します。変数です。
  6. 現在の値が「local」であることを確認します。
  7. 変数名「target」をダブルクリックします。ポップアップで“Cloud”など別の名前に変更し
  8. デバッグ用コントロール パネルで [続行] ボタンをクリックします。
  9. ブラウザでレスポンスを確認します。入力した値が更新されています。

ホットリロード

  1. 「Hello from %s Code」など、別の値を返すようにステートメントを変更します。
  2. ファイルが自動的に保存され、GKE のリモート コンテナに同期されます。
  3. ブラウザを更新して、更新された結果を確認してください。
  4. デバッグ ツールバーの赤い正方形 a13d42d726213e6c.png をクリックして、デバッグ セッションを停止します

5. シンプルな CRUD REST サービスを開発する

これで、アプリケーションはコンテナ化された開発用に完全に構成され、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>

REST サービスをコーディングする

Quote.java

Quote.java というファイルを /src/main/java/com/example/springboot/ に作成し、次のコードをコピーします。アプリケーションで使用する Quote オブジェクトのエンティティ モデルを定義します。

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 ファイルに追加すると、アプリケーションを Cloud SQL インスタンスに接続できます。

  • TARGET - アプリが実行される環境を示すように変数を構成します
  • SPRING_PROFILES_ACTIVE - アクティブな Spring プロファイルを示します。これは cloud-dev に構成されます。
  • DB_HOST - データベースのプライベート IP。この IP は、データベース インスタンスの作成時に、または Google Cloud コンソールのナビゲーション メニューで SQL をクリックして確認できます。値を変更してください。
  • DB_USER と DB_PASS - Cloud SQL インスタンス構成で設定され、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] を選択し、画面上部の [Debug on Kubernetes] を選択します。
  2. ビルドとテストが完了すると、[Output] タブに Resource deployment/demo-app status completed successfully と表示され、URL として「Forwarded URL from service demo-app: http://localhost:8080」が表示されます。
  3. ランダムな引用を表示

cloudshell ターミナルから、ランダム引用符のエンドポイントに対して以下のコマンドを複数回実行します。異なる引用符を返す繰り返し呼び出しを確認する

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 Not Found ステータス コードを返すように例外ブロックをリファクタリングする必要があります。

エラーを修正します。

  1. デバッグ セッションが実行中のままで、[続行] をクリックしてリクエストを完了します。[デバッグ コントロール パネル] ボタンを使用します。
  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. プロジェクトを削除して、関連するすべてのインフラストラクチャとリソースを削除する