雑に描いた餅

餅は餅屋、わたしは技術屋、と言いたかった。

Kotlin 1.2のアプリケーションを1.7までバージョンアップする

対象読者

  • 「このアプリケーション、Kotlinのバージョン古くなってきてるし、最新まで上げようぜ!」って仕事をやることになった、ちょっと前の私。
  • およびそのような人(いるのか)。

前提条件

  • 2017年頃ファーストリリース、現在も稼働中のバックエンドアプリケーション
    • ただしアクティブなメンテナとなるチームはおらず、今回たまたま私が改修するタイミングがあった
  • Java
    • 1.8系
  • Kotlin
    • 1.2.10
      • 2017年12月頃にリリースされた模様 github.com
  • フレームワーク
    • Spring Boot(1.5.8.RELEASE)*1
  • プロジェクト管理
  • その他
    • JUnit5
    • mockk
      • 1.7.15
    • mockito-kotlin
      • 1.6.0

結果

  • Kotlin
    • 1.2.10 -> 1.7.10
  • mockk
    • 1.7.15 -> 1.12.0
  • mockito-kotlin
    • 1.6.0 -> 2.2.0 *2
  • その他一部アプリケーションコードの修正(後述)

バージョンアップのモチベーション

Kotlin 1.2系が、最新のIntelliJ IDEAではサポートされていない為です。

執筆時点で最新バージョンの2022.2.1のIntelliJ IDEAでKotlin 1.2を利用しているアプリケーションを起動しようとしても、そもそも起動することができません。 (正確にはビルドすらできません。Kotlin: Language version 1.2 is no longer supported; please, use version 1.3 or greater.というメッセージの通り、最低でも1.3以上にする必要があります)

このことは開発生産性の観点で大きなマイナスなので、バージョンアップすることにしました。

方針

  • 時間をあまりかけたくないので、変更は必要最小限に
    • 極力バージョンアップ前に利用していたライブラリをそのまま利用する(ライブラリの追加、削除よりも、既存ライブラリの新しいバージョンを利用する)
    • 極力アプリケーションコードの修正を加えない

Kotlinのバージョンを上げる

最新のバージョンを確認する

  • GithubのReleasesページを確認 github.com
    • なおIntelliJ IDEAでは、より新しいバージョンのライブラリを利用できる場合、pom.xmlファイル上でガイドしてくれる機能がある

とりあえず上げてみる

  • アプリケーションコード上で修正が必要な箇所はビルド時点では特になし*3
  • Kotlinで書かれたコードをJava8上で動かす場合には、kotlin-stdlib-jre8 ではなく、kotlin-stdlib-jdk8を利用する必要がある点に注意が必要

単体テストを実行できるようにする

  • この時点では単体テストが実行できなくなっていた

    • mockkが依存するCoroutine周りのクラスパスが変わっていた
      • Kotlin 1.3以降でCoroutine周りの機能がデフォルトに変わったため
    • ということでmockkを最新までバージョンアップ github.com
  • 今度は一部のテストケースで「Caused by: java.lang.NoSuchFieldError: ASM_API」というエラーメッセージが出てテストに失敗するようになる

    • これはmockito-kotlinが依存しているbytebuddyのバージョンが古いことによって起こっているらしい
    • mockito-kotlinのバージョンも最新にすることで解決

アプリケーションが問題なく稼働することを確認する

  • JAX-RSのParamConverterを利用する処理の周りがうまく動かないことを確認
    • これはKotlinをバージョンアップしたことで、Null Safetyがより厳しく判定されるようになったため

例えば以下のようなParamConverterを実装したクラスがあるとします。(LocalDateTimeはJava標準のクラスとする)

class LocalDateConverter: ParamConverter<LocalDateTime> {
    override fun fromString(value: String): LocalDateTime {
        try {
            return LocalDateTime.parse(value, DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"))
        } catch (e: Exception) {
            throw IllegalRequestException("Invalid date string. $value")
        }
    }

    override fun toString(value: LocalDateTime) =
            value.format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"))
}

ParamConverterはJAX-RSにおいて、String型のメッセージパラメータと特定の型Tで相互に変換する責務を担います。

参考:https://spring.pleiades.io/specifications/platform/8/apidocs/javax/ws/rs/ext/paramconverter

上記クラスで実装している fromString 関数は、文字列から特定の型Tへの変換処理を担っています。

Kotlin1.2においては、この fromString 関数の引数としてnullが渡ってきても問題なく動いていました。しかし、Kotlin 1.7までバージョンアップされる過程で、Null Safetyが厳しく検査されるようになり、nullが渡ってきた場合に想定通りに動かなくなってしまったようです。

その為、ワークアラウンドとして以下のように、nullの値が渡ってきても問題ないようにアプリケーションコードを修正しました。

    override fun fromString(value: String?): LocalDateTime? {
        try {
            return value
                ?.takeIf { value.isNotEmpty() }
                ?.let { LocalDateTime.parse(value, DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'")) }
        } catch (e: Exception) {
            throw IllegalRequestException("Invalid date string. $value")
        }
    }

ということで

こまめなバージョンアップって大事ですね。

*1:こいつもついでにバージョンアップしたかったが今回は時間の都合で割愛

*2:mockito-kotlinは2系にバージョンアップされる過程でGroup IDが微妙に変わっているので注意が必要です。com.nhaarman -> com.nhaarman.mockitokotlin2

*3:Deprecatedな関数はいくつかあります。String.toUpperCase等。https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.text/to-upper-case.html