Kotlin Advent Calendar 2020 1日目の記事です。
サーバーサイドKotlinで使われるフレームワークとして、Spring BootとKtorはよく挙げられます。
Spring BootがJavaの流れを組んで主流で、新鋭の軽量なフレームワークとしてKtorがあるというイメージだと思います。
今回は本当に基本的な部分ですが、この2つのフレームワークの実装方法を並べて比較してみたいと思います。
導入
まずはプロジェクトの作成、起動から。
プロジェクトの作成
Spring Boot
Spring BootはSpring Initializrで作成します。
IntelliJ IDEAでSpring Bootプロジェクトとして作成することも可能ですが、有償版のみの機能になるのでここでは使いません。
GENERATEボタンでzipがダウンロードされるので、任意の場所に解凍しIntelliJ IDEAでインポートします。
Ktor
こちらはIntelliJ IDEAのプロジェクトとして作成します。
Ktorプラグインがないとプロジェクト種別として選択できないため、入ってない場合はインストールしてください。
build.gradle
作成されたプロジェクトのbuild.gradleの比較です。
Spring Initializrの方はKotlinを選択するとktsで作成されるため、Ktorプロジェクトの方も合わせてktsで作成しています。
Spring Boot
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { id("org.springframework.boot") version "2.4.0" id("io.spring.dependency-management") version "1.0.10.RELEASE" kotlin("jvm") version "1.4.10" kotlin("plugin.spring") version "1.4.10" } group = "com.example" version = "0.0.1-SNAPSHOT" java.sourceCompatibility = JavaVersion.VERSION_11 repositories { mavenCentral() } dependencies { implementation("org.springframework.boot:spring-boot-starter-web") implementation("com.fasterxml.jackson.module:jackson-module-kotlin") implementation("org.jetbrains.kotlin:kotlin-reflect") implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") testImplementation("org.springframework.boot:spring-boot-starter-test") } tasks.withType<Test> { useJUnitPlatform() } tasks.withType<KotlinCompile> { kotlinOptions { freeCompilerArgs = listOf("-Xjsr305=strict") jvmTarget = "11" } }
Ktor
import org.jetbrains.kotlin.gradle.dsl.Coroutines import org.jetbrains.kotlin.gradle.tasks.KotlinCompile val ktor_version: String by project val kotlin_version: String by project val logback_version: String by project plugins { application kotlin("jvm") version "1.4.10" } group = "com.example" version = "0.0.1" application { mainClassName = "io.ktor.server.netty.EngineMain" } repositories { mavenLocal() jcenter() } dependencies { implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version") implementation("io.ktor:ktor-server-netty:$ktor_version") implementation("ch.qos.logback:logback-classic:$logback_version") testImplementation("io.ktor:ktor-server-tests:$ktor_version") } kotlin.sourceSets["main"].kotlin.srcDirs("src") kotlin.sourceSets["test"].kotlin.srcDirs("test") sourceSets["main"].resources.srcDirs("resources") sourceSets["test"].resources.srcDirs("testresources")
アプリケーションの起動
Spring Boot
以下のmain関数を実行します。
@SpringBootApplication class DemoSpringApplication fun main(args: Array<String>) { runApplication<DemoSpringApplication>(*args) }
もしくは
- IntelliJ IDEAのGradleビューから Tasks -> application -> bootRun を実行
- ターミナルからプロジェクトのルートで
./gradlew bootRun
を実行
でも起動できます。
Ktor
以下のmain関数を実行します。
fun main(args: Array<String>): Unit = io.ktor.server.netty.EngineMain.main(args) @Suppress("unused") // Referenced in application.conf @kotlin.jvm.JvmOverloads fun Application.module(testing: Boolean = false) { }
もしくは
- IntelliJ IDEAのGradleビューから Tasks -> application -> run を実行
- ターミナルからプロジェクトのルートで
./gradlew run
を実行
でも実行できます。
いずれもアプリケーションのmain関数が用意されていて、それを実行するとサーバーとアプリケーションが起動してListenになり、似たような形になっています。
基本的な機能
ルーティング
基本的なルーティングの定義です。
まずはGETメソッドで固定の文字列を返す、シンプルな処理を記述してみます。
Spring Boot
Spring Bootでは @RestController
というアノテーションを付けたクラスを作成し、その中で @GetMapping
アノテーションで定義したパスがルーティングの対象として認識されます(HTTPメソッドごとに PostMapping
PutMapping
などのアノテーションがある)。
@RestController class HelloController { @GetMapping("/hello") fun hello(): String { return "hello" } }
Ktor
Ktorでは前述のmain関数のあったファイルに記述されている、Application.module
の中に routing
というブロックで定義します。
GETメソッドの場合は get
にパスを記述し(HTTPメソッドごとに post
put
などの関数がある)、そのブロック内にレスポンスを返す処理を記述します。
@Suppress("unused") // Referenced in application.conf @kotlin.jvm.JvmOverloads fun Application.module(testing: Boolean = false) { routing { get("/hello") { call.respondText("hello") } } }
パスパラメータ
パスパラメータを渡す場合の記述です。
Spring Boot
Spring Bootでは @GetMapping
などのアノテーションで定義されたパスの、パラメータを含めたい箇所に {} で括ったパラメータ名を定義します。
そしてルーティングされた関数の引数に @PathVariable
アノテーションを使用し、渡されるパラメータ名(アノテーションのパスで定義した{}内の名前)を指定します。
@GetMapping("/hello/{name}") fun helloName(@PathVariable("name") name: String): String { return "hello $name" }
Ktor
Ktorもパスのパラメータを含めたい箇所に、 {} で括ったパラメータ名を定義します。
そして call.parameters[パラメータ名]
で取得できます。
get("/hello/{name}") { val name = call.parameters["name"] call.respondText("hello $name") }
ただし、Ktorの方はこのやり方だとパラメータを問答無用にString型で受け取ってしまうため、他の型で扱う場合はキャストや下記のように変換が必要になります。
val id = call.parameters["id"]?.toInt()
このリクエストをタイプセーフに扱うためには Locationsが必要になります。
Locations
を使うにはいくつか手順が必要で、まずはbuild.gradleに依存関係を追加します。
implementation("io.ktor:ktor-locations:$ktor_version")
そして Application.module
内でフィーチャーをinstallします。
install(Locations)
これで Locations
の機能は使えるようになります。
使い方としては、まず以下のようなデータクラスを定義します。
@Location("/hello/{id}") data class HelloLocation(val id: Int)
@Location
アノテーションでパスを定義し、その中でパスパラメータのパラメータ名も指定します。
また、パラメータ名と同じ名前のプロパティをコンストラクタで定義します。
そしてルーティング側の定義は下記のように、型パラメータとして @Location
を付与したデータクラスの型を渡します。
get<HelloLocation> { request -> call.respondText("hello ${request.id}") }
ただし、ルーティングの定義とそれに紐づく処理の定義がバラけてしまうため、視認性は落ちます。
以下で紹介されているような工夫は必要かもしれません。
engineering.visional.inc taro.hatenablog.jp
クエリパラメータ
次はクエリパラメータの定義です。
Spring Boot
Spring Bootでは、ルーティングされた関数の引数に @RequestParam
アノテーションを付与し、パラメータ名を指定します。
@GetMapping("/hello/query") fun helloNameByQuery(@RequestParam("name") name: String): String { return "hello $name" }
Ktor
Ktorではパスパラメータと同様に call.parameters
を使用して取得します。
指定した名前がパスパラメータとして定義されていない場合、クエリパラメータとして扱います。
get("/hello/query") { val name = call.parameters["name"] call.respondText("hello $name") }
こちらもタイプセーフに扱いたい場合は、Locations
を使用します。
以下のようにパスの定義にパラメータを書かず、プロパティを定義すればクエリパラメータとして扱われます。
@Location("/hello/query") data class HelloQueryLocation(val name: String)
get<HelloQueryLocation> { val name = call.parameters["name"] call.respondText("hello $name") }
JSONのリクエスト、レスポンス
REST APIで使用するJSONのリクエスト、レスポンスです。
以下をリクエスト、レスポンスのクラスとして使います(Spring Boot、Ktor共通)。
data class HelloRequest(val name: String) data class HelloResponse(val message: String)
Spring Boot
Spring Bootではルーティングされた関数の引数を前述のデータクラスの型で定義し、 @RequestBody
のアノテーションを付与すると、データクラスと同じ構造のJSONでのリクエストを受け付けます。
@PostMapping("/hello/json") fun helloJson(@RequestBody request: HelloRequest): HelloResponse { return HelloResponse("hello ${request.name}") }
レスポンスはルーティングの項で書いた @RestController
のアノテーションが付与されたクラスであれば、データクラスの構造でJSON化した結果を返却されます。
Ktor
Ktorではまず、Jacksonのフィーチャーを追加する必要があります。
下記の依存関係をbuild.gradleに追加します。
implementation("io.ktor:ktor-jackson:$ktor_version")
そして Application.module
に下記を追加します。
install(ContentNegotiation) { jackson() }
これでリクエスト、レスポンスのシリアライズ、デシリアライズにJacksonが使用されます。
ルーティング側では、call.receive
に型パラメータとしてデータクラスの型を渡すと、JSONのリクエストを受け付けます。
そしてレスポンスは call.respond
にデータクラスの型のオブジェクトを渡すと、同じ構造のJSONを返却します。
post("/hello/json") { val request = call.receive<HelloRequest>() call.respond(HelloResponse("hello ${request.name}")) }
認証、認可
認証、認可の実装も簡単に紹介します。
ベーシック認証
まずはシンプルなベーシック認証です。
Spring Boot
Spring Bootでの認証、認可の実装は、Spring Securityを使用します。
build.gradleに以下の依存関係を追加します。
implementation("org.springframework.boot:spring-boot-starter-security")
Spring Securityはデフォルトでベーシック認証が有効になるため、これだけでアクセス時に認証を求められるようになります。
ID、パスワードはデフォルトで
- ID: user
- パスワード: アプリケーションの起動時に表示される文字列(下記のような形式で表示される)
Using generated security password: xxxxxxxx
となります。
Ktor
KtorではAuthのフィーチャーを追加します。
build.gradleに以下を追加します。
implementation("io.ktor:ktor-auth:$ktor_version")
そして下記のinstallブロックを Application.module
に追加します。
install(Authentication) { basic { validate { credentials -> if (credentials.name == "user" && credentials.password == "password") UserIdPrincipal(credentials.name) else null } } }
こちらはID、パスワードのデフォルト値などはありません。
validate
で使用している credentials
に入力値が入っていて、if文でチェックをしています(ここでは固定値を使用)。
正しい値が入力された場合(ここではuser、passwordという値が入力された場合)、セッション情報を返却しています。
UserIdPrincipal
がセッション情報を格納しているオブジェクトで、コンストラクタに入力されたIDを渡します。
そしてKtorは認証の対象となるルーティング定義を以下のように authenticate
のブロックで囲う必要があります。
authenticate { get("/hello") { call.respondText("hello") } // ・・・ }
他の認証方式の追加(例: Form認証)
他の認証方式の追加です。
ここでは例としてForm認証を使用します。
Spring Boot
下記のように、 WebSecurityConfigurerAdapter
を継承し、@EnableWebSecurity
アノテーションを付与したクラスを作成します。
@EnableWebSecurity class WebSecurityConfig : WebSecurityConfigurerAdapter() { override fun configure(http: HttpSecurity) { http.authorizeRequests() .mvcMatchers("/login").permitAll() .anyRequest().authenticated() .and() .formLogin() } }
ここで認証、認可の挙動の様々な設定を入れるのですが、.formLogin()
を使用することでForm認証が有効になります。
Spring Bootはこれだけでデフォルトのログインページまで用意してくれるため、ブラウザで任意のURLにアクセスするとログインページにリダイレクトされます。
デフォルトのID、パスワードはベーシック認証と同じです。
.mvcMatchers("/login").permitAll()
で指定しているのは、ログインページを認証の対象外(全リクエストを受け付ける)にするための記述です。
Ktor
Ktorは正直Form認証を実装した情報がかなり少なかったのですが・・・
下記のように form
ブロックで定義します。
form { challenge { call.respondRedirect("/login") } validate { credentials -> userParamName = "user" passwordParamName = "password" if (credentials.name == "user" && credentials.password == "password") UserIdPrincipal(credentials.name) else null } }
challenge
で実装しているのが認証失敗した際の処理で、ここでは /login
のパスにリダイレクトしています。
ただし、Spring Bootのようにログインページが用意されているわけではないので、自前で作る必要があります。
validate
で実装しているのは認証時の処理です。
ベーシック認証のところと同様、固定値を入力値と比較して一致した場合はセッションに乗せる情報を返却しています。
userParamName
passwordParamName
で指定しているのは、認証で送られてくるID、パスワードのパラメータ名です。
Ktorではこのように、認証方式ごとに処理の定義ができるようになっています。
他の認証方式を使う場合も、下記のようにブロックを追加すればできます(build.gradleへの依存関係追加が必要なものもあり)。
jwt { // ・・・ } oauth { // ・・・ }
最後に
Spring BootとKtorの基本的な機能の比較をしました。
基本的にSpring Bootはフルスタックなフレームワークな上、過去に色々と追加されてきた経緯もあるので、ちょっと記述を追加するだけでデフォルトとしてはしっかりした機能を提供してくれます。
歴史が長いしJavaのデファクトスタンダードみたいなフレームワークなので、完成度も高いです。
対してKtorはベースの状態でできる機能は限られていて、必要に応じてフィーチャーを追加していく形。
紹介している中でも少し見えたかもしれませんが、機能追加する時に面倒な部分があったり、未成熟な箇所もあります。
ただ、それゆえ軽量であることと、やはりKotlin製ということで期待は高いです。
今回は初級編ということで、またもっと踏み込んだ機能の比較なども(多分)書きます。