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")
}
これで、Mono
やFlux
といったReactorのインターフェースに触れずにWebFluxのアプリケーションに変換することができました。
おわりに
最後までお付き合いいただきありがとうございます。
この記事では、2020年現在におけるSpring FrameworkのKotlinサポート状況最新動向を紹介しました。
筆者が所属する日経IDチームでもSpring with Kotlinを採用していますが、Kotlinはサーバーサイドでも十分通用していますし、筆者自身さまざまなプログラミング言語に触れてきましたがKotlinはとても書き味の良い言語だと感じています。
現状ではサーバーサイドのAltJava言語としてはScalaに一日の長があるように思いますが、 Kotlinは言語デザイン上の原則として、実用的な進化の原則(Principles of Pragmatic Evolution)を掲げており、Scalaとはまた違った方向性の進化を遂げていくのではないでしょうか。 今後Kotlinがどのように発展を遂げていくのか、楽しみです。
筆者個人としては、Kotlinは非常に扱いやすい言語だと感じていますので、KotlinがAndroidの開発言語としてだけでなく、サーバーサイドの言語としても利用が広がっていってくれると嬉しい限りです。