Spring Bootを1.5から2へマイグレーションするステップとポイント

Spring Bootを1.5から2へマイグレーションするステップとポイント
目次

Spring Bootの2がリリースされたので、Spring Boot 2.0 Migration Guide を参考に既存のSpring Boot 1.5のプロジェクトをマイグレーションしてみた。行なったときの段取りとポイントを簡単にまとめました。

spring-boot-starter-web、spring-boot-starter-data-jpa、spring-boot-starter-actuator、spring-boot-starter-thymeleafを主に使っている。結論だけ先に言うと、spring-boot-starter-actuatorのマイグレーションがめんどくさかったです。

springboot

モチベーション

これからのJava時代に備えて

Spring Bootの 1.5.9 を使っていたのだけど、2018/05時点において、公式からは Spring Boot 1.5のJava9サポート予定はない ことが公表されている。

以前、JavaプロジェクトをModule System(Java9のProject Jigsaw)にマイグレーションするステップ を書いた時には Spring Bootの1.5がJava9のmodule pathでのクラスロードに対応しておらず(複数ライブラリ間でのパッケージ重複問題)、完全移行を断念した経緯があった。

その後、Spring Bootの2.0が2018/03にローンチされた後、Java9上で動作することを一応確認しておいたので、 Javaの進化に追従していきたいプロダクトは、Spring Boot2にマイグレーションする必要がある し、 Spring Boot使いは 既にJavaのマイグレーション準備期間に突入した のだ。

2019年1月までにSpring Boot2への移行を

「いつまでに何をしないといけないか?」、つまり、スケジュール感の算段を立てる必要がありそうだ。

まずは、 Oracleの公式 でJavaのロードマップを確認しよう。

Javaに関して抑えておきたいのは2点

  • Java11は 2018/09 から利用可能
  • Java8は 2019/01 にサポートが切れる

次に Spring Bootのロードマップ も確認しておく。

springboot-milestone

Spring Bootの場合には

  • 2.0.2 のリリースは2018/05予定
  • 2.1.0.RC1 のリリースが2018/09予定
  • 2.1.0.RC2 以降は未定

がポイントだ。

結論として、

  • 2018/09までに sprignboot 2.0.x にマイグレーションする
  • 2019/01までに Java 11 にマイグレーションする

という段取りで進めていくとスムーズだと考えている。

マイグレーションに必要な事前準備

いきなりマイグレーション作業をすると破綻するので、事前準備が必要だ。

具体的に言うと テストコードメトリック取得の仕組み の2つが必要になる。

テストコードは最低でも以下の3種類を準備する。CI基盤と組み合わせるなどして簡単に実行できる工夫をしておくのが好ましい。

  • 単体テスト
  • 結合テスト
  • 負荷テスト

メトリック取得の仕組みはなんでも良いが、私は Datadog を使っている。

プロジェクトによっては、結合テストと負荷テストまで手が届いていないかもしれないが、今回のようなマイグレーションには欠かせないので作っておこう。

外側から見たシステムの振る舞いやパフォーマンスに影響がないかを確認する必要があるからだ。

なお、負荷テストではbefore/afterの比較ができないと意味がないので、既存のシステムでさばけるパフォーマンスは一度計測しておく。

とりあえず私の場合は既にメンテンナスされている資産があるので、それを使うことにする。

いざマイグレーション!!

環境情報

  • Java
    • 1.8.0_152
  • Gradle
    • 4.4
  • マイグレーション対象のSpring Bootモジュール
    • spring-boot-starter-web
    • spring-boot-starter-data-jpa
    • spring-boot-starter-actuator
    • spring-boot-configuration-processor
    • spring-boot-starter-thymeleaf
    • spring-boot-gradle-plugin(ビルド用)

build.gradleの変更

  • Spring Bootをバージョンアップ

記事を書いている時点での最新 2.0.1.RELEASE に変更する。

  • bootRepackage タスクを削除

Spring Boot Gradle PluginbootRepackage タスクが廃止になったため削除した。 public static void main(String[] args) を探してよしなにやってくれるようなので、シンプルな構成のアプリケーションであれば、そもそも mainClassName の記述は必須ではない。

1// 削除
2//bootRepackage {
3//    mainClass = 'com.soudegesu.demo.app.Application'
4//    executable = true
5//}

application.yaml の修正

application.yaml を修正する。設定項目は使っているモジュールに依存するので、詳細は触れないが、 私の場合には主に springboot-actuator の設定変更が発生した。 yaml構造の変更actuator endpointの公開設定メトリック取得方法指定 といったところ。

 1management:
 2  endpoints:
 3    web:
 4      exposure:
 5        include: info,health,metrics,httptrace,threaddump,heapdump
 6  metrics:
 7    export:
 8      datadog:
 9        api-key: (APIのキー)
10        step: 30s
11  security:
12    enabled: false
13  server:
14    port: ポート番号

コンパイルエラーやwarningを解決していく

build.gradle を編集して依存関係を更新すると、コンパイルエラーやwarningが出てくるので、それを適宜直していく。

パッケージやクラスに変更があり、それに合わせてIFも修正する必要があった。 ざっくり以下に置き換えている。

変更前変更後
org.springframework.boot.autoconfigure.web.ErrorAttributesorg.springframework.boot.web.servlet.error.ErrorAttributes
org.springframework.web.context.request.RequestAttributesorg.springframework.web.context.request.WebRequest
org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapterorg.springframework.web.servlet.config.annotation.WebMvcConfigurer

少し面倒だったのは spring-data-jpaCrudRepository から findOne が削除されたため、 implementしているクラス側で findByXX を自前定義してあげた。

コンパイルエラーが治ったら、単体テストを実行し、クラスレベルのデグレードが起きないことを確認した。

実行時エラーを解決する

次に、マイグレーション済みのSpring Bootアプリケーションを起動できるようになったら、ローカルで結合テストを流して、振る舞いに変化がないかをチェックする。

エラーがやはり出た。

DBへのINSERTが伴うリクエストの処理にて、 spring-data-jpa がエラーを吐き出している。

1org.springframework.dao.InvalidDataAccessResourceUsageException: error performing isolated work; SQL [n/a]; nested exception is org.hibernate.exception.SQLGrammarException: error performing isolated work
2(中略)
3Caused by: java.sql.SQLException: Table '(Schema名).hibernate_sequence' doesn't exist
4
5  Query is: select next_val as id_val from hibernate_sequence for update

DB(MySQL)へのINSERTで1箇所、AUTO INCREMENTしているところがあって、 Entityでフィールドに @GeneratedValue(strategy= GenerationType.AUTO) アノテーションを付与しているのだが、 hibernate_sequence を使ったID生成を試みてしまっているようだ。

確認してみたところ、デフォルトの挙動が変わっているようだ。

Id generator

The spring.jpa.hibernate.use-new-id-generator-mappings property is now true by default to align with the default behaviour of Hibernate. If you need to temporarily restore this now deprecated behaviour, set the property to false.

そのため、 spring.jpa.hibernate.use-new-id-generator-mappings: falseapplication.yaml に追加してあげる。

メトリックの取得設定を変える(springboot-actuator)

正直これが一番めんどくさかった。

springboot-actuator を使用しているのだが、 バージョンアップに伴い大きく以下の変更が入っている。

  • メトリック毎にエンドポイントが分割された
    • 一発で取得できなくなった
  • 取得できるメトリック名に後方互換がなくなった
    • 諸事情で後方互換性を持たせたい時には自前実装が必要
  • メトリック拡張のロジックに修正が必要になった

springboot-actuator のメトリックをシステム監視に利用しているプロダクトは辛い。


もう少し具体的に説明しておく。

以前は一発で取れた

以前はactuator endpointに対してリクエストすると、以下のようにメトリックが一発で取れた。 拡張メトリックもレスポンスのjsonにプロパティが追加される形で拡張がなされていた。

 1{
 2    "mem": 485331,
 3    "mem.free": 253058,
 4    "processors": 4,
 5    "instance.uptime": 312097,
 6    "uptime": 338799,
 7    "systemload.average": 3.69384765625,
 8    "heap.committed": 419840,
 9    "heap.init": 131072,
10    "heap.used": 166781,
11    "heap": 1864192,
12    "nonheap.committed": 66648,
13    "nonheap.init": 2496,
14    "nonheap.used": 65492,
15    "nonheap": 0,
16    "threads.peak": 177,
17    "threads.daemon": 155,
18    "threads.totalStarted": 243,
19    "threads": 158,
20    "classes": 9613,
21    "classes.loaded": 9613,
22    "classes.unloaded": 0,
23    "gc.ps_scavenge.count": 14,
24    "gc.ps_scavenge.time": 373,
25    "gc.ps_marksweep.count": 2,
26    "gc.ps_marksweep.time": 388,
27    〜(中略)〜
28    "httpsessions.max": -1,
29    "httpsessions.active": 0,
30    "datasource.primary.active": 0,
31    "datasource.primary.usage": 0
32}

これからはメトリック毎に取得する

/actuator/metrics を参考に取得可能なメトリックを確認して (この時点でメトリック名に互換性がないことがわかる)

 1{
 2    "names": [
 3        "http.server.requests",
 4        "jvm.buffer.memory.used",
 5        "jvm.memory.used",
 6        "jvm.gc.memory.allocated",
 7        "jvm.memory.committed",
 8        "jdbc.connections.min",
 9        "tomcat.sessions.created",
10        "tomcat.sessions.expired",
11        "hikaricp.connections.usage",
12        "tomcat.global.request.max",
13        (中略)
14        "tomcat.threads.busy",
15        "tomcat.global.request",
16        "hikaricp.connections.creation",
17        "jvm.gc.memory.promoted",
18        "tomcat.sessions.rejected",
19        "tomcat.sessions.alive.max"
20    ]
21}

/actuator/metrics/(メトリック名) でリクエストをしてあげなければならない。

例えば以下のようになる。

パス: /actuator/metrics/tomcat.sessions.created

 1{
 2    "name": "tomcat.sessions.created",
 3    "measurements": [
 4        {
 5            "statistic": "COUNT",
 6            "value": 0
 7        }
 8    ],
 9    "availableTags": []
10}

結構変わってしまったではないか。。

micrometer-registry-datadog を入れる

たまたま Datadog を導入していたため、 簡易な解決策として、micrometer-registry-datadog にメトリックを打ち上げてもらうことにした。

  • build.gradle に依存モジュールを追加
1compile group: 'io.micrometer', name: 'micrometer-registry-datadog', version: '1.0.3'

まさかのEC2(AmazonLinux)デプロイで落とし穴

ローカルマシン(Mac)で起動できたため、大方いけると考えていたが、EC2に jar をデプロイする時に落とし穴に遭遇した。

service コマンド起動時に

1invalid file (bad magic number): Exec format error

のようなエラーメッセージが出て起動できなくなってしまったのだ。

しかし、 java -jar コマンドでは起動できる。

これには bootJar タスク実行時に起動スクリプトを含めることで対応した。

1bootJar {
2    launchScript()
3}

とどめの負荷テスト

最後に環境にデプロイして、負荷試験を行う。 プロダクトによって指標は異なると思うので、私は以下の2種類だけ実施した。(人によって言い方が変わるので注意)

  • ストレステスト
    • サービス需要予測とそれ以上の瞬間最大風速を計測
    • できれば3点計測する
  • ロングランテスト
    • 長期間実施してサーバリソースが枯渇しないか、周辺のコンポーネントに迷惑かけないか計測

これらをdatadog上で大きな変化がないことを確認して、終了。

まとめ

今回はSpring Boot 1.5のプロジェクトを 2.0にマイグレーションしてみました。

大きな流れをまとめると

  • テストコード準備する
  • マイグレ前のアプリケーションのパフォーマンスを計測しておく
  • ライブラリを差し替える
  • 単体テストを動かしながら、コンパイルエラーを取り除く
  • 結合テストを動かしながら、実行時エラーを取り除く
  • サーバにデプロイする
  • 負荷テストして、パフォーマンスが大きく変わっていないことを確認

負荷テスト含めて実施したため、全体としての所要時間はかかりました(1人でやって1.5weekくらい)が、コードのマイグレーション作業自体はそこまで時間がかかりませんでした。これは Spring Boot 2.0 Migration Guide の内容が以前よりも充実したことが寄与していると思う。

強いて言うと、 springboot-actuator のメトリック変更がコード以外の部分に波及したのは厄介でした。

2.x自体もリリースされて日が浅いので、事故っても損害が少ないプロダクトから適用していきたいですね。

そして、来るべきJava 11に早めに備えておきたい!

参考にさせていただいたサイト