Java
Kotlin
spring-boot

Java から Server Side Kotlin + Spring-boot に移行して

More than 1 year has passed since last update.

Google が Android の公式言語として採用したことを受け、Kotlin コミューンが広がります。そんな中、Server Side Language として採用している Java から Server Side Kotlin へ移行しました。

Kotlin を採用するメリット・デメリット :baby_chick:

Kotlin に移行するに当たって受けられる恩恵を一部ピックアップします。良く語られる部分の言語仕様なので軽めの紹介です。

メリット

  • Null Safety :ok_woman:
  • val 宣言で Immutable :no_entry_sign:
  • When 式による列挙の網羅性担保 :muscle:
  • コード量の減少と優しいインタフェースで見通し UP :sparkles:

デメリット

  • 現段階で Server Side Kotlin のエコシステムが弱い

Kotlin では、Optional でない型の変数に null を代入することは許容されていません。null でないことがコンパイラで保証されているのでとても安全です。また、Optional である場合には値を取り出すときにチェックが必須となります。

fun main(vararg args: String) {
    val user: User? = null
    println(user?.fullName ?: "unknown")
}

data class User(val familyName: String, val firstName: String) {
    val fullName: String
        get() = "${this.familyName} ${this.firstName}"
}

Kotlin には Scala の match と似た when 式があります。Java で言うところの switch に該当しますが、違う点は値を返すことと列挙の網羅性が担保されることです。

fun method(license: License) {
    val service = license.createService()
}

enum class License {
    FREE,
    ENETERPRISE;

    fun createService(): Service {
        return when (this) {
            FREE -> FreeService()
            ENETERPRISE -> EnterpriseService()
        }
    }
}

interface Service { ... }
class FreeService: Service { ... }
class EnterpriseService: Service { ... }

when 式では this (License) が渡されています。License は FREE か ENTERPRISE であることは分かっているので、when 式で default (kotlin では else) の処理を書く必要がないのです。また、列挙のパターンに漏れがあるときにコンパイルエラーとなるので網羅性を担保できます。(※ 少し制約があります)

・・・

システムは長い時間をかけてスケーリングや引き継ぎが行われます。Kotlin はそれらを安全に遂行するために有効な言語仕様が搭載されています。メリットで上げた機能は Java よりも強力なサポートが得られるため、Kotlin を採用する着目すべき機能だと思います。

余談ですが、既存システムが Spring-boot + Mavan で構築されていたため、これをそのまま利用できることも学習コストが格段に下がるため移行の大きな要因となりました。

Server Side Kotlin + Spring-boot を採用して当たった問題 :feet:

メリットを連ねていても、実際の開発では躓いた点が幾つかありました。その一部を紹介したいと思います。

SpringApplication の static run メソッドを見つけてくれない

Java では以下のように記述する SpringApplication ですが、Spring Initializr で生成したまま利用すると動作しませんでした。

@EnableAutoConfiguration
@SpringBootApplication
public class MyApplication {
    public static void main(String[] args) {
        SpringApplication.run(MyApplication.class, args);
    }
}

Kotlin の static は Companion を介して実装するため(Scala も同様)、次のように記述します。また、Java からの呼び出しが可能になるように JvmStatic アノテーションを付与します。

@SpringBootApplication
@EnableAutoConfiguration
class MyApplication {
    companion object {
        @JvmStatic fun main(vararg args: String) {
            SpringApplication.run(MyApplication::class.java, *args)
        }
    }
}

Mockito の any や anyObject が例外を throw する

Mockito の any・anyObject は内部で null を返すように実装されています。これを Kotlin で利用しようとすると non-null の引数に渡されたときに IllegalStateException の例外が発生します。

解決策として、Kotlin の Generics を利用することで null チェックをくぐり抜けることが可能なようです。以下のような Helper クラスを利用するとよいです。

class MockitoHelper {
    companion object {
        fun <T> any(): T {
            return Mockito.any() ?: null as T
        }
    }

    fun <T> eq(value: T): T {
        return if (value != null) Mockito.eq(value)
               else null ?: null as T
    }
}

こちらの記事を参考にさせていただきました。ありがとうございます :bow:
*この解決方法は今後利用できなくなる可能性があることを考慮に入れてください

RequestBody アノテーションを付与した data class の Primary Constructor を見つけてくれない

普段通りの Java コードをそのまま Kotlin に落とし込んでも Json が DataClassResource にマッピングできず例外を throw しました。

data class DataClassResource(val key: String, val value: String)

@RestController
@Component
class DataClassController {

    @ResponseStatus(HttpStatus.OK)
    @RequestMapping(value = "/update", method = arrayOf(PUT), produces = arrayOf(APPLICATION_JSON_VALUE))
    fun update(@RequestBody resource: DataClassResource) {
        ...
    }
}

RequestBody アノテーションの付与されたクラスのコンストラクタを呼んで初期化するのですが、Kotlin のプライマリコンストラクタが見つけられないようです。

この問題は Kotlin 対応バージョンの Jackson module を依存に追加することで解決できます。以下の package を依存追加しましょう。

<dependency>
    <groupId>com.fasterxml.jackson.module</groupId>
    <artifactId>jackson-module-kotlin</artifactId>
    <version>2.8.0</version>
</dependency>

Tips :cookie:

static メソッドの Extension は Companion Object を持つクラスしかできないため、Java クラスは拡張できない

Kotlin で static メソッドを拡張するときは Companion を通して実装します。しかし Java のクラスは Companion Object を持たないので以下のような拡張関数や拡張プロパティを実装できません。

fun JavaClass.Companion.extensionMethod() {
    ...
}

YouTrack に Issue も挙がっています。是非実装されてほしいです。
https://youtrack.jetbrains.com/issue/KT-11968

Null の可能性がある Java メソッドは Optional で支える

Java のメソッドは Optional でなくとも null の可能性はあります。Kotlin の領域でしっかりと安全性を担保するなら、Java メソッドの結果は Optional で受け取るのが良さそうですね。

val maybe: String? = javaClass.method()

Bean の Injection を val 定義

Autowired アノテーションで Injection 対象のプロパティを定義するとき、普通に定義すると未初期化でコンパイルエラーになるため遅延初期化の lateinit で定義します。

@Component("sample")
class Sample {
    @Autowired
    private lateinit var dependency: Dependency
}

しかし、lateinitvar でしか定義できません。 val で定義したい場合には Primary Constructor を利用します。

@Component("sample")
class Sample @Autowired constructor(private val dependency: Dependency) {
}

変更のないプロパティは、できるだけ Immutable で定義したいですね。

終わりに

躓いた点がありつつも、安定したベロシティで開発を進めることができました。コンパイラレベルで安全性が増し、コードの見通しも上がりました。今後スケーリングするときにそのサポートの威力が発揮されると思います。
Kotlin 特有の記述もあるので、どのような方針で行くかもレビューで探りながら進められるとよいです!とは言え、Java から Kotlin への移行の敷居や学習コストは実施してみて改めて低いと感じました。Server Side Kotlin もどんどん盛り上がって欲しいです :tada: