Kotlinでつくるサーバサイドアプリケーション【02】
この記事はなに?
以下の記事の続きです
今回の内容
前回、Kotlin製のフレームワークであるKtorの導入を行いました。今回は、そこから少しだけ発展して、
- 型安全なルート定義
- JSONのやり取り
あたりに触れていきたいと思います。
型安全にルートを定義する
依存関係の解決
Ktor ver.2では、Resources
モジュールを使って型安全なルートを定義します。ちなみにver.1時代では Location
モジュールを使っていました
// 追加
implementation("io.ktor:ktor-server-resources:$ktor_version")
プラグインをインストールする
依存関係を追加したら、忘れずにプラグインをインストールしましょう
fun Application.module{
// 追加
install(Resources)
}
エンドポイントを定義する
ひとまず、こんな感じでしょうか
import io.ktor.resources.Resource
import io.ktor.server.application.*
import io.ktor.server.resources.get
import io.ktor.server.response.*
import io.ktor.server.routing.*
@Resource("/test")
class TypeSafeRoutes{
@Resource("{id}")
class Details(val parent: TypeSafeRoutes = TypeSafeRoutes(), val id: String)
}
fun Route.typesafeRoutes(){
get<TypeSafeRoutes>{
}
get<TypeSafeRoutes.Details> {
call.respondText("String ID: ${it.text}")
}
}
ルートの登録も忘れてはいけません
fun Application.module{
install(Routing){
// 追加
typesafeRoutes()
}
}
呼び出してみる
./gradlew run
で実行したあと、curlを使ってAPIを呼び出してみます。
curl -vvv http://localhost:8888/test/something_id
* Trying 127.0.0.1:8888...
* Connected to localhost (127.0.0.1) port 8888 (#0)
> GET /test/something_id HTTP/1.1
> Host: localhost:8888
> User-Agent: curl/7.81.0
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Content-Length: 23
< Content-Type: text/plain; charset=UTF-8
<
* Connection #0 to host localhost left intact
String ID: something_id
しっかりとIDが受け取れていますね
では、 Details
のid
の型を String
からInt
に変えてみるとどうでしょう?
@Resource("{id}")
class Details(val parent: TypeSafeRoutes = TypeSafeRoutes(), val id: Int)
~~~~~~~
get<TypeSafeRoutes.Details> {
call.respondText("Int ID: ${it.id}")
}
このAPIを実行してcurlを叩いてみましょう
$ curl -vvv http://localhost:8888/test/1234567
* Trying 127.0.0.1:8888...
* Connected to localhost (127.0.0.1) port 8888 (#0)
> GET /test/1234567 HTTP/1.1
> Host: localhost:8888
> User-Agent: curl/7.81.0
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Content-Length: 15
< Content-Type: text/plain; charset=UTF-8
<
* Connection #0 to host localhost left intact
Int ID: 1234567
$ curl -vvv http://localhost:8888/test/abcdefg
* Trying 127.0.0.1:8888...
* Connected to localhost (127.0.0.1) port 8888 (#0)
> GET /test/abcdefg HTTP/1.1
> Host: localhost:8888
> User-Agent: curl/7.81.0
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 400 Bad Request
< Content-Length: 0
<
* Connection #0 to host localhost left intact
文字列を渡したらしっかりとエラーになってくれました。
JSONを用いてデータの受け渡しをする
Webアプリケーションを扱うなら、やはりJSONが使いたいですよね?僕は使いたいです。
てなわけで、JSONを使えるようにしましょう
依存関係の解決
例のごとく、build.gradle.kts
に依存関係を追加して、モジュール側でプラグインを有効化します。
なお、jsonのシリアライズ・デシリアライズには jackson
や kotlin-serialization
などを使用することができます。
個人的なプロダクトではjackson
を用いているのですが、ここではkotlin-serialization
を使うことにします。
plugins {
// 追加
id("org.jetbrains.kotlin.plugin.serialization") version "1.8.0"
}
dependenties {
// 追加
implementation("io.ktor:ktor-server-content-negotiation:$ktor_version")
implementation("io.ktor:ktor-serialization-kotlinx-json:$ktor_version")
}
// 追加
install(ContentNegotiation){
json()
}
JSONをレスポンスする
JSONを用いてデータをやり取りするには、それに対応したクラスを作成する必要があります。こういうのをPOJOって言うんですかね?そのへんの言葉はよくわかりませんので。
レスポンス用モデル
というわけで、まずはレスポンスするためのモデルを作成してみます。
今回は適当に、以下のような感じでしょうか
@Serializable
data class TestResponseModel(
val param1: String,
val param2: Int
)
レスポンスする
モデルさえ定義してしまえば、レスポンス自体は簡単で、 ApplicationCall.respond()
にモデルのインスタンスを渡してあげるだけです。
call.respond(
TestResponseModel(
param1 = "param1",
param2 = 1
))
リクエストを受け取る
モデルを定義する
レスポンスするときと同様に、受け取ったボディをデシリアライズするためのクラスを作成します。今回は下記のようなモデルを対象にしてみます。
@Serializable
data class TestRequestModel(
val param1: String,
val param2: Int?
)
リクエストボディの読み取り
前回、リクエストボディを受け取るには以下のようにすると説明しました。
val parameters = call.receiveParameters()
val param1 = parameters.get("param1")
この場合、キーバリュー形式のボディしか読み取ることができません。
今回、作成したモデルに対応したデータに埋め込みたいので、以下のように変更します。
val parameters = call.receive<TestResponseModel>()
呼び出してみる
というわけで、ここまででJSONのやり取りができるようになったので、実際に呼び出してみます。
なお、今回は下記のようなルートを設定しました。
post("/"){
val req = call.receive<TestRequestModel>()
call.respond(
TestResponseModel(
param1 = "param1 = ${req.param1}",
param2 = req.param2 ?: -1
))
}
これに対していろいろと投げてみます。
まずは普通に投げてみます。
url -vvv -X POST -H "Content-Type:application/json" http://localhost:8888/ -d '{"param1": "string parameter", "param2": 1111}'
Note: Unnecessary use of -X or --request, POST is already inferred.
* Trying 127.0.0.1:8888...
* Connected to localhost (127.0.0.1) port 8888 (#0)
> POST / HTTP/1.1
> Host: localhost:8888
> User-Agent: curl/7.81.0
> Accept: */*
> Content-Type:application/json
> Content-Length: 46
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Content-Length: 52
< Content-Type: application/json
<
* Connection #0 to host localhost left intact
{"param1":"param1 = string parameter","param2":1111}
次に、リクエストの param2
を欠損させてみます。Optionalになっているので欠けていても良さそうですが、どうでしょう。。。
curl -vvv -X POST -H "Content-Type:application/json" http://localhost:8888/ -d '{"param1": "string parameter"}'
Note: Unnecessary use of -X or --request, POST is already inferred.
* Trying 127.0.0.1:8888...
* Connected to localhost (127.0.0.1) port 8888 (#0)
> POST / HTTP/1.1
> Host: localhost:8888
> User-Agent: curl/7.81.0
> Accept: */*
> Content-Type:application/json
> Content-Length: 30
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 400 Bad Request
< Content-Length: 0
<
* Connection #0 to host localhost left intact
他にも、型が合わない場合や、余計なフィールドが混じっているパターンなど色々考えられますが、 めんどくさくなってきた 挙げていくとキリがないのでここでは触れないことにします。試してみてください。
まとめ
最後のほうがだいぶ駆け足になってしまいましたが、
-
Resources
プラグインを使って、型安全なルートを作成できる -
ContentNegotiation
と、適宜シリアライザを導入することで、JSONを取り扱うことができる
という内容でした。冒頭に書いたとおりでしたね。
最後に
Ktorを使ったAndroidアプリを出しているので興味があればぜひ。
Discussion