Spring Boot 1.5→2.1 に移行したときのお話

# はじめに

Spring Boot 1系が[2019年8月1日にEOLを迎える](https://spring.io/blog/2018/07/30/spring-boot-1-x-eol-aug-1st-2019)ということで、今更ながらSpring Bootのバージョンを2系に更新しました（`1.5.2`→`2.1.5`）。
そのときにやったことをメモしておきます。

## 主な構成

- Java 8
- Gradle 4.10.3
- Web、バッチ両方ありの巨大なマルチプロジェクト
    - Springアプリケーション5個
    - プレーンなJavaアプリケーション1個
    - ライブラリ系のモジュール6個
- MySQL
- Flyway
- JOOQ
- Thymeleaf

# やったこと

## 情報収集

↓の公式の移行ガイドに基本的な移行方法や注意点が書かれています。
[Spring Boot 2.0 Migration Guide](https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-2.0-Migration-Guide)

公式の移行ガイドを読みながら作業し、不明な点や躓いた点はググって解決していきました。
また、メジャーバージョンが変わる主要ライブラリについては、リリースノートを見て破壊的変更がないかチェックしました。

## 1. まずは`spring-boot-properties-migrator`をdependenciesに追加（※移行が終わったら外す）

Spring Boot 2で`application.properties`/`application.yml`で定義するプロパティのキーや設定内容が変わったものがあります。
`spring-boot-properties-migrator`をdependenciesに足しておくと、アプリケーション起動時に、設定内容が古いままになっているプロパティがある場合に警告ログを出してくれます。また、一時的に古いプロパティの書き方のままでも動作するようにしてくれます。

dependenciesに↓の行を追加します。（※移行が終わったら外します）

```build.gradle
runtime "org.springframework.boot:spring-boot-properties-migrator"
```

## 2. GradleのDependency Managementプラグインを追加

1系ではSpring Bootプラグインの中にDependency Managementプラグインが含まれていましたが、2系からはDependency Managementプラグインは別になったようです。
build.gradleにDependency Managementプラグインを追加します。

```diff
  apply plugin: 'org.springframework.boot'
+ apply plugin: 'io.spring.dependency-management' // <-- コレを追加
```

## 3. Spring Bootのバージョンを上げる

```diff
  etx {
-   springBootVersion = '1.5.2.RELEASE'
+   springBootVersion = '2.1.5.RELEASE'
  }
```

Sprin Bootのバージョンを上げると、いろいろのライブラリのバージョンも自動的に上がります。

今回の主な変更

- Spring (4→5)
- Thymeleaf (2→3)
- MySQLのJDBCドライバー (5.1→8.0)
- Flyway (4→5)
- JOOQ (3.9→3.11)

また、RDBのコネクションプールのライブラリが`Tomcat JDBC`から`HikariCP`に変わりました。

※ 各ライブラリの具体的なバージョン番号は[spring-boot-dependenciesのPOMファイル](https://github.com/spring-projects/spring-boot/blob/master/spring-boot-project/spring-boot-dependencies/pom.xml)に載っています

## 4. Spring Bootの設定

### 4.1. Gradleの設定

`bootRepackage`というタスクがなくなり、`bootJar`という名前のタスクになりました

```build.gradle
bootJar {
  mainClassName = 'foo.bar.App'
  launchScript()                // <-- bootRapackageで`executable = true`を設定していた場合、これがその代わりになる
}
```

### 4.2. プロパティをバインドするときのキー名を修正

`@ConfigurationProperties`や`@Value`でSpring BootのプロパティをJavaにバインドしている場合は、プロパティ名の書き方を"Canonical"なものに統一する必要があります。
詳しくは[Spring Boot 2.0 Canonical Properties](https://github.com/spring-projects/spring-boot/wiki/Canonical-properties)を参照してください。

基本的には全部小文字にし、「-」と「_」は削除すればOKでした

```diff
- @ConfigurationProperties(prefix = "foo-bar.apiKey")
+ @ConfigurationProperties(prefix = "foobar.apikey")
```

## 5. Spring Web MVC

### 5.1. WebMvcConfigurerAdapter→WebMvcConfigurer

`WebMvcConfigurerAdapter`クラスが廃止になり、`WebMvcConfigurer`インターフェースに変更されました

```diff

- public class WebMvcConfig extends WebMvcConfigurerAdapter {
+ public class WebMvcConfig implements WebMvcConfigurer {
```

### 5.2. Controllerで拡張子ありのURLの自動マッピングができなくなった

```java
@GetMapping("/users")
public List<Users> listUsers() {
```

のようなエンドポイントを用意し、これに対してフロントエンドから`GET /users.json`のようなパスでアクセスしていましたが、これができなくなりました。
普通に`GET /users`でアクセスすればOKなので、`.json`を全部消しました。

## 6. MySQL

`5.1`→`8.0`になります

### 6.1. MySQLのドライバーのクラス名を変更

`com.mysql.jdbc.Driver`となっている箇所を探して`com.mysql.cj.jdbc.Driver`に置換しました。

## 7. JOOQ

`3.9`→`3.11`になります。
JOOQのバージョンが新しくなったことに伴い、何箇所か修正しました。
[The jOOQ Release Note History](http://www.jooq.org/notes)の`Breaking changes`は一通り読んでおくといいと思います。

### 7.1. JOOQのGradleプラグインのバージョンを上げる


JOOQ`3.11.x`以降は、gradle-jooq-plugin`3.x`を使う必要があります

```diff
 plugins {
-  id 'nu.studer.jooq' version '2.0.11'
+  id 'nu.studer.jooq' version '3.0.3'
 }
```

### 7.2. JOOQのコード生成のGradle設定を修正

APIのパッケージ名が変わっていたので修正

```diff
  mainDb(sourceSets.main) {
    jdbc {
      ...
    }
    generator {
-     name = 'org.jooq.util.DefaultGenerator'
+     name = 'org.jooq.codegen.DefaultGenerator'
      ...
      database {
-       name = 'org.jooq.util.mysql.MySQLDatabase'
+       name = 'org.jooq.meta.mysql.MySQLDatabase'
        ...
```

### 7.3. JOOQの生成をし直す

コードを再生成します

### 7.4. Cursor#fetchOne()が非推奨になったので修正

```diff
 while (cursor.hasNext()) 
-  Record record = cursor.fetchOne();
+  Record record = cursor.fetchNext();
```

## 8. Flyway

※3系から5系に移行する場合は、一旦4系にする必要があるそうです
https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-2.0-Migration-Guide#flyway

### 8.1. プロパティ名変更

application.properties / application.ymlのプロパティ名が、`flyway.*`→`spring.flyway.*`になりました

```application.yml.diff
- flyway:
-   enabled: true
+ spring:
+   flyway:
+     enabled: true
```

### 8.2. マイグレーション実行履歴管理テーブルの名前を設定

Flyway 3系, 4系では`schema_version`という名前のテーブルでFlywayの実行履歴が管理されていましたが、5系では`flyway_schema_history`という名前に変わっています。
`schema_version`を引き続き使用するためには`application.properties` / `application.yml`の`spring.flyway.table`というプロパティを設定します。

```application.yml
spring:
  flyway:
    table: schema_version
```

また、GradleでFlywayプラグインを使用している場合はそちらにも設定しておきます

```build.gradle
flyway {
  ...
  table = 'schema_version'
}
```

## 9. Thymeleaf

### 9.1. 依存ライブラリの修正

```diff
- implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity4'
+ implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity5' // <--- Spring SecurityのDialectのバージョンアップ
```

### 9.2. テンプレートレイアウトの記述方法を修正

Thymeleaf3からレイアウトの記法が変わりました。
参考:

- https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-2.0-Migration-Guide#template-engines
- https://www.thymeleaf.org/doc/tutorials/3.0/usingthymeleaf.html#template-layout


#### 例

##### レイアウト

```layout/default.html.diff
  <!DOCTYPE html>
  <html lang="ja"
        xmlns:th="http://www.thymeleaf.org"
        xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
  <head>
    <meta charset="utf-8"/>
    <!-- 「{各ページのタイトル} | アプリ名」のようなtitleになる -->
-   <title layout:title-pattern="$CONTENT_TITLE | $DECORATOR_TITLE">アプリ名</title>
+   <title layout:title-pattern="$CONTENT_TITLE | $LAYOUT_TITLE">アプリ名</title>
    ...
  </head>
  <body>
    <!-- 共通のHTMLを埋め込む -->
-   <div layout:replace="common/header::partial"></div>
+   <div layout:replace="~{common/header::partial}"></div>

    <!-- 各ページのコンテンツをここに展開 -->
    <div layout:fragment="content"></div>
  </bod>
  </html>
```

##### 個別ページ

```sample/index.html.diff
  <!DOCTYPE html>
  <html
      xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
      xmlns:th="http://www.springframework.org/schema/mvc"
-     layout:decorator="layout/default">
+     layout:decorate="~{layout/default}">
  <head>
    <title>各ページのタイトル</title>
  </head>
  <body>
    <!-- 各ページのコンテンツ -->
    <div layout:fragment="content">
      Hello World
    </div>
  </body>
  </html>
```

## 10. コネクションプール

Tomcat JDBCからHikariCPに変更なったので、設定内容を修正します。

プロパティ名は
`spring.datasource.tomcat.*` → `spring.datasource.hikari.*`
になります。

HikariCPで設定可能な項目は[com.zaxxer.hikari.HikariConfig](https://github.com/openbouquet/HikariCP/blob/master/src/main/java/com/zaxxer/hikari/HikariConfig.java)に定義されています。


## トラブルシューティング

### Jar生成時にフロントエンドのビルド内容が含まれなくなってしまった

もともとGradleでフロントエンドのビルドを行うタスクを定義し、jar作成時に実行されるように設定していました。

```build.gradle
jar.dependsOn compileFrontend
```

が、Spring Boot 2系からはbootJarタスクでjarを生成するようになったので↓のように変更しました

```build.gradle
bootJar.dependsOn compileFrontend
```

### DBUnitがコケる（MySQL）

```
org.dbunit.database.AmbiguousTableNameException: ACCOUNTS
```

複数のデータベース間で同じ名前のテーブルが存在するときにエラーが発生するようです。
JDBCのURLに`nullCatalogMeansCurrent=true`というクエリパラメータを追加したら直りました。
（MySQLのJDBCドライバーのデフォルトの挙動が変わったようです）

### JOOQで`TINYINT(1)`のフィールドに対してSELECTしたときにコケる

```
java.lang.ClassCastException: java.lang.Boolean cannot be cast to java.lang.Byte
```

JOOQを利用するときの方針として`BIT(1)`をBoolean、`TINYINT(1)`をByteにマッピングしていました。
が、Spring Boot 2にアップデート後、`TINYINT(1)`がBooleanにマッピングされるようになってしまいClassCastExceptionが発生していました。

JDBCのURLに`tinyInt1isBit=false`というクエリパラメータを追加したら直りました。
MySQLのJDBCドライバーは内部的に`TINYINT(1)`を`BIT(1)`として扱うらしく、Booleanになってしまったようです。

### WEBのドメイン名に「_」が入っている場合に、IllegalArgumentException

```
java.lang.IllegalArgumentException: The character [_] is never valid in a domain name.
```
Tomcatのバージョンが上がり、`_`を含むドメイン名を受け付けなくなったようです。
（ドメイン名に`_`が含まれるのはRFCの仕様としておかしいらしい。）
→サブドメインに`_`が入っていたので、サブドメイン名を変更して対応しました。

参考: https://stackoverflow.com/questions/53504857/the-character-is-never-valid-in-a-domain-name