Google App Engine (Java 8) + Spring Boot + Gradle で Hello World

## 概要
- Google App Engine Java 8 スタンダード環境 + Spring Boot の構成でシンプルな Hello World 表示 Web アプリケーションを作る

## 環境
- Google App Engine Java 8 スタンダード環境
- Spring Boot 2.2.0
- Gradle 6.0
- JUnit 5
- Thymeleaf 3

## ソースコード

### ソースコード一覧

```
├── build.gradle
├── settings.gradle
└── src
    ├── main
    │   ├── java
    │   │   └── com
    │   │       └── example
    │   │           └── helloworld
    │   │               ├── HelloworldApplication.java
    │   │               ├── HelloworldController.java
    │   │               ├── HelloworldErrorController.java
    │   │               └── HelloworldServletInitializer.java
    │   ├── resources
    │   │   ├── application.yml
    │   │   ├── static
    │   │   │   └── assets
    │   │   │       └── helloworld.png
    │   │   └── templates
    │   │       ├── error.html
    │   │       └── helloworld.html
    │   └── webapp
    │       └── WEB-INF
    │           └── appengine-web.xml
    └── test
        └── java
            └── com
                └── example
                    └── helloworld
                        └── HelloworldApplicationTests.java
```

### build.gradle

Gradle でビルドに関する処理を記述するファイル。
Google App Engine Gradle plugin はバージョン2系を使用。

```build.gradle
buildscript {
  repositories {
    mavenCentral()
  }
  dependencies {
    // Spring Boot Gradle Plugin を使用
    classpath 'org.springframework.boot:spring-boot-gradle-plugin:2.2.0.RELEASE'
    // Google App Engine Gradle plugin を使用
    classpath 'com.google.cloud.tools:appengine-gradle-plugin:2.2.0'
  }
}

plugins {
  // Java プラグインを導入
  id 'java'
  // War プラグインを導入
  id 'war'
  // https://plugins.gradle.org/plugin/org.springframework.boot
  id 'org.springframework.boot' version '2.2.0.RELEASE'
  // https://plugins.gradle.org/plugin/io.spring.dependency-management
  id 'io.spring.dependency-management' version '1.0.8.RELEASE'
}

// App Engine プラグインを導入
apply plugin: 'com.google.cloud.tools.appengine'

repositories {
  mavenCentral()
}

dependencies {
  // App Engine API の最新版
  implementation 'com.google.appengine:appengine-api-1.0-sdk:+'
  // Thymeleaf
  implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
  // Spring Web
  implementation 'org.springframework.boot:spring-boot-starter-web'
  // 組み込み Tomcat はデプロイの際には使わない
  providedRuntime 'org.springframework.boot:spring-boot-starter-tomcat'
  // Test
  testImplementation('org.springframework.boot:spring-boot-starter-test') {
    // JUnit 4 のサポートを除外する
    exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
  }
}

test {
  // JUnit 5 のサポートを有効にする
  useJUnitPlatform()

  testLogging {
    // テスト時の標準出力と標準エラー出力を表示する
    showStandardStreams true
    // イベントを出力する (TestLogEvent)
    events 'started', 'skipped', 'passed', 'failed'
  }
}

// Web アプリケーションのグループIDとバージョン
group   = "com.example.helloworld"
version = "0.0.1"

// Java 8 を使用
sourceCompatibility = '1.8'
targetCompatibility = '1.8'

// Google App Engine タスクの設定
appengine {
  // デプロイ時の設定
  // GCLOUD_CONFIG を指定しておくと
  // gcloud config で設定しているプロジェクト情報がセットされる
  deploy {
    // デプロイ先の Google Cloud Project ID
    projectId = "GCLOUD_CONFIG"
    // デプロイによって反映される Web アプリのバージョン
    // 指定しなければ新しく生成される
    version = "GCLOUD_CONFIG"
  }
}

// デプロイ前にテストを実行
appengineDeploy.dependsOn test
appengineStage.dependsOn test
```

参考:

- [GitHub \- GoogleCloudPlatform/app\-gradle\-plugin: Gradle plugin to build and deploy Google App Engine applications\.](https://github.com/GoogleCloudPlatform/app-gradle-plugin)
- [app\-gradle\-plugin/USER\_GUIDE\.md at master · GoogleCloudPlatform/app\-gradle\-plugin · GitHub](https://github.com/GoogleCloudPlatform/app-gradle-plugin/blob/master/USER_GUIDE.md)
- [Gradle と App Engine プラグインを使用する  \|  Java 8 の App Engine スタンダード環境  \|  Google Cloud](https://cloud.google.com/appengine/docs/standard/java/tools/gradle?hl=ja)
- [App Engine Gradle プラグインのタスクとプロパティ  \|  Java 8 の App Engine スタンダード環境  \|  Google Cloud](https://cloud.google.com/appengine/docs/standard/java/tools/gradle-reference)
- [Test \- Gradle DSL Version 6\.0](https://docs.gradle.org/6.0/dsl/org.gradle.api.tasks.testing.Test.html)
- [TestLogEvent \(Gradle API 6\.0\)](https://docs.gradle.org/6.0/javadoc/org/gradle/api/tasks/testing/logging/TestLogEvent.html)

### settings.gradle

setting.gradle を設置しないと親ディレクトリを辿って setting.gradle を探しにいってしまうのでシングルプロジェクトでも置いておく。

```settings.gradle
rootProject.name = 'helloworld'
```

### HelloworldApplication.java

アプリケーションクラス。Spring Boot を使うための定型的な処理だけを書いている。

```java
package com.example.helloworld;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class HelloworldApplication {

  public static void main(String[] args) {
    SpringApplication.run(HelloworldApplication.class, args);
  }
}
```

### HelloworldController.java

コントローラークラス。

```java
package com.example.helloworld;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.servlet.ModelAndView;

@Controller
public class HelloworldController {

  /**
   * application.yml から取得したメッセージ。
   */
  @Value("${application.message}")
  private String applicationYamlMessage;

  /**
   * トップページのレスポンスを返す。
   *
   * @return ページ表示情報
   */
  @GetMapping("/")
  public ModelAndView index() {

    System.out.println("HelloworldController#index");

    // システム・プロパティから取得
    String systemPropertyMessage = System.getProperty("com.example.helloworld.message");

    // 表示するデータをセット
    ModelAndView mav = new ModelAndView();
    mav.addObject("systemPropertyMessage", systemPropertyMessage);
    mav.addObject("applicationYamlMessage", applicationYamlMessage);
    mav.setViewName("helloworld"); // ビュー名。Thymeleaf テンプレートファイルを指定

    return mav;
  }

  /**
   * エラーページを表示するテスト用メソッド。
   */
  @GetMapping("/exception/")
  public void exception() {
    System.out.println("HelloworldController#exception");
    throw new RuntimeException("This is a sample exception.");
  }
}
```

### HelloworldErrorController.java

Web アプリケーション全体のエラーコントローラークラス。
一般的なコントローラクラスでは捕捉できない Not Found などを処理している。
ここでは最低限の処理しか書いていないが、必要に応じてカスタマイズする。

```java
package com.example.helloworld;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.web.servlet.error.ErrorController;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;

/**
 * Web アプリケーション全体のエラーコントローラー。
 * ErrorController インターフェースの実装クラス。
 */
@Controller
@RequestMapping("${server.error.path:${error.path:/error}}") // エラーページへのマッピング
public class HelloworldErrorController implements ErrorController {

  /**
   * エラーページのパス。
   */
  @Value("${server.error.path:${error.path:/error}}")
  private String errorPath;

  /**
   * エラーページのパスを返す。
   *
   * @return エラーページのパス
   */
  @Override
  public String getErrorPath() {
    return errorPath;
  }

  /**
   * レスポンス用の ModelAndView オブジェクトを返す。
   *
   * @param req リクエスト情報
   * @param mav レスポンス情報
   * @return HTML レスポンス用の ModelAndView オブジェクト
   */
  @RequestMapping
  public ModelAndView error(HttpServletRequest req, ModelAndView mav) {

    System.out.println("HelloWorldErrorController#error");

    // どのエラーでも 404 Not Found にする
    // 必要に応じてステータコードや出力内容をカスタマイズ可能
    HttpStatus status = HttpStatus.NOT_FOUND;
    mav.setStatus(status);
    mav.setViewName("error"); // error.html
    return mav;
  }
}
```

### HelloworldServletInitializer.java

WAR ファイルをデプロイして動作させる環境で必要な WebApplicationInitializer 実装クラス。
Google App Engine では WAR ファイルをデプロイして動作させるためこのクラスが必要になる。

```java
package com.example.helloworld;

import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;

public class HelloworldServletInitializer extends SpringBootServletInitializer {

  @Override
  protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
    System.out.println("HelloworldServletInitializer#configure");
    return application.sources(HelloworldApplication.class);
  }
}
```

参考: [SpringBootServletInitializer \(Spring Boot Docs 2\.2\.0\.RELEASE API\)](https://docs.spring.io/spring-boot/docs/2.2.0.RELEASE/api/org/springframework/boot/web/servlet/support/SpringBootServletInitializer.html)

### application.yml

Web アプリケーション設定情報を記述するファイル。application.properties でも良い。
今回は、アプリケーション独自の情報だけ設定している。

```yaml
application:
  message: Hello, application yaml.
```

### helloworld.png

静的ファイル置き場を示すサンプルとして置いている画像。
静的ファイルは src/main/resources/static に置くと、http://hostname/ にマッピングされる。
今回は http://hostname/assets/helloworld.png にアクセスしたときに src/main/resources/static/assets/helloworld.png のファイルを配信するような構成になっている。

### error.html

エラーが発生する際に表示する HTML の Thymeleaf テンプレートファイル。
今回は動的な値を埋め込んでいないが、必要に応じてエラーコントローラークラスで値をセットして、テンプレート側で表示するようにカスタマイズすることが可能。

```html
<!DOCTYPE html>
<html lang="ja" xmlns:th="http://www.thymeleaf.org">
<head>
  <meta charset="UTF-8">
  <title>404 Not Found</title>
</head>
<body>
<h1>404 Not Found</h1>
</body>
</html>
```

### helloworld.html

コントローラークラスがセットした値を表示するための HTML の Thymeleaf テンプレートファイル。

```html
<!DOCTYPE html>
<html lang="ja" xmlns:th="http://www.thymeleaf.org">
<head>
  <meta charset="UTF-8">
  <title>Hello, world.</title>
</head>
<body>
<h1>Hello, world.</h1>
<div th:text="'System Property: ' + ${systemPropertyMessage}"></div>
<div th:text="'application.yml: ' + ${applicationYamlMessage}"></div>
<div><img src="./assets/helloworld.png"></div>
</body>
</html>
```

### appengine-web.xml

Google App Engine 用の設定ファイル。Web アプリケーションの情報を記述する。

```xml
<?xml version="1.0" encoding="utf-8"?>
<appengine-web-app xmlns="http://appengine.google.com/ns/1.0">

  <!-- Java VM -->
  <runtime>java8</runtime>

  <!-- スレッドセーフ設定 -->
  <threadsafe>true</threadsafe>

  <!-- インスタンス数のオートスケール設定 -->
  <automatic-scaling>
    <!-- ここで指定したCPU負荷率を超えたら新しいインスタンスを立ち上げる -->
    <target-cpu-utilization>0.95</target-cpu-utilization>
    <!-- 最小インスタンス数。0にすると使われていないときはインスタンス数が0になる -->
    <min-instances>0</min-instances>
    <!-- 最大インスタンス数 -->
    <max-instances>1</max-instances>
    <!-- 許容する同時リクエスト数 -->
    <max-concurrent-requests>80</max-concurrent-requests>
  </automatic-scaling>

  <!-- 静的ファイル -->
  <static-files>
    <include path="/assets/**.*"/>
  </static-files>

  <!-- Google App Engine 上や gradle appengineRun で動かしているときに有効になるシステム・プロパティ -->
  <system-properties>
    <property name="com.example.helloworld.message" value="Hello, system property."/>
  </system-properties>
</appengine-web-app>
```

参考: [appengine\-web\.xml リファレンス  \|  Java 8 の App Engine スタンダード環境  \|  Google Cloud](https://cloud.google.com/appengine/docs/standard/java/config/appref?hl=ja)

### HelloworldApplicationTests.java

最低限のテストクラス。形式的なことだけ記述している。実際には何もテストしない。

```java
package com.example.helloworld;

import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
class HelloworldApplicationTests {

  @Test
  void contextLoads() {
  }
}
```

## オペレーション

### テストを実行

gradle の test タスクでテストを実行できる。

```
$ gradle test 
```

### gradle appengineRun でローカルにサーバを起動

gradle appengineRun でローカルサーバ http://localhost:8080/ を起動できる。

```
$ gradle appengineRun
```

### gradle appengineDeploy で Google App Engine にデプロイ

gradle appengineDeploy で Google App Engine にデプロイできる。

```
$ gradle appengineDeploy
```

## その他

### Web アプリケーション全体のエラーハンドリングが適用されない

エラーコントローラーなどで独自のエラー処理が使用されない場合もある。

[デプロイ記述子: web\.xml  \|  Java 8 の App Engine スタンダード環境  \|  Google Cloud](https://cloud.google.com/appengine/docs/standard/java/config/webxml?hl=ja)

> 注: 現在のところ、一部のエラー条件ではカスタム エラーハンドラを構成できません。具体的には、特定の URL にサーブレット マッピングが定義されていない場合は HTTP 404 レスポンス ページをカスタマイズできません。また、「403 割り当てエラー」のページや、App Engine の内部エラーにより表示される「500 サーバーエラー」のページもカスタマイズできません。

実際に試してみたところ、
gradle appengineRun でローカルに立てたサーバでは 404 Not Found が独自にカスタマイズしたものになってくれなかった。
gradle appengineDeploy で Google App Engine にデプロイしたものは独自にカスタマイズした 404 Not Found が表示された。

### 参考資料
- [Google App Engine Java スタンダード環境のドキュメント  \|  Java 8 の App Engine スタンダード環境  \|  Google Cloud](https://cloud.google.com/appengine/docs/standard/java/?hl=ja)
- [Spring Boot Reference Documentation](https://docs.spring.io/spring-boot/docs/2.2.0.RELEASE/reference/htmlsingle/)
- [getting\-started\-java/appengine\-standard\-java8/springboot\-appengine\-standard at master · GoogleCloudPlatform/getting\-started\-java · GitHub](https://github.com/GoogleCloudPlatform/getting-started-java/tree/master/appengine-standard-java8/springboot-appengine-standard)