18
12

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

KtorのLocationsの使い方

Last updated at Posted at 2017-11-23

最近Ktorを触りだしたのですが、あまりにもドキュメントがなかったのでLocationsで試したものを書いてみました。
Versionは0.9.0です。

Ktorとは?

Kotlin製のWeb frameworkです。
Easy to use, fun and asynchronous.を謳っており、現状まだExperimentalなKotlinの言語機能coroutineを利用した実装がされています。
Getting startedや詳細は、ktor.ioGitHubのリポジトリを参照してください。

Locationsとは?

KtorはFeatureと呼ばれるもの必要に応じてinstallすることでWebアプリケーションに必要な機能を拡張するようになっています。(公式ページ)
Featureここなどで実装が進められており、Auth, WebSocket, Jackson, Gson, Guice, HtmlBuilderなどがあります。

LocationsFeatureのうちのひとつで、installすることでType safe routingを実現できます。
でも公式にあるのは空のページだけ…
なので、ktor-locationsの実装やテストコードを読みながら試しました。

installとは?

前述の通り、Webアプリケーションに機能を追加するための仕組みです。
コードでは以下のようになります。

// アプリケーションのエントリポイント
fun Application.main() {
    install(Locations) // ← Locations featureをinstall

    install(WebSockets) { // ← WebSockets featureをinstall
        pingPeriod = Duration.ofMinutes(1) // ← WebSocketsの設定
    }

    install(Routing) { // ← Routing featureをinstall
        get("/") { // ← GET `/` の時の処理を定義
            call.respond("Hello world from Ktor!")
        }
    }
}

このように必要な機能をinstallすることで、その機能が使えるようになります。
Webアプリケーションに必須なRoutingでさえもFeatureとして提供されています。
自前でFeatureの実装もできるようで、簡単なサンプルもあります。

Locations featureの使い方

何ができるのか?

公式のドキュメントがまだないので自分の理解できている範囲での話になりますが、

  • パスをclassobjectを使って構造化でき、一箇所にまとめることができるようにする
  • 型安全にリクエストパラメータやクエリパラメータを扱えるようにする
  • 各パスでの処理の指定をString literalではなく、構造化されたclassobjectで行えるようにする

などかなと思っています。

以下のエンドポイント(GET)でのパス、リクエストパラメータ、クエリパラメータの扱いをSpringと比較してみます。

  • /users
  • /users/search
  • /users/{id}

UserクラスやUserServiceクラス、そのメソッドなどはある前提で🙏

Spring

@RestController
@RequestMapping("/users")
class UserController(private val userService: UserService) {
    @GetMapping
    fun index(): List<User> {
        val users = userService.findAll()
        return users
    }

    @RequestMapping(value = "/search", method = RequestMethod.GET)
    fun search(@RequestParam(name = "id", required = false) id: Int?, @RequestParam(name = "name", required = false) name: String?): List<User> {
        val users = userService.search(id, name)
        return users
    }

    @RequestMapping(value = "/{id}", method = RequestMethod.GET)
    fun get(@PathVariable int id): User {
        val user = userService.find(id)
        return user
    }
}

Locations

// エントリポイント
fun Route.main() {
    install(Locations)
    install(Routing) {
        users()
    }
}

// /users のパスを組み立てる
@location("/users") class Users {
    @location("/search") data class Search(val id: Int? = null, val name: String? = null)
    @location("/{id}") data class Id(val id: Int)
}

// /users 以下の実装
fun Route.users() {
    val userService = UserService()

    get<Users> {
        val users = userService.findAll()
        call.respond(users)
    }

    get<Users.Search> {
        val users = userService.search(it.id, it.name)
        call.respond(users)
    }

    get<Users.Id> {
        val user userService.find(it.id)
        call.respond(user)
    }
}

ひとつずつ見ていきましょう。

パスの構造化とリクエスト・クエリパラメータの扱い

Springでは@RequestMappingなどでパスを指定し、UserControllerクラス全体を使って構造化しています。

@RequestMapping("/users") // ← ここ
class UserController(private val userService: UserService) {
    @GetMapping // ← ここ
    fun index(): List<User> {...}

    @RequestMapping(value = "/search", method = RequestMethod.GET) // ← ここ
    fun search(@RequestParam(name = "id", required = false) id: Int?, @RequestParam(name = "name", required = false) name: String?): List<User> {...}

    @RequestMapping(value = "/{id}", method = RequestMethod.GET) // ← ここ
    fun get(@PathVariable int id): User {...}
}

それに対し、KtorのLocations featureでは@locationをつけたクラスで構造化します。

// /users の定義
@location("/users") class Users {
    // /users/search?id={id}&name={name} の定義
    @location("/search") data class Search(val id: Int? = null, val name: String? = null)
    // /users/{id} の定義
    @location("/{id}") data class Id(val id: Int)
}

@locationに渡すStringでパスを表し、@locationをつけたclassのフィールドでリクエストパラメータやクエリパラメータの値を扱うことができるようになります。
上記の通り、内部クラスに@locationをつけることでネストしたパスも表現ができます。
また、以下のように定義することもできます。

@location("/users/search") data class UserSearch(val id: Int? = null, val name: String? = null)
@location("/users/{id}") data class UserId(val id: Int)

ちなみに、objectでも定義できます。

@location("/single") objet Signle

@RequestMappingと違い、ここでHttp Methodを指定することはできません。

クエリパラメータの扱い

Nullableは普段通り?の有り無しで表現します。
Requiredはデフォルト引数を設定するかどうかで表現するようです。

// Non nullかつ`name`が必須
@location("/non-null-required") data class NonNullRequired(val name: String)
// Non nullかつ`name`が必須ではない
@location("/non-null-option") data class NonNullOption(val name: String = "hoge")
// Nullableかつ`name`が必須
@location("/nullable-required") data class NullableRequired(val name: String?)
// Nullableかつ`name`が必須ではない
@location("/nullable-required") data class NullableOption(val name: String? = null)

なので上記の実装から、Searchclassのデフォルト引数を無くして、

@location("/search") data class Search(val id: Int? = null, val name: String? = null)
// ↓こうする
@location("/search") data class Search(val id: Int?, val name: String?)

/users/searchにクエリパラメータなしでリクエストを送ると/users/{id}にマッチしてしまい、searchは数字じゃないと言われます。

同一名のクエリパラメータを複数扱いたい場合は、Listで受け取ることができます。

@location("/multiple-id") data class MultipleId(val id: List<Int>)

リクエストパラメータの扱い

リクエストパラメータは@locationに渡す文字列のうち、{}で囲ったものをフィールドにマッピングすることができます。
リクエストパラメータは1つ以上指定でき、

@location("/users/{userId}/friends/{friendName}") data class UsersFriend(val userId: Int, val friendName: String)

のように定義することも可能です。

また、同一のリクエストパラメータ名を指定してListとして受け取ることもできます。

@location("/users/{id}/{id}") data class UserIds(val id: List<Int>)

ちなみに、

get<UserIds> {
    call.respond(it.id)
}

に対して/users/1/2?id=3&id=4のようにアクセすると[3, 4, 1, 2]が返ってきます。
クエリパラメータに対して処理をしてからリクエストパラメータの処理をしているようです。

{}で囲う文字とフィールド名は同じである必要があり、一致していない場合はアプリケーション起動時にio.ktor.locations.RoutingExceptionが発生しますが、ビルドは問題なく通ってしまいます。(もちろんIntelliJ IDEAも教えてくれない)
また、リクエストパラメータがフィールドの型と合わない場合はjava.lang.NumberFormatExceptionなどが発生します。

パラメータを持つクラスのネスト

classを分けて構造化できるので、パラメータを持つクラスの中にさらにクラスを作ることができます。
その時は、1つ上の階層のクラスをフィールドとして持っていないといけないようです。

@location("/users") class Users {
    @location("/{id}") data class Id(val id: Int) {
        @location("/friends") data class Friends(val id: Id) {
            @location("/{friendId}") data class FriendId(val friendId: Int, val friend: Friend)
        }
    }
}

最後のFriendIdclassをIdclassにすることはできません。

また、このような定義はビルド、起動までできますが、ランタイムでエラーになります。

@location("/users") class Users {
    // このリクエストパラメータ名と
    @location("/{id}") data class Id(val id: Int) {
        @location("/friends") data class Friends(val id: Id) {
            // このリクエストパラメータ名が一緒
            // 🔥ここにアクセスするとエラーになる🔥
            @location("/{id}") data class FriendId(val id: Int, val friend: Friend)
        }
    }
}

各パスでの処理の指定

Springの場合、@RequestMappingなどが同時に処理の指定となります。

    @GetMapping // ← ここ
    fun index(): List<User> {...}

    @RequestMapping(value = "/search", method = RequestMethod.GET) // ← ここ
    fun search(@RequestParam(name = "id", required = false) id: Int?, @RequestParam(name = "name", required = false) name: String?): List<User> {...}

    @RequestMapping(value = "/{id}", method = RequestMethod.GET) // ← ここ
    fun get(@PathVariable int id): User {...}

Location featureの場合、@locationを付与したclassとio.ktor.locationspackageにあるメソッドを使用して指定します。

    // /users GETの処理
    get<Users> {}

    // /users/search GETの処理
    get<Users.Search> {}

    // /users/{id} GETの処理
    get<Users.Id>

io.ktor.locationspackageにはHttp Methodに対応する以下のメソッドが定義してあります。

  • get
  • post
  • put
  • patch
  • delete
  • head
  • options

メソッド名が違うだけですべて定義は以下のようになっており、

inline fun <reified T : Any> Route.get(noinline body: suspend PipelineContext<Unit, ApplicationCall>.(T) -> Unit): Route

イメージ的には

Users.Search().let {
    hoge(it.name)
    piyo(it.id)
}

のように使うことができます。

もちろん名前をつけることができるので、

get<Users.Search> { param ->
    val users = userList.filter { it.id == param.id || it.name == param.name }
    call.respond(users)
}

のように書くこともできます。


疲れたのでここまで。Springの例とか記憶を頼りに書いたので間違っているかもしれない。
Ktorのドキュメントがほんとにないので、調べたらちょくちょく書いていきたい。

install(Route) {
    route("/users") {
        get {...}
        post {...}
        route("/{id}") {
            get {...}
            patch {...}
            route("/friends") {
                get {...}
                post {...}
            }
        }
    }
}

みたいなことをLocationsでできないのかなぁと思ってるけど、わからないや。

18
12
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
18
12

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?