今朝もちょうどこの記事が話題になってましたが
先日Kotlin製のフレームワークである、Ktorのバージョン1.0がリリースされました。
今のところサーバーサイドKotlinのフレームワークはSpringほぼ一択の状態なのですが、可能性を広げる意味でも期待しています。
前から存在は知っていたのですが、正式版も出たということでちょっと触ってみました。
公式のサンプルを参考に、まずはREST APIを作ってみるところとかをやっています。
導入方法
IntelliJ IDEAにプラグイン導入
IntelliJ IDEAにKtorのプラグインがあるので、導入しましょう。
これがあった方が簡単に始められます。
プロジェクト作成
メニューの File -> New -> Project... でKtorのプロジェクトを作ります。
プラグインが入っていれば下記のようにKtorが選択できるようになっています。
一旦初期状態のまま
- Project SDK: 1.8
- Project: Gradle
- Using Netty
- Ktor Version: 1.0.0
で作成しましょう。
アプリケーションサーバーはTomcatとかも使えるみたいですが、Nettyがよく使われているようなのでこちらを使います。
その他の設定はお好みで。
サンプルのプロジェクトは一旦デフォルトの名前で動かします。
まずは動かしてみる
Gradle
生成したプロジェクトのデフォルトの状態ですが、下記になります。
buildscript { repositories { jcenter() } dependencies { classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } apply plugin: 'kotlin' apply plugin: 'application' group 'example' version '0.0.1' mainClassName = "io.ktor.server.netty.EngineMain" sourceSets { main.kotlin.srcDirs = main.java.srcDirs = ['src'] test.kotlin.srcDirs = test.java.srcDirs = ['test'] main.resources.srcDirs = ['resources'] test.resources.srcDirs = ['testresources'] } repositories { mavenLocal() jcenter() } dependencies { compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" compile "io.ktor:ktor-server-netty:$ktor_version" compile "ch.qos.logback:logback-classic:$logback_version" testCompile "io.ktor:ktor-server-tests:$ktor_version" }
dependencies
の io.ktor:
と入っているものがKtor関連の依存関係です。
後でいくつか追加していきます。
サンプルアプリケーション
Application.ktを開くと
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) { }
一旦起動してみましょう。
上記Application.ktのmain関数を実行してもらうだけで大丈夫です。
コンソールに
[main] INFO Application - Responding at http://0.0.0.0:8080
と表示されていれば8080ポートで起動しています。 しかし、まだなにもpathを定義していないので、アクセスしても404エラーになります。
下記のようにルートパスの処理を追加します。
fun Application.module(testing: Boolean = false) { routing { get("/") { call.respondText("Hello World!") } } }
Application.module
Ktorはこの Application.module
の中でWebアプリケーションの処理を定義していきます。
moduleという名前は任意で変更できます(後述します)。
ルーティング
そして routing
ブロックの中でpathのルーティングを定義します。
今回は get("/")
で、GETメソッドでルートパスへのアクセスを定義しています。
レスポンス
call.respondText("Hello World!")
で文字列を返却しています。
アクセス
アプリケーションを再起動し、ブラウザで http://localhost:8080 へアクセスすると、画面に「Hello World!」が表示されたと思います。
起動するモジュールの設定
プロジェクトのresources配下に、applicaiton.confというファイルがあります。
ktor { deployment { port = 8080 port = ${?PORT} } application { modules = [ com.example.ApplicationKt.module ] } }
ここでは起動するポートと、実行するモジュールが定義されています。
先程のApplication.ktのmain関数を起動した時、 port
で定義したポートでNettyを起動し、 modules
で定義したモジュールを起動します。
Application.module
のモジュールという名前は任意と書きましたが、変更する場合はこちらで定義を変更します。
例えばApplication.ktのモジュール名を
fun Application.mainModule(testing: Boolean = false) {
とし、application.confの設定を
modules = [ com.example.ApplicationKt.mainModule ]
と変えればmainModuleという名前で起動します。
pathの共通化
例えば、 /main/1
/main/2
という2つのpathを作りたい時、下記のようにまとめることができます。
route("/main") { get("/1") { call.respondText("main1") } get("/2") { call.respondText("main2") } }
route
でベースとなる main
というpathを設定し、そのブロックの中で後ろのpathを設定します。
これで
http://localhost:8080/main/1
http://localhost:8080/main/2
がそれぞれアクセスできるようになります。
複数モジュールの起動
モジュールを複数起動することもできます。
例えば、別ファイルで下記2つのモジュールを用意したとします。
HogeApplication.kt
@Suppress("unused") // Referenced in application.conf @kotlin.jvm.JvmOverloads fun Application.hogeModule(testing: Boolean = false) { routing { get("/hoge") { call.respondText("Hello Hoge!") } } }
FugaApplication.kt
@Suppress("unused") // Referenced in application.conf @kotlin.jvm.JvmOverloads fun Application.fugaModule(testing: Boolean = false) { routing { get("/fuga") { call.respondText("Hello Fuga!") } } }
そしてapplication.confの modules
にもモジュール名を書き加えます。
modules = [ com.example.ApplicationKt.mainModule, com.example.HogeApplicationKt.hogeModule, com.example.FugaApplicationKt.fugaModule ]
配列なので、カンマ区切りで複数設定できます。
そしてApplication.ktのmain関数を起動すると、
http://localhost:8080/hoge
http://localhost:8080/fuga
にアクセスできるようになります。 実際に開発をする時は、サーバー(この場合はnetty)を起動するmain関数を持ったApplication.ktを用意し、後は機能ごとでファイル、モジュールを分けるというSpringBootに近いイメージになるのかなと思っています(開発規模にもよりますが)。
REST APIを実装してみる
JSONを使う
JSONを扱うのには、Jacksonを使用します。
まず、build.gradleの dependencies
に下記の1行を追加します。
compile "io.ktor:ktor-jackson:$ktor_version"
そして、Application.ktの Application.mainModule
の中に
install(ContentNegotiation) {
jackson {
// ここに設定を書ける
}
}
を追加します。
Ktorはフレームワーク内の機能を使いたい時にこの install
で読み込むことで各モジュールで使えるようになるみたいです。
この ContentNegotiation
を使うことでjacksonの設定を書けるようになります。
ContentNegotiation
はktor-coreに入っているクラスで、 jackson
は ContentNegotiation
の拡張関数として定義されてるようなんですが・・・みたいなところまでは読んだんですが、仕組み的な部分は一旦省略します。
routing
ブロックに次のpathを追加し、実行してみてください。
get("/json") { call.respond(mapOf("status" to "OK")) }
ブラウザで http://localhost:8080/json を叩くと、
{ status: "OK" }
というJSONが返って来ると思います。
POSTでJSONのリクエスト
もうちょっと現実的な使い方を書きます。
まず、リクエスト、レスポンスに紐づくデータを持った下記2つのデータクラスを作ってください。
data class SampleRequest(val id: Int)
data class SampleResponse(val id: Int, val name: String)
そして、 routing
に下記のpathを追加してください。
post("/json") { val request = call.receive<SampleRequest>() val response = SampleResponse(request.id, "ktor") call.respond(response) }
今回は post
で定義しているので、POSTメソッドの /json
というpathになります。
そして call.receive<リクエストの型>
と書くことで、リクエストのJSONを定義したデータクラスにparseして受け取れます。
この処理では、リクエストで受け取ったidと「ktor」というnameを SampleResponse
にsetしています。
最後に、 call.responsed
にレスポンスのデータクラスを渡すことで、JSONにformatしてレスポンスを返します。
ターミナルで次のcurlコマンドを実行して確認しましょう。
curl \ --request POST \ --header "Content-Type: application/json" \ --data '{"id" : 1}' \ http://127.0.0.1:8080/json
下記のJSONが返ってくると思います。
{"id":1,"name":"ktor"}
これで簡単なREST APIが作れました。 実際に開発で使う時は、リクエスト、レスポンスのデータクラスはSwaggerとかでも作れるもので対応できますね。
認証
Basic認証を有効化する
Ktorの機能で認証機構も実装することができます。
SpringでいうSpring Securityみたいなイメージですね。
まず、Gradleのdependencyに下記を追加します。
compile "io.ktor:ktor-auth:$ktor_version"
そしてまたApplication.ktの Application.mainModule
に、下記の install
を追加します。
install(Authentication) { basic { validate { if (it.name == "user" && it.password == "password") UserIdPrincipal("user") else null } } }
Basic認証を走らせる設定になります。
validate
の中で入力されたユーザー名とパスワードをチェックしています。
今回はユーザー名が「user」、パスワードが「password」という文字列と一致すれば認証が通ります。
ここで出てくる it
は UserPasswordCredential
というクラスの値を参照しています。
そして UserIdPrincipal("user")
を返却することで認証成功となります。
この UserIdPrincipal
がユーザー情報としてセッションに保持されます(後述します)。
そして、 routing
に下記のpathを追加します。
authenticate { get("/auth") { call.respondText("Authenticated!!") } }
authenticate
ブロックで括られているpathが認証の対象となります。
この場合 http://localhost:8080/auth へアクセスすると、Basic認証が求められます。
そして先程のユーザー名、パスワードを入力して認証が成功すれば、「Authenticated!!」が表示されます。
今回はBasic認証での処理を紹介していますが、OAuth等にも対応しているようです。
セッションのユーザー情報の取得
前述した認証時に登録されたセッションのユーザー情報を呼び出す方法です。
先程の /auth
の処理を下記のように書き換えます。
get("/auth") { val user = call.authentication.principal<UserIdPrincipal>() call.respondText("${user!!.name}:Authenticated!!") }
call.authentication.principal<UserIdPrincipal>
でセッションの情報を取得しています。
(Null可で取れてしまうので、今回は一旦!!で回避しています)
ユーザー名「user」で登録していたので、 user:Authenticated!!
と表示されます。
ちなみに UserIdPrincipal
は、 io.ktor.auth.Principal
インターフェースを実装したクラスであればなんでも使えます。
なので例えば
data class User(val id: Int, val name: String): Principal
というデータクラスを作って、
// 認証チェック(Userに設定している値は仮) validate { if (it.name == "user" && it.password == "password") User(1, "hoge") else null }
// 取得処理 val user = call.authentication.principal<User>()
みたいにすることもできます。
実際はこういう使い方になるのかな?と思います。
(本当に認証にだけ使いたい場合は UserIdPrincipal
でも充分ですが)
エラーハンドリング
例外に応じて返却するステータスコードを変更する
例外がthrowされた時、その種類に応じてレスポンスで返却するステータスコードを変更できます。
動作確認用に下記のpathを追加します。
get("/exception") { throw IllegalArgumentException("exception example.") }
IllegalArgumentException
を投げるだけの処理です。
そして下記の install
を追加します。
install(StatusPages) {
exception<IllegalArgumentException> { cause ->
call.respond(HttpStatusCode.BadRequest)
}
}
これで http://localhost:8080/exception にアクセスすると、400エラーが返ってくると思います。
ちなみに上記のエラーハンドリングの install
を削除すると、問答無用に500エラーが返ります。
ステータスコードに応じてレスポンスを変更する
さらに、ステータスコードに応じてエラーを返すだけでなく、レスポンスの内容を変更することもできます。
先程の install(StatusPage)
のブロックに、下記を追加してみてください。
status(HttpStatusCode.BadRequest) { call.respond(TextContent("${it.value} ${it.description}", ContentType.Text.Plain.withCharset(Charsets.UTF_8), it)) }
これでもう一度 http://localhost:8080/exception にアクセスすると、
400 Bad Request
という文字列が返って来るようになります。
微妙だったポイント
設定を知らないだけかもしれませんが、微妙だと感じたポイントを2つ。
(解決方法あったら誰か教えてください)
同じpathを2個設定すると先に読み込まれた方が有効になる
例えば、下記のように2つの同じpathを設定していたとします。
get("/sample") { call.respondText("sample1") } get("/sample") { call.respondText("sample2") }
これは普通に起動できます。
そして http://localhost:8080/sample にアクセスすると、 sample1
の文字列が返ってきます。
なので多分上に書いてある方の処理が有効になってるっぽいです。
同様に、他のモジュール間で同じpathが定義されていた場合は、 application.confの modules
の配列で先に書いてあるモジュールの方が優先されているようでした。
これは起動の時点でエラーにして欲しい・・・
application.confのmodulesの記述に正規表現が使えない
application.confの modules
に起動するモジュールを設定する時、
modules = [ com.example.*ApplicationKt.module ]
みたいに特定のファイル名とか、特定のパッケージ配下のモジュールを全部登録する感じにしたいけど、ちょっと触った感じできなさそうでした。
モジュール追加する度に都度書き換えるのは面倒なので、ここはなんとかしたいですね。
まとめ
とりあえず今回はここまで。
今のところ感触としては
- 基本的なREST APIを作るには問題なさそう
- Springは重厚(Web関連以外の要素も色々あるので)なので、ちょっとしたもの作るならKtorでも良さそう
- Kotlin製フレームワーク触るの楽しい(気持ちの問題)
という感じですかね。
まだInterceptor的な使い方とか、認証のセッション情報の保存方法の変更とか、gRPCやGraphQLへの対応とか調べきれてない部分があります。
あと
Coroutines - Quick Start - Ktor
にも書いてありますが、KtorはCoroutinesを重用しているみたいです。
たしかにちょっとフレームワークの中覗いてみただけでも結構出てきました。
今回は使い方の話だけ書いたので、この辺の仕組みとかも追々調べていければと思います。