NIKKEI TECHNOLOGY AND CAREER

Spring FrameworkのKotlinサポート最新動向 (2020年版)

Nikkei Advent Calendarの4日目の記事です。

はじめに

日経IDチームの浦野です。日経IDチームは認証・認可や課金・決済といったプラットフォームの開発を行っているチームで、私は認証・認可プラットフォームの開発を担当しています。

この開発チームでは、システムの刷新プロジェクトを進行中で、SpringのアプリケーションをKotlinで開発しています。

KotlinというとAndroidアプリの開発言語というイメージが強いかもしれませんが、Javaとの相互運用性が高い言語で、サーバーサイドでの採用事例も増えてきています。 Javaの主要なWebアプリケーションフレームワークであるSpringもKotlinをサポートしています。

そこで2020年現在、SpringによるKotlinのサポートがどの程度進んでいるのかをご紹介したいと思います。

KotlinでSpring Bootアプリケーションの開発をはじめる

Spring Bootのテンプレートを作成するWebサイトのSpring Initializrは、Kotlinにも対応しています。

以下がテンプレートから作成されたアプリケーションのエントリーポイントです。

@SpringBootApplication
class DemoApplication

fun main(args: Array<String>) {
    runApplication<DemoApplication>(*args)
}

Javaで生成されるコードと比べると空のclassに @SpringBootApplication がついている点や、mainがトップレベルで定義されている点などが異なり、Javaのテンプレートに慣れている人は違和感を覚えるかもしれません。

@SpringBootApplication
public class DemoApplication {

  public static void main(String[] args) {
    SpringApplication.run(DemoApplication.class, args);
  }

}

このアプリケーションに、ユーザーを登録したり、ユーザーの情報を参照したりするAPIを実装してみます。 それぞれのユーザーを表現するUserクラスは以下のように定義します。

data class User(val id: Int, val name: String)

Controllerが受けたリクエストを実際に処理するUserServiceは以下のようなインターフェースとします。

interface UserService {
    fun get(id: Int): User?
    fun create(name: String): User
}

続いてRESTの各URIと実際の処理を紐付けるControllerを書くと、以下のようになります。

@RestController
class UserController(private val userService: UserService) {
    @GetMapping("/user")
    fun get(@RequestParam id: Int) = userService.get(id)

    @PostMapping("/user")
    fun post(@RequestBody req: CreateUserRequest) = userService.create(req.name)

    data class CreateUserRequest(val name: String)
}

Javaと同じように、アノテーションを使ってルーティングの設定を書くことができました。 @Bean@Componentといったそのほかのアノテーションも問題なく使用できるので、 Kotlinでアプリケーションを書くといっても、Javaと文法が異なるだけで特別なことをする必要はほとんどありません。

さて、このアプリケーションのテストを書いてみましょう。

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
internal class UserControllerTest {

    @Autowired
    private lateinit var testRestTemplate: TestRestTemplate

    @LocalServerPort
    private var port: Int = 0

    @Test
    fun get() {
        val id = 1
        val url = "http://localhost:${port}/user?id=${id}"
        val expected = User(id = id, name = "John Smith")
        val actual = testRestTemplate.getForObject(url, User::class.java)
        assertEquals(expected, actual)
    }

    @Test
    fun post() {
        val name = "John Smith"
        val url = "http://localhost:${port}/user"
        val requestBody = mapOf("name" to name)
        val actual = testRestTemplate.postForObject(url, requestBody, User::class.java)
        val expected = User(id = actual.id, name = name)
        assertEquals(expected, actual)
    }
}

このテストコードでは、@Autowiredを使ってDIコンテナからインスタンスをインジェクションしています。

Kotlinはnull safetyな言語設計となっているため、プロパティは通常宣言と同時に初期化するためのコードを書く必要があります。 testRestTemplateのようにDIコンテナからインスタンスをインジェクションするようなプロパティは@Autowiredで、フィールドインジェクションする場合はlateinit varというキーワードとともにプロパティを宣言する必要があります。 もっとも、フィールドインジェクションではvalを使用できず、プロパティをイミュータブルにできるというメリットが享受できません。 Springのドキュメントではコンストラクタインジェクションが推奨されています。

また、@LocalServerPortのようにプリミティブ型として扱われる値にインジェクションする場合はlateinit varが使用できません。そのため、ワークアラウンドとして初期値を仮代入する必要があります。

上記のように、多少Kotlinならではの記述を必要とする部分もありますが、Spring Bootのアプリケーションやテストも、ほとんどJavaのコードと変わらない感覚で書くことができます。

DSLで設定

KotlinではJavaのような記法に加えて、アプリケーションの設定を使いやすくて簡潔なDSLで記載する方法が提供されています。

Kotlinは文法上DSLを作成しやすい言語で、Springアプリケーションの色々な設定をDSLで記述できるようにするSpring Fuというプロジェクトがベースになっています。

DSLを使って設定を書いていくことで、よりfunctionalな記述が可能になります。

Router DSLを使うと先程のUserControllerは以下のように書き換えることができます。

fun userRouter(userHandler: UserHandler) = router {
    GET("/user", userHandler::get)

    POST("/user", userHandler::post)
}

Router DSLが登場した当初は、Spring WebFluxのみのサポートでしたが、Spring Framework 5.2以上のバージョンではSpring MVCもサポートされています。 ただし、Router DSLでは、ServerRequestを受け取りServerResponseを返す必要があります。リクエストを処理するために UserHandler を作成し処理を実装します。

class UserHandler(private val userService: UserService) {
    fun get(req: ServerRequest): ServerResponse = req.paramOrNull("id")?.toInt()
            ?.let { userService.get(it) }
            ?.let {
                ServerResponse.ok().body(it, object : ParameterizedTypeReference<User>() {})
            } ?: ServerResponse.notFound().build()

    fun post(req: ServerRequest): ServerResponse = req.body(PostUserRequest::class.java).name
            .let { userService.create(it) }
            .let { ServerResponse.ok().body(it) }

    private data class PostUserRequest(val name: String)
}

Bean定義もDSLで行うことができます。 Bean Definition DSLを使って、先ほどのUserHandlerとDSLで記述したルーティングの設定をBeanとして登録してみます。

bean というキーワードとラムダ式でBeanを定義していきます。

ラムダ式の中では、ref()でDIコンテナからインスタンスをインジェクションすることができます。

fun beans() = beans {
    bean { UserHandler(ref()) }
    bean { userRouter(ref()) }
}

Bean Definition DSLを使って定義したBeanは以下のようにエントリーポイントでApplicationContextのInitializerとして追加します。

fun main(args: Array<String>) {
    runApplication<DemoApplication>(*args) { addInitializers(beans()) }
}

2020年現在の最新バージョンであるSpring Framework 5.3ではSpring SecurityのDSLによる設定がサポートされるなど、裾野が広がっています。

coroutineサポート

Spring Framework 5.3では、Spring MVC と WebFluxの両方でKotlinのcoroutineがサポートされています。

具体的には以下のようなことがサポートされています:

  • ControllerでDeferred<T>, Flow<T>の戻り値をサポート
  • Controllerでsuspend functionをサポート

Spring MVCの場合はcoroutineサポートの恩恵がイマイチわかりづらいかもしれません。 WebFluxの場合、 fun handler(): Mono<T>suspend fun handler(): T に、 fun handler(): Flux<T>fun handler(): Flow<T> という書き方に置き換えられます。

Kotlinのcoroutineの知識があれば、MonoやFluxといったReactorやリアクティブプログラミングの知識がなくても従来のように命令的にノンブロッキングなWebFluxのアプリケーションを書くことができるため、WebFluxの敷居が下がりそうです。

例えば、前節で出てきたUserControllerを次のように書き換えることができます。

@RestController
class UserController(private val userService: UserService) {
    @GetMapping("/user")
    fun getAsync(@RequestParam id: Int): Deferred<User?> = userService.getAsync(id)

    @PostMapping("/user")
    suspend fun post(@RequestBody req: CreateUserRequest): User = userService.createAsync(req.name).await()

    data class CreateUserRequest(val name: String)
}

ユーザーを取得するエンドポイントはfun get(): User?からfun getAsync(): Deferred<User?>に置き換えています。

Deferred<T>はスレッドをブロックせずに値を返すインターフェースで、await()を呼ぶことでT型の戻り値が返されますが、ここではawait()を呼ぶことなくDeferred<User?>型の戻り値を直接Controllerのメソッドの戻り値とすることができるようになっています。

また、ユーザーを作成するエンドポイントはfun post() から suspend fun post()に書き変わっていて、関数内で中断関数のawait()を呼び出しています。

UserServiceクラスは以下のように実装してみました。

@Service
class UserService(private val userRepository: UserRepository) {
    fun getAsync(id: Int): Deferred<User?> = GlobalScope.async { userRepository.findById(id) }
    fun createAsync(name: String): Deferred<User> = GlobalScope.async { User(id = idGenerator.next(), name = name).also { userRepository.save(it) } }

    companion object {
        val idGenerator = sequence {
            var seq = 1
            repeat(Int.MAX_VALUE) {
                yield(seq)
                seq++
            }
        }.iterator()
    }
}

なお、実際のコードではUserRepositoryの処理をノンブロッキングな処理に書き換える必要があります。

build.gradle.kts で依存するパッケージに以下を追加します。

dependencies {
    implementation("org.springframework.boot:spring-boot-starter-webflux")
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core")
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor")
}

これで、MonoFluxといったReactorのインターフェースに触れずにWebFluxのアプリケーションに変換することができました。

おわりに

最後までお付き合いいただきありがとうございます。

この記事では、2020年現在におけるSpring FrameworkのKotlinサポート状況最新動向を紹介しました。

筆者が所属する日経IDチームでもSpring with Kotlinを採用していますが、Kotlinはサーバーサイドでも十分通用していますし、筆者自身さまざまなプログラミング言語に触れてきましたがKotlinはとても書き味の良い言語だと感じています。

現状ではサーバーサイドのAltJava言語としてはScalaに一日の長があるように思いますが、 Kotlinは言語デザイン上の原則として、実用的な進化の原則(Principles of Pragmatic Evolution)を掲げており、Scalaとはまた違った方向性の進化を遂げていくのではないでしょうか。 今後Kotlinがどのように発展を遂げていくのか、楽しみです。

筆者個人としては、Kotlinは非常に扱いやすい言語だと感じていますので、KotlinがAndroidの開発言語としてだけでなく、サーバーサイドの言語としても利用が広がっていってくれると嬉しい限りです。

浦野裕也
ENGINEER浦野裕也

Entry

各種エントリーはこちらから

キャリア採用
Entry
新卒採用
Entry
短期インターン
Entry
カジュアル面談
Entry