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

## 概要
- Google App Engine Java 11 スタンダード環境 + Spring Boot の構成でシンプルな Hello World 表示 Web アプリケーションを作る
- Google App Engine Java 11 スタンダード環境の正式版リリースは2019年10月30日にアナウンスされている
- Java 11 リリースは2018年9月25日にアナウンスされている

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

## Java 8 スタンダード環境から Java 11 への基本的な箇所の変更点
- インスタンスクラスのメモリが2倍に増えた (例えば F1 は 128MB が 256MB になった)
- アプリケーション設定ファイルが appengine-web.xml から app.yaml に変更された
- war ファイルではなく、実行可能な jar ファイルをデプロイする必要がある
 
## ソースコード

### ソースコード一覧

```
├── build.gradle
├── settings.gradle
└── src
    ├── main
    │   ├── appengine
    │   │   └── app.yaml
    │   ├── java
    │   │   └── com
    │   │       └── example
    │   │           └── helloworld
    │   │               ├── HelloworldApplication.java
    │   │               ├── HelloworldController.java
    │   │               └── HelloworldErrorController.java
    │   └── resources
    │       ├── application.yml
    │       ├── static
    │       │   └── assets
    │       │       └── helloworld.png
    │       └── templates
    │           ├── error.html
    │           └── helloworld.html
    └── 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'
  // 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'
  // 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 11 を使用
sourceCompatibility = '11'
targetCompatibility = '11'

// 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
```

参考:

- [Using Gradle and the App Engine Plugin  \|  App Engine standard environment for Java 11 docs  \|  Google Cloud](https://cloud.google.com/appengine/docs/standard/java11/using-gradle?hl=ja)
- [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)
- [Spring Boot Gradle Plugin Reference Guide](https://docs.spring.io/spring-boot/docs/2.2.0.RELEASE/gradle-plugin/reference/html/)
- [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'
```

### app.yaml

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

```yaml
# Java 11 を使う
runtime: java11

# Java 8 ランタイムの F1 ではメモリが 128MB だったが Java 11 では 256MB
instance_class: F1

# インスタンス数のオートスケール設定
automatic_scaling:
  max_instances: 1 # 最大インスタンス数
  min_instances: 0 # 最小インスタンス数。0にすると使われていないときはインスタンス数が0になる
  target_cpu_utilization: 0.95 # 新しいインスタンスを立ち上げるトリガーとなるCPU負荷率(0.5から0.95の間で指定)
  max_concurrent_requests: 80 # 許容する同時リクエスト数(指定できる最大値は80)

# 環境変数を設定
env_variables:
  JAVA_TOOL_OPTIONS: "-XX:MaxRAM=256m -XX:ActiveProcessorCount=2 -Xmx32m"
```

参考:

- [Defining Runtime Settings  \|  App Engine standard environment for Java 11 docs  \|  Google Cloud](https://cloud.google.com/appengine/docs/standard/java11/configuring-your-app-with-app-yaml)
- [app\.yaml Configuration File  \|  App Engine standard environment for Java 11 docs  \|  Google Cloud](https://cloud.google.com/appengine/docs/standard/java11/config/appref?hl=ja)

### 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");

    // 表示するデータをセット
    ModelAndView mav = new ModelAndView();
    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 にする
    // 必要に応じてステータコードや出力内容をカスタマイズ可能
    mav.setStatus(HttpStatus.NOT_FOUND);
    mav.setViewName("error"); // error.html
    return mav;
  }
}
```

### 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="'application.yml: ' + ${applicationYamlMessage}"></div>
<div><img src="./assets/helloworld.png"></div>
</body>
</html>
```

### 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 test 
```

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

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

```
$ gradle bootRun
```

bootRun タスクは Spring Boot Gradle Plugin の機能のため、Google App Engine 用の設定ファイルである app.yaml は読み込まない。

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

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

```
$ gradle appengineDeploy
```

### Gradle で利用可能なタスク

gradle tasks で利用可能なタスク一覧を見ることができる。

```
$ gradle tasks

> Task :tasks

------------------------------------------------------------
Tasks runnable from root project
------------------------------------------------------------

App Engine app.yaml based projects tasks
----------------------------------------
appengineCloudSdkLogin - Login and set the Cloud SDK common configuration user
appengineDeploy - Deploy an App Engine application
appengineDeployAll - Deploy an App Engine application and all of its config files
appengineDeployCron - Deploy Cron configuration
appengineDeployDispatch - Deploy Dispatch configuration
appengineDeployDos - Deploy Dos configuration
appengineDeployIndex - Deploy Index configuration
appengineDeployQueue - Deploy Queue configuration
appengineShowConfiguration - Show current App Engine plugin configuration
appengineStage - Stage an App Engine app.yaml based project for deployment
checkCloudSdk - Validates the Cloud SDK
downloadCloudSdk - Download the Cloud SDK

Application tasks
-----------------
bootRun - Runs this project as a Spring Boot application.
(以下略)
```

## 参考資料
- [Java 11 runtimes come to App Engine \| Google Cloud Blog](https://cloud.google.com/blog/products/application-development/app-engine-java-11-is-ga-deploy-a-jar-scale-it-all-fully-managed)
- [Turn it up to eleven: Java 11 runtime comes to App Engine \| Google Cloud Blog](https://cloud.google.com/blog/products/application-development/turn-it-up-to-eleven-java-11-runtime-comes-to-app-engine)
- [Google App Engine Java 11 Standard Environment documentation  \|  App Engine standard environment for Java 11 docs  \|  Google Cloud](https://cloud.google.com/appengine/docs/standard/java11/?hl=ja)
- [Migrating your App Engine app from Java 8 to Java 11  \|  App Engine standard environment for Java 11 docs  \|  Google Cloud](https://cloud.google.com/appengine/docs/standard/java11/java-differences?hl=ja)
- [java\-docs\-samples/appengine\-java11 at master · GoogleCloudPlatform/java\-docs\-samples · GitHub](https://github.com/GoogleCloudPlatform/java-docs-samples/tree/master/appengine-java11)
- [Spring Boot Reference Documentation](https://docs.spring.io/spring-boot/docs/2.2.0.RELEASE/reference/htmlsingle/)
- [Google App EngineのJava 11アプリをGradleでビルドする（Spring Boot \+ Kotlin \+ Gradle Kotlin DSL） \- Qiita](https://qiita.com/orangain/items/7a072ce53e931c7f6ee0)