sbtのGAEプロジェクトをMavenに移行する

# 経緯

GAEのスタンドアロンなSDKが非推奨になり、Google Cloud SDKへの移行が必須になりました。


> 2019 年 7 月 30 日以降、スタンドアロンの App Engine SDK は非推奨になりました。2020 年 8 月 30 日以降は、ダウンロードできなくなります。

参考: https://cloud.google.com/appengine/docs/standard/java/sdk-gcloud-migration?hl=ja

sbtで使っていたGAE用のプラグイン[sbt-appengine](https://github.com/sbt/sbt-appengine)がスタンドアロンSDKに依存しているのでGoogleさんが標準で用意しているMavenに移行してみた時の記録。


# XMLからYAMLへのファイル形式の移行
Google Cloud SDKでは`queue.xml`などの設定系ファイルのXML形式が非サポートになり、`queue.yaml`などYAML形式に変換しないとダメな様です。

`gcloud`コマンドで変換ができるのでこれを使います。
https://cloud.google.com/appengine/docs/standard/java/sdk-gcloud-migration?hl=ja#migrating_xml_to_yaml_file_formats

```
gcloud beta app migrate-config queue-xml-to-yaml src/main/webapp/WEB-INF/queue.xml 
gcloud beta app migrate-config datastore-indexes-xml-to-yaml src/main/webapp/WEB-INF/datastore-indexes.xml 
gcloud beta app migrate-config cron-xml-to-yaml src/main/webapp/WEB-INF/cron.xml 
```
など。

テストで`queue.xml`を読んでる場合はここも`setQueueXmlPath`から`setQueueYamlPath`に変更する必要があります。

（1.9.80以降で追加されたメソッドの様です。参考: https://stackoverflow.com/questions/59406695/appengine-unit-test-non-default-queues-in-queue-yaml ）

```diff
-      new LocalTaskQueueTestConfig().setQueueXmlPath("src/main/webapp/WEB-INF/queue.xml")
+      new LocalTaskQueueTestConfig().setQueueYamlPath("src/main/webapp/WEB-INF/queue.yaml")
```

# build.sbtからpom.xmlへの移行
## pom.xmlのテンプレを生成する
Mavenのarchetypeが用意されているので[公式ドキュメント](https://cloud.google.com/appengine/docs/standard/java/using-maven?hl=ja#creating_a_new_app)に従ってMavenプロジェクトを生成します。

```sh

mvn archetype:generate -Dappengine-version=1.9.81 -Dfilter=com.google.appengine.archetypes:
```

すると出てくる `2: remote -> com.google.appengine.archetypes:appengine-skeleton-archetype (A skeleton application with Google App Engine)` を選びます。

※ドキュメント記載のappengine-versionが古かった(1.9.59)ので現時点での最新（1.9.81)を指定しました

`artifactId`で指定したサブディレクトリに`pom.xml`が生成されるのでプロジェクトのルート（`build.sbt`と同じところ）にコピーします。

## Scala Maven Pluginを追加
https://docs.scala-lang.org/tutorials/scala-with-maven.html を参考に、
`src/main/scala`以下の`*.scala`ファイルをコンパイルする様に`<plugins>`以下に追加します。

```xml
      <plugin>
        <groupId>net.alchim31.maven</groupId>
        <artifactId>scala-maven-plugin</artifactId>
        <version>4.4.0</version>
        <configuration>
          <recompileMode>incremental</recompileMode>
        </configuration>
        <executions>
          <execution>
            <goals>
              <goal>compile</goal>
              <goal>testCompile</goal>
            </goals>
          </execution>
        </executions>
      </plugin>
```

`<properties>`以下に使用するScalaのバージョンを指定できる様にしておいて、

```xml
    <scala.version>2.10.4</scala.version>
```

該当バージョンのscala-libraryを`<dependencies>`以下に追加します。


```xml
    <dependency>
      <groupId>org.scala-lang</groupId>
      <artifactId>scala-library</artifactId>
      <version>${scala.version}</version>
    </dependency>
```

## build.sbtからdependencyの移動
続けて`build.sbt`で設定していたライブラリ依存関係の定義を`pom.xml`に移していきます。

```sh
sbt makePom
```

すると`target`ディレクトリ以下に`pom.xml`に変換してくれるので、そこから必要なdependencyをコピペするのが楽です。


例えば

```scala
libraryDependencies += "org.json4s" %% "json4s-jackson" % "3.1.0"
```

はこんな感じ↓になります。

```xml
    <dependency>
        <groupId>org.json4s</groupId>
        <artifactId>json4s-jackson_2.10</artifactId>
        <version>3.1.0</version>
    </dependency>

```

## Twirlのコンパイルができる様にする
テンプレートエンジンとして[`Twirl`](https://github.com/playframework/twirl)を使っている場合は[Twirl Maven Plugin](https://github.com/JakeWharton/twirl-maven-plugin)を追加する必要があります。

`<plugins>`以下に↓を追加します。

```xml
      <plugin>
        <groupId>com.jakewharton.twirl</groupId>
        <artifactId>twirl-maven-plugin</artifactId>
        <version>1.2.0</version>
        <executions>
          <execution>
            <phase>generate-sources</phase>
            <goals>
              <goal>compile</goal>
            </goals>
          </execution>
        </executions>
      </plugin>
```

## 標準のコンパイラプラグインをスキップする

Scala Maven Pluginが`*.java`ファイルもコンパイルしてくれるので、標準のMaven Compiler Pluginの方は`<configuration><skipMain>true</skipMain></configuration>`を追加してコンパイルをスキップさせます。

```xml

      <plugin>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>3.6.1</version>
        <configuration>
          <skipMain>true</skipMain>
        </configuration>
      </plugin>
```

## Specsのテストが実行される様にする

※めっちゃ古いプロジェクトなので未だにSpecs（Specs2ではなく）のテストが残っていました

若干ソースコードの修正も必要です。とは言ってもextendするのを`org.specs.Specification`から`org.specs.SpecificationWithJUnit`に変更するだけです。
（あと、`object`ではなく`class`にする必要があります）

参考: https://code.google.com/archive/p/specs/wikis/RunningSpecs.wiki#Run_your_specifications_with_JUnit4_and_Maven


```diff

-import org.specs.Specification
+import org.specs.SpecificationWithJUnit

-object ExampleSpec extends Specification {
+class ExampleSpec extends SpecificationWithJUnit {
```

Surefire Pluginの`<configuration>`を↓の様に足して`Specs`のテストが実行される様にします。

```xml
      <plugin>
        <artifactId>maven-surefire-plugin</artifactId>
        <version>2.20</version>
        <configuration>
          <useSystemClassLoader>false</useSystemClassLoader>
          <argLine>-Xmx512m</argLine>
          <includes>
            <include>**/*Spec.java</include>
          </includes>
        </configuration>
      </plugin>
```


# MavenでGAE開発サーバーを起動する

```sh
sbt appengineDevServer
```

に該当するのはMavenでは

```sh
mvn appengine:run
```

になります。

また、

```sh
sbt console
```

は

```sh
mvn scala:console
```

になります。

# 応用編
## hot reloadingに対応する
sbt-appengineの場合は

```sh
sbt ~appengineDevServer
```

でソースコードの変更を検出して自動でGAEの開発サーバーを再起動してくれましたがMavenのPluginではやってくれません。
https://github.com/fizzed/maven-plugins に含まれるWatcher Pluginを使うとソースコードの変更に応じてMavenの任意のゴールを実行してくれるのでこれを使います。

変更を監視したいディレクトリを`<watches>`以下で設定し、実行したいゴールを`<goals>`以下で指定します。

ゴールは複数書けるので、Twirlで`*.scala`を生成してからコンパイル、は↓みたいに設定できます。


```xml
      <plugin>
          <groupId>com.fizzed</groupId>
          <artifactId>fizzed-watcher-maven-plugin</artifactId>
          <version>1.0.6</version>
          <configuration>
              <touchFile>${project.build.directory}/${project.build.finalName}/WEB-INF/appengine-web.xml</touchFile>
              <watches>
                  <watch>
                      <!-- *.java混在してない場合はここ不要 -->
                      <directory>${project.basedir}/src/main/java</directory>
                  </watch>
                  <watch>
                      <directory>${project.basedir}/src/main/scala</directory>
                  </watch>
                  <watch>
                      <directory>${project.basedir}/src/main/twirl</directory>
                  </watch>
              </watches>
              <goals>
                  <goal>twirl:compile</goal>
                  <goal>scala:compile</goal>
              </goals>
          </configuration>
      </plugin>
```

また、GAEの開発サーバーは、`appengine-web.xml`の変更を検出して再起動してくれるので`<touchFile>`で`appengine-web.xml`のタイムスタンプを書き換える様にします。


参考: https://cloud.google.com/appengine/docs/standard/java/maven-reference?hl=ja#appenginerun
> サーバーの稼働中は、appengine-web.xml が変更されていないかどうか継続的に確認します。変更されている場合、サーバーがアプリケーションを再度読み込みます


以上の設定をして、別プロセスで

```sh
mvn fizzed-watcher:run
```

を起動しておくとhot reloadingぽいことが実現できます。

## Dockerを使用する場合

例:

```sh
docker run --rm -ti \
  -v $HOME/.m2:/root/.m2 -v $HOME/.sbt:/root/.sbt \
  -v `pwd`:/app -p 8080:8080 $YOUR_IMAGE \
  mvn package appengine:run -DcloudSdkHome=/root/google-cloud-sdk
```

### Mavenレポジトリのキャッシュ
`-v $HOME/.m2:/root/.m2`
dependencyのjarたちを落としてくるのが非常に遅いので、ホストのMavenレポジトリキャッシュのディレクトリをコンテナにマウントするとビルド時間を節約できます。


### `~/.sbt`のキャッシュ
`-v $HOME/.sbt:/root/.sbt`
sbtは使っていませんが、Scala Maven Pluginで使われている https://github.com/sbt/zinc が`~/.sbt/`以下のキャッシュを使う様なのでここもマウントしています。


### インストール済みCloud SDKを使う
`-DcloudSdkHome=/root/google-cloud-sdk`
何も指定しないとGoogle Cloud SDKが既にインストール済みでもPluginが別途ダウンロードしてしまいます。

Dockerのイメージに入れておけば、`cloudSdkHome`でインストール済みのCloud SDKのパスを指定してやるとそっちを使ってくれるのでだいぶ時間を短縮できます。

参考: https://github.com/GoogleCloudPlatform/app-maven-plugin/blob/master/USER_GUIDE.md#cloud-sdk-configuration

これはDockerに限った話では無いので、`-DcloudSdkHome=$(dirname $(dirname $(which gcloud)))`とかしておくとローカル環境でもインストール済みCloud SDKを使ってくれて便利です。