Spring Boot 1.5系から2.1系へのマイグレーションガイド

# はじめに
![図1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/182576/c5c14bc4-81b9-10fd-d9ef-24f8ff12b782.png)

あるWebサービスで利用していたSpring Bootを、サービスリリース時の1.5.4.RELEASEから（2019年5月時点の最新版である）2.1.4.RELEASEまでバージョンアップした際の知見集です。
バージョンアップ対応は昨年の5月頃に完了し、完了時点での知見を社内向けのesaにまとめていたのですが、こちらはそれをベースに公開用に編集したものになります。

（グラフは某社の決算発表をイメージして、バージョンアップのインパクトを視覚的に伝えるために作成したネタです）

弊プロダクトにおいては、Lombokで横着していた箇所の宣言順による問題と、Mockitoの挙動変更に伴うテストの修正が大きめの印象でした。
メトリクスの収集にPrometheusを使用していたのですが、Spring Boot 2系から標準になったMicrometerへの切り替えは挙動を理解しながら修正していくコストが少々大きかったですが、慣れると非常に便利で分かりやすいと感じました。
また、一部のサブシステムにおいてapplication.ymlを思いっきりキャメルケースで書いていたため、ケバブケースへの書き換えに伴って`@ConfigurationProperties` などを利用して直接YAMLのキーを指定して値を読み出している箇所の修正も必要でした。

# 実装周り
## Lombokアノテーションの仕様変更
Lombok v1.16.22の[CHANGELOG](https://projectlombok.org/changelog)によると、

```
FEATURE: Private no-args constructor for @Data and @Value to enable deserialization frameworks (like Jackson) to operate out-of-the-box. 
Use lombok.noArgsConstructor.extraPrivate = false to disable this behavior.
```

簡単に言うと「`@Data`や`@Value`アノテーションを付与すると引数なしの（デフォルト）コンストラクタがprivateで宣言されるのがデフォルトの挙動になったよ」とのこと。

「lombok.propertiesに`lombok.noArgsConstructor.extraPrivate = false`って書くとこの挙動が外れるよ」ともあるが、`@Data`より**先に**`@NoArgsConstructor`を宣言することで先にpublicなデフォルトコンストラクタが作成されるので、影響範囲が小さかったり、余計なプロパティファイルを書きたくない場合は（今のところは）アノテーションの順番を変えるという解決策をとることもできます（[出典](https://github.com/rzwitserloot/lombok/issues/1708#issuecomment-394184052)）。

ただ、この変更の目的（Jacksonがデシリアライズするときにデフォルトコンストラクタが必要だから、という趣旨）と一致するのであれば、明示的にデフォルトコンストラクタを作らなくてもよくなったということですので、用途や現状のシステムにおけるユースケースによって判断する必要がありそうです。

## `Netty4ClientHttpRequestFactory` is Deprecated
SpringFramework 5系から廃止されたため、Apache HttpClientを利用することにしました（[参考](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/http/client/Netty4ClientHttpRequestFactory.html)）。
他にもOkHttp3なども利用できるようです。

## メトリクスがMicrometer経由になった
弊プロダクトでは`io.prometheus`の`simpleclient-spring-boot`を使用してPrometheusフォーマットによるメトリクスの出力を行っていたのですが、これをSpring Boot 2系の標準メトリクスライブラリであるMicrometerに切り替える必要がありました。
ちょっとサンプルは書きづらいのですが、基本的にはMicrometerのセットアップ（どのメトリクス製品を使うか）と、GaugeやCounterなどを読み替えて適宜用途に合うように修正していきます。

```java
// io.prometheus でのGaugeの作り方の例
Gauge gauge = Gauge.build()	
        .name("health_dsl_context")	
        .help("Health dsl context")	
        .register();

// io.micrometer でのGaugeの作り方の例
@Autowired /* 何らかの方法でBeanをInjectしてください */
private PrometheusMeterRegistry prometheusMeterRegistry;

AtomicDouble newGauge = prometheusMeterRegistry.gauge("health_dsl_context", new AtomicDouble(0.0));
```

## `@ConfigurationProperties` のプレフィックス指定は小文字のケバブケースで書かないと例外を吐くよ
`application.yml` に記述したプロパティを `@ConfigurationProperties` によって読み出すことがあるかと思いますが、Spring Boot 2.0以降から参照する時にキャメルケース以外のケースで書くと `InvalidConfigurationPropertyNameException` を吐くようになりました。

[リファレンス](https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#boot-features-external-config-relaxed-binding) によると、

```
The prefix value for the annotation must be in kebab case (lowercase and separated by -, such as acme.my-project.person).
```

とのことで、YAMLファイルのほうは（上述の通り）キャメルケースなどで書いても問題ないのですが、コード側からの読み出し時に上記のような制約をかけられている以上、ケースを混ぜ書きしていると設定値の可読性が下がりそうなので、小文字ケバブケースでの記述に揃えることにしました。

## `allow-bean-definition-overriding` は明示的に指定する
標準ではBeanのオーバーライドが無効になりました。
利用している場合は `application.yml` に `spring.main.allow-bean-definition-overriding=true` 指定を追加します。
（テストで使っているだけ、といった場合は環境プロファイルなどで切り替えるとよいかもしれません）

## MySQLのConnector/JとjOOQのパッケージクラス変更
Connector/Jのバージョンアップに伴い、`com.mysql.jdbc.Driver` -> `com.mysql.cj.jdbc.Driver` に変更します（パッケージだけではなくドライバ指定もすべて）。

また、jOOQのコードジェネレータ設定は以下のように変更します。

```
org.jooq.util.JavaGenerator -> org.jooq.codegen.JavaGenerator
org.jooq.util.mysql.MySQLDatabase -> org.jooq.meta.mysql.MySQLDatabase
```

## Jacksonでシリアライズするときのタイムスタンプフォーマット指定
`application.yml` で  `spring.jackson.datetime="yyyy-MM-dd'T'HH:mm:ss.SSSX"` を指定しました。
これを書かないと、タイムゾーン識別子が `+0000` のような形式になります。

## SpringBootApplicationBuilderの起動モード
リアクティブモードが増えたことでEnumによる指定に切り替わったため `.web(WebApplicationType.(SERVLET|NONE|REACTIVE))` といった形で書き直します。

```java
public static void main(String... args) {
	new SpringApplicationBuilder()
		.web(WebApplicationType.NONE)
		.sources(ExampleConfiguration.class)
		.main(ExampleApp.class)
		.build()
		.run(args);
}
```

## RequestInterceptorでHandlerMethodを受け取る場合
ちゃんとinstanceofで型チェックを入れないと死にます（むしろ、今までよく動いてたな…）。

```java
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
    throws Exception {

  if (handler instanceof  HandlerMethod) {
    /* handlerを扱う処理 */
  }

}
```

## `Content-type: multipart/form-data;boundary=` で完全一致のテストケースを書かない
小ネタですが、`boudary=`の前に`encoding=UTF-8;`が入るようになったため、完全一致を成立条件にしていたテストが落ちました。
テストケースの設定も悩みどころではありますが、containsStringで比較する等、ある程度柔軟に受け入れるようなケースを書いてあげるほうがよいでしょう。

## `@RestController` を付与したコントローラーで返り値をString型にしているときのContent-typeが `text/plain` になりました
以前は（おそらく明示的な指定がない限り）`application/json` で返っていたため、この修正によってクライアントがレスポンスをパースできなくなるといった問題が発生しました。
文字列だけのJSONも構造としては[許容される](https://stackoverflow.com/questions/7487869/is-this-simple-string-considered-valid-json)ので今までの挙動でも問題はなかったと思いますが、思わぬハマりポイントでした。

回避策としては、`Content-type: application/json` で返すことを正とすると、JacksonのTextNodeとして返してあげることで解決できます。
（[参考にしたStackOverflowの回答](https://stackoverflow.com/questions/38083872/how-to-make-restcontroller-handle-json-strings-properly)）

```java
@GetMapping("/v1/test")
public TextNode returnStringOnlyJson() {
    return new TextNode("responseText");
}
```

## `RestTemplateBuilder`のタイムアウト指定は`Duration`による指定になりました
今まではミリ秒をlong型で指定する形だったので、下記のように書き直しました。

```java
private RestTemplateBuilder restTemplateBuilder(int connectTimeout, int readTimeout) {
	return new RestTemplateBuilder()
			.setConnectTimeout(Duration.ofMillis(connectTimeout))
			.setReadTimeout(Duration.ofMillis(readTimeout));
}
```

# テスト周り
## Mockitoの`anyObject()`や`anyListOf()`はDeprecatedになりました。
`any()` や `anyList()` 、`any(HogeHoge.class)` を使いましょう。

## Mockitoの`anyInteger()` などはそのまま使うとnull非許容になりました。
`nullable(HogeHoge.class)` を使いましょう。