KtorはKotlinを使用して非同期サーバまたは非同期Webクライアントを構築できるフレームワーク。
Ktorは様々なアプリケーションを相互接続することを目的として作られたKotlinフレームワーク。基本的にはHTTPによる相互接続になっておりサーバーとクライアントの機能を提供している。KotlinのCoroutineを使っているので非同期で処理を実行することができるのが特徴。Kotlinのマルチプラットフォーム対応にも対応しているのでJavaScriptやiOS,Android環境でも動作することができるらしい。
使用したバージョン
KtorlはIntelliJのプラグインを使用することで簡単にHello,Worldプロジェクトを作成できる。ちなみに、作成されたプロジェクトはデフォルトでGradleプロジェクトになっている。
デフォルトで以下のファイルが作成される。 src/Application.kt
はエントリーポイントになる。
HelloWorld
├── build.gradle
├── gradle
│ └── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradle.properties
├── gradlew
├── gradlew.bat
├── resources
│ ├── application.conf
│ └── logback.xml
├── settings.gradle
└── src
└── Application.kt
Application.kt
サーバーのエントリーポイントに当たるファイル。プロジェクトを作成した段階では以下のような内容になる。
// Application.kt
package com.example.ktor
import io.ktor.application.Application
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) {
}
main
関数が定義されているので、この関数を呼び出すことでサーバーを起動することができるが、resource/application.conf
を使用してアプリケーションを起動する方法もある。conf
ファイルを使用して起動した方が、アプリケーションに関する設定値をソースファイルから分離することができる。デフォルトでは以下の内容が生成される。
// application.conf
ktor {
deployment {
port = 8080
port = ${?PORT}
}
application {
modules = [ com.example.ktor.ApplicationKt.module ]
}
}
IntelliJでconf
ファイルを使用するにはbuild.gradle
ファイルに以下の行を追加し(IntelliJ プラグインでプロジェクトを作成した場合はデフォルトで出力されていた)Run -> Edit Configurations でKotlinのMainClassをio.ktor.server.netty.EngineMain
にして実行する。
apply plugin: 'application'
...
mainClassName = "io.ktor.server.netty.EngineMain"
ただ、デフォルトの内容でサーバーを起動してもエンドポイントが作成されていないのでアクセスできない。エンドポイントを以下のように追加することで、"Hello, World"を返すサーバーになる。
// application.conf
package com.example.ktor
import io.ktor.application.Application
import io.ktor.application.call
import io.ktor.http.ContentType
import io.ktor.response.respondText
import io.ktor.routing.get
import io.ktor.routing.routing
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) {
routing {
get("/") {
call.respondText("Hello World!", ContentType.Text.Plain)
}
}
}
Gradleに関するファイルは以下の通りで生成されている。
gradle.properties
ktor_version=1.1.2
kotlin.code.style=official
kotlin_version=1.3.20
logback_version=1.2.1
build.gradle
buildscript {
repositories {
jcenter()
}
dependencies {
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
apply plugin: 'kotlin'
apply plugin: 'application'
group 'helloworld'
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"
}
KtorはKotlinのDSLを使って直感的にルーティングを行うことができる。ルーティングは木構造で定義することができ、リクエストがルーティングの条件と一致するまで再帰的に処理が遷移する。
例えば、以下のように書くことでルーティングを定義できる。基本的にはroute
でパスを組み立て、param, header, method
でリクエストの条件を指定し、get, post, delete, put
を使用してエンドポイントの定義かつハンドラーの実装を行う感じ。
package com.example.ktor
import io.ktor.application.Application
import io.ktor.application.call
import io.ktor.response.respondText
import io.ktor.routing.*
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) {
// ルーティング
routing {
// routeでパスを組み立てる
route("a") {
// get, post, delete, putでエンドポイントを定義しハンドラーを実装できる
get {
call.respondText("GET a")
}
// headerでヘッダーに対する条件を指定できる
header("name", "bookstore") {
get {
call.respondText("GET a/b : has query parameter [name] value [bookstore]")
}
}
// routeは入れ子にすることで階層構造にすることができる
route("b") {
// paramでリクエストパラメータに対する条件を指定できる
param("name") {
// get, post, delete, putにパスを指定することも可能
get("c") {
call.respondText("GET a/b/c : has query parameter [name]")
}
}
}
// { ... } を使うことでリクエストパスに含まれるパラメータを取得できる
get("e/{username}") {
call.respondText("GET a/b/c/d/e : has path parameter ${call.parameters["username"]}")
}
}
}
}
リクエストパスにパラメータを含めることができる。パラメーターはString
としてハンドラの中で参照することができる。
// パスの一部を { ... } で区切ることでパスにパラメータを含めることができる。
// リクエストパス:login/fogefoge レスポンス:fogefoge
get("login/{username}") {
call.respondText(call.parameters["username"] ?: "error")
}
// リクエストパス:login/bookstore/infomation/10 レスポンス:bookstore , 10
get("login/{username}/infomation/{pagenumber}") {
call.respondText("${call.parameters["username"]} , ${call.parameters["pagenumber"]}")
}
パスに含めるパラメータは上の例の他にも、以下のように定義することができる。
書き方 | 内容 |
---|---|
{param?} |
オプションのパラメータ。あってもなくても良い。ある場合は値を指定した名前(例ではparam)で取得できる。 |
* |
ワイルドカード。全てのパラメータに一致する。空白不可。 |
{...} |
テイルカード。URI全体に一致する。一番最後に書く必要がある。空白可。 |
{param...} |
テイルカード。URI全体に一致する。一番最後に書く必要がある。空白可。指定した名前(例ではparam)で取得できる。 |
実際に書いてみると以下のようになる。注意して欲しいのはワイルドカードとテイルカードの微妙な違い。ワイルドカードの場合、パスパラメータに何もなければ404になり一致しない。一方、テイルカードはパスパラメータに何もなくても一致する。
// リクエストパス:login/optional/fogefoge レスポンス:fogefoge
// リクエストパス:login/optional レスポンス:no name
get("login/optional/{username?}") {
call.respondText(call.parameters["username"] ?: "no name")
}
// リクエストパス:login/wildcard/fogefoge レスポンス:wildcard
// リクエストパス:login/wildcard レスポンス:(404 not found)
get("login/wildcard/*") {
call.respondText("wildcard")
}
// リクエストパス:login/tailcard/fogefoge レスポンス:Tailcard
// リクエストパス:login/tailcard/fogefoge/foge レスポンス:Tailcard
// リクエストパス:login/tailcard レスポンス:Tailcard
get("login/tailcard/{...}") {
call.respondText("Tailcard")
}
// リクエストパス:login/tailcard_capture/fogefoge レスポンス:[fogefoge]
// リクエストパス:login/tailcard_capture/fogefoge/foge レスポンス:[fogefoge, foge]
// リクエストパス:login/tailcard_capture_capture レスポンス:[]
get("login/tailcard_capture/{usernames...}") {
val all: List<String>? = call.parameters.getAll("usernames")
call.respondText(all.toString())
}
ハンドラの処理の前に特定の処理を割り込ませることができる。
route("admin/{loginInfo}") {
// intercept関数でインターセプターを定義できる。
// 後続のハンドラに処理が遷移する前に必ず実行される。
intercept(ApplicationCallPipeline.Features) {
// インターセプター内でパスパラメータを参照できる。
val loginInfo = call.parameters["loginInfo"]
// パスパラメータが期待と一致しなければリクエストを弾く
if (loginInfo != "bookstore") {
call.respond(HttpStatusCode.NotAcceptable)
// 後続の処理を実行したくない場合や、インターセプター内でクライアントにレスポンスを送った場合
// 以下の様にすることで処理を終了できる。
return@intercept finish()
}
}
get {
call.respondText("intercept")
}
}
インターセプター内でレスポンスを送った場合はreturn@intercept finish()
を実行しないと、後続のハンドラが実行されレスポンスを2回送ろうとして以下の様に例外が発生する。
2019-02-24 19:41:27.709 [nettyCallPool-4-1] ERROR Application - 406 Not Acceptable: GET - /admin/bookstor io.ktor.server.engine.BaseApplicationResponse$ResponseAlreadySentException: Response has already been sent
at io.ktor.server.engine.BaseApplicationResponse.commitHeaders(BaseApplicationResponse.kt:40)
(以下略)
intercept
関数は第一引数にPipelinePhase
を、第二引数に割り込み処理のラムダを受け取る。PipelinePhase
はフェーズの名前を表現するクラスでインターセプターを作る場合にはApplicationCallPipeline.Features
が使われる。
ハンドラやインターセプターにおいてApplicationCall
を取得する事が出来る。ApplicationCall
はKtorにとってとても重要なクラスで、ほとんどの機能はこのクラスによって実現されているらしい。
get {
// call によって ApplicationCall のインスタンスを取得することができる。
call.respondText("Hello, World")
}
request
はその名前の通り、リクエストに関する情報。ApplicationCall
を通じてアクセスすることができる。
ハンドラやインターセプター内で取得することができる。取得するにはcall.request
と呼び出せば良い。例えばこの様にアクセスできる。
get("/") {
// request からリクエストの情報を取得することができる
val uri: String = call.request.uri
val headers: Headers = call.request.headers
val cookies: RequestCookies = call.request.cookies
val httpMethod: HttpMethod = call.request.httpMethod
// 内部のコンテキストも取得することができる
val pipeline: ApplicationReceivePipeline = call.request.pipeline
}
request.queryParameters
とすることでクエリパラメータにアクセスできる。例えば、?param1=value1¶m2=value2
というクエリパラメータがあった場合、それぞれコレクションの様に保持され以下の様にアクセスできる。当然ながら、該当するクエリパラメータがない場合もあるので、戻り値はNull許容型になる。
get("/") {
val value1: String? = call.request.queryParameters["param1"]
}
複数のクエリパラメータをリストとして受け取る場合はgetAll
を使う。例えば?param1=a¶m1=b¶m1=c
の様なクエリパラメータの場合、以下の様になる。
get("/") {
val value1 = call.request.queryParameters.getAll("param1")
print(value1) // [a, b, c]
}
フォーム値はcall.receiveParameters
として受け取ることができる。フォーム値はString
のマップによって表現されているみたいで、クエリパラメータ同様call.receiveParameters[" フォーム値名 "]
で取得可能。
例えば以下の様にname=bookstore
, age=25
とPOSTすると...
POST / HTTP/1.1
Host: localhost:8080
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW
cache-control: no-cache
Postman-Token: bc85d43d-5575-4aaf-b1bb-9b707d1ab012
Content-Disposition: form-data; name="name"
bookstore
Content-Disposition: form-data; name="age"
25
------WebKitFormBoundary7MA4YWxkTrZu0gW--
以下の様に取得できる。
post("/") {
val receiveParameters = call.receiveParameters()
val userName: String? = receiveParameters["name"]
val userAge = receiveParameters["age"]
call.respond("Hey $userName, your age is $userAge")
}
response
はレスポンスを表すインスタンス。このインスタンスを通じてクライアントに対してレスポンスを行う。
例えば単純にテキストをクライアントに返すにはこうする。
get("/") {
call.respondText("Hello, I`am Ktor")
}
以下の様に書くこともできるが、通常は上記の様な便利な関数を使うだろう。ちなみに、コンテンツタイプをセットする機能は見当たらなかった。response
では自動的にコンテンツタイプが付与される見たい。
get("/") {
val applicationCall = call.response.call
call.response.status(HttpStatusCode.OK)
call.response.pipeline.execute(applicationCall, "Hello, I`am Ktor")
}
ラムダを渡すことで非同期でレスポンスを返すこともできる。
get("/") {
call.respondText {
// なんか長い処理...
"Hello, I`am Ktor"
}
}
他にもいろんな便利なメソッドが用意されている。
メソッド | 内容 |
---|---|
status(HttpStatusCode.OK) |
HTTPステータスコードを設定 |
status(HttpStatusCode(999, "Hey!") |
HTTPステータスコード(カスタム)を設定 |
status() |
現在設定されているHTTPステータスコードを取得 |
header("X-Custom-Header", "Hey!") |
ヘッダーを設定 |
etag("283882kdkks2j2j3h2j...") |
etagを設定 |
lastModified(ZonedDateTime.now()) |
Last-Modified ヘッダーを設定 |
contentLength(1024L) |
Content-Length を設定。通常は明示的に設定しなくても自動的に設定される。 |
cacheControl(CacheControl.NoStore(CacheControl.Visibility.Private)) |
Cache-Control ヘッダーを設定 |
expires(LocalDateTime.now()) |
Expires ヘッダーを設定 |
response
を使ってリダイレクト処理を行うことができる。
respndRedirect
の第一引数はリダイレクト先で、第二引数のpermanent
にBooleanで恒久的か一時的かを設定できる。
例えばこの様に実装できる。
routing {
get("/") {
call.respondRedirect("/redirect", true)
}
get("/redirect") {
call.respondText {
"You Redirected"
}
}
}