Google App Engine Java 8 から Java11への移行 実践編

# はじめに
この記事は、[Google App Engine Java 8 から Java11 移行対策](https://qiita.com/hiroa2/items/e5b124aba1a828b14879) の続きです。
Java11からサーブレット非対応になったGoogle App Engine / Java (GAE/J)でJava8のサーブレットをGAE/J Java11 でも稼働可能なSpring Bootアプリへ置き換える具体的な方法を記述している。

# Java8 サーブレットから Java11 Spring Bootへ
今回はサンプルとして、GAE/J Java8のガイドに記載されているHello World サーブレットを [Spring Boot](https://spring.io/projects/spring-boot)化してGAE/J Java11にデプロイしてみることにする。
## 前提条件
この記事は、Java8版のGAE/JのソースをJava11版に書き換えることを主眼とするため、Java 11やCloud SDKのインストールは終了しているものとする。
さらにGAEへデプロイ可能なプロジェクトも作成済みとする。
また、WebフレームワークとしてSpring Bootを使用しているが、Spring FrameworkやSpring Bootの知識もあるものとする。

**前提とする環境**

- Java 11 (OpenJDK 11)
- Apache Maven 3.6
- Google Cloud SDK 319
- Spring Boot 2.4
- Maven 3.6


## GAE/J Java8のHello World取得
GAE/J Java8のサンプルは、下記のGitレポジトリーから取得できる

```shell
git clone https://github.com/GoogleCloudPlatform/appengine-try-java
```
取得したファイルは以下のようになっている

```:一部省略
|--pom.xml
|--src
|  |--main
|  |  |--java
|  |  |  |--myapp
|  |  |  |  |--DemoServlet.java
|  |  |--webapp
|  |  |  |--index.html
|  |  |  |--WEB-INF
|  |  |  |  |--appengine-web.xml
|  |  |  |  |--web.xml
```
web.xmlの中身を見てみよう

```xml:web.xml
<!DOCTYPE web-app PUBLIC
 "-//Oracle Corporation//DTD Web Application 2.3//EN"
 "http://java.sun.com/dtd/web-app_2_3.dtd">
<web-app xmlns="http://java.sun.com/xml/ns/javaee" version="2.5">
    <servlet>
        <servlet-name>demo</servlet-name>
        <servlet-class>myapp.DemoServlet</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>demo</servlet-name>
        <url-pattern>/demo</url-pattern>
    </servlet-mapping>
    <welcome-file-list>
        <welcome-file>index.html</welcome-file>
    </welcome-file-list>
</web-app>
```
DemoServletというサーブレットとindex.htmlというHTMLがあるだけということである。

```java:DemoServlet.java
package myapp;

import java.io.IOException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class DemoServlet extends HttpServlet {
  @Override
  public void doGet(HttpServletRequest req, HttpServletResponse resp)
      throws IOException {
    resp.setContentType("text/plain");
    resp.getWriter().println("{ \"name\": \"World\" }");
  }
}
```

```html:index.html
<!doctype html>
<html>
  <head>
    <title>App Engine Demo</title>
    <script src="//ajax.googleapis.com/ajax/libs/jquery/1.11.0/jquery.min.js"></script>
  </head>
  <body>
    <div id="result">Loading...</div>

    <script>
$(document).ready(function() {
  $.getJSON('/demo', function(data) {
    $('#result').html("Hello, " + data.name);
  });
});
    </script>
  </body>
</html>
```
index.htmlがDemoServletを呼び出して`{"name":"World"}`というJSONを受け取り、`Hello World`とDIVに表示している。
最後にappengine-web.xmlを確認する。

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

ランタイムがJava8であるということが書かれている。
なお、GAE/JのJava11版のSpring Bootを使用したHello Worldは、下記Gitから取得できる。この中の`appengine-java11/springboot-helloworld`が、ほぼほぼ完成形となるので、手っ取り早くスケルトンを確認したい方は、こちらを参照していただきたい。

```shell
git clone https://github.com/GoogleCloudPlatform/java-docs-samples
```
## GAE/J Java11 と Spring Bootの体裁を整える
まずは、Mavenのコンパイルが通る形にファイル構成の体裁を整えよう
### pom.xml 修正
とりあえず、Mavenの依存関係やbuild用のpluginを GAE/JとSpring Bootに対応したものに書き換えてしまおう。
もちろん、本来は自分のアプリケーションに必要な依存関係を追加する必要があるので、適時追加していただきたい。

```xml:pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project>
    <modelVersion>4.0.0</modelVersion>
    <version>2.0</version>
    <groupId>com.google.appengine.demos</groupId>
    <artifactId>appengine-try-java</artifactId>

    <properties>
        <maven.compiler.target>11</maven.compiler.target>
        <maven.compiler.source>11</maven.compiler.source>
    </properties>
    
    <dependencyManagement>
        <dependencies>
            <dependency>
                <!-- Import dependency management from Spring Boot -->
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>2.4.0</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>

            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>Hoxton.SR9</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <exclusions>
            <!-- Exclude the Tomcat dependency -->
            <exclusion>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-tomcat</artifactId>
            </exclusion>
            </exclusions>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jetty</artifactId>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <version>2.4.0</version>
                <executions>
                    <execution>
                        <goals>
                            <goal>repackage</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>

            <plugin>
                <groupId>com.google.cloud.tools</groupId>
                <artifactId>appengine-maven-plugin</artifactId>
                <version>2.4.0</version>
                <configuration>
                    <skip>true</skip>
                    <version>2.0.0</version>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>
```

`<plugin>com.google.cloud.tools</plugin>`の中の`<version>2.0.0</version>`の部分だが、これはGAEにデプロイされるバージョンを表す。このバージョンをJava8版と違う値にしておけば、旧バージョンのGAEに残ったままの状態となり、いざというときに切り戻しができるようになる。
ここで、とりあえずmvn compileしておけば、依存するJarファイルがダウンロードできるはずだ。

```shell
mvn compile
```
### app.yaml 作成
GAE Java11では、appengine-web.xml は廃止になり、設定はyamlファイルに一本化されたので、app.yamlファイルを作成する。
ファイルの保存場所は以下の通り。
`src/main/appengine/app.yaml`
必要最小限のapp.yamlファイルの記述は下記の通り。とりあえず、Runtimeが java11であることが書かれていれば後はデフォルト値で稼働する。

```yaml:app.yaml
runtime: java11
```
app.yamlの全設定は [app.yaml 構成ファイル] (https://cloud.google.com/appengine/docs/standard/java11/config/appref)を参照。

### エンドポイント作成
最後にSpring Bootのエンドポイントとなるクラスを作成する。
DemoServlet.javaと同じパッケージに新しいクラスDemoAppを作成しよう。

```java:DemoApp.java
package myapp;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@SpringBootApplication
@RestController
public class DemoApp {

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

}
```
### スケルトン完成
とりあえず中身は全くないが、これで体裁は整った。
DemoServlet.javaとWEB-INF以下のファイルは不要なので消してしまおう。
結果的に下記のようなフォルダ構成になるはずだ。
なお、webapp以下のファイルはMavenでビルドすれば、Spring Bootでも静的ファイルとして扱われるのでそのまま放置しておいてよい。

```
|--pom.xml
|--src
|  |--main
|  |  |--appengine
|  |  |  |--app.yaml
|  |  |--java
|  |  |  |--myapp
|  |  |  |  |--DemoApp.java
|  |  |--webapp
|  |  |  |--index.html
```
これでSpring BootのアプリケーションとしてMavenのコンパイルも通るはずだ。

```shell
mvn install
```

## Servlet移行
ここからが本番。いよいよ、サーブレットをSpring Boot対応のクラスに書き換えよう。

### DemoServlet 書き換え
DemoAppクラスに下記のようなメソッドを書き加えれば、*/demo*というGETリクエストに対して、`{"name":"World"}`というJSON文字列を返すエンドポイントを作成することができる。
一目瞭然だが、＠GetMapping アノテーションがメソッドがGetリクエストのエンドポイントであることを表しており、引数の**"/demo"**の部分がURLのマッピングである。
これで完成と言えば完成だ。

```java:DemoApp.java(一部)
  @GetMapping("/demo")
  public String hello() {
    return "{ \"name\": \"World\" }";
  }
```
ただし、もっとSpring Bootっぽくするならば、戻り値にBeanにすべきである。Beanを戻せば、フレームワークで自動的にJSON文字列のレスポンスを生成してくれる。
これらを踏まえた最終的なDemoApp.javaの全ソースは下記のようになる。


```java:DemoApp.java
package myapp;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@SpringBootApplication
@RestController
public class DemoApp {

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

  @GetMapping("/demo")
  public Hello hello() {
    Hello hello = new Hello();
    hello.name = "World";
    return hello;
  }

  public class Hello {
    public String name;
  }
}
```

### テストサーバ起動
テストを行うためには、下記のコマンドを実行すれば良い。
ブラウザで http://localhost:8080 を開けば、**Hello, World**の文字が表示されるはずである。

```shell
mvn spring-boot:run
```
### デプロイ
最後にデプロイを行おう。この部分はJava8版と変わりない。
デプロイを行うGclooudのMavenのゴールもプラグインによって読み込まれているので、下記コマンドを実行すればGAEサーバーにデプロイされるはずだ。

```shell
mvn appengine:deploy
```


# 最後に
最後のMavenコマンドで、GAEにデプロイできたはずである。
最後により実践的な参考情報を列挙する。

### サーブレットマッピング
今回はサーブレットをSpring Boot形式に全面的に書き換えるたが、サーブレットクラスには手を加えず、Spring Bootから直接呼び出す方法もある。この方法は[Spring Boot Servletマッピング](https://qiita.com/blue_islands/items/75c54e747ed1bbbf71fa) などの記事を参考にしてほしい。
特にJava8の段階で、すでにWeb.xmlではなく＠WebServletアノテーションでマッピングをしているのであれば、エンドポイントのクラスに＠ServletComponentScanを付加するだけで移行は完了する。

### リクエストパラメータ/ボディ
今回のサンプルはリクエストパラメータやボディ情報の必要ないサーブレットだったが、実際には必ずパラメータなどの受け取りが必要になるはずだ。
Spring Boot (厳密にはSpring MVCの機能となるが)で、リクエストパラメータやボディを受け取る方法は、[Spring MVC コントローラの引数](https://qiita.com/MizoguchiKenji/items/2a041f3a3eb13274e55c) といった記事が参考になるだろう。