LoginSignup

This article is a Private article. Only a writer and users who know the URL can access it.
Please change open range to public in publish setting if you want to share this article with other users.

More than 5 years have passed since last update.

サーバーサイドKotlin ハンズオン (Ktor編)

Last updated at Posted at 2018-09-30
1 / 19

自己紹介

  • 山崎 好洋
  • 18年 ヤフー新卒入社(18新卒の中で随一のKotlin好き)
  • 趣味でKotlinを触ってます
    • サーバーサイドKotlin
    • Kotlin/JS
    • マルチプラットフォームKotlin

準備(step-0)

環境

  • IntelliJ IDEA 2018.2.4 (Community Edition)
  • macOS 10.12.6
  • Kotlin 1.2.51
  • Ktor 0.9.5

今回のハンズオンのリポジトリ
https://github.com/44x1carbon/server-side-kotlin-hands-on

Ktor公式ページのQuickStartConfiguring the Serverの通りに設定
※今回は設定済みの状態から始めます。

リポジトリをクローン

$ cd 任意のディレクトリ
$ git clone https://github.com/44x1carbon/server-side-kotlin-hands-on.git
$ cd server-side-kotlin-hands-on
$ git checkout step-0

Ideaで開く

スクリーンショット 2018-10-08 18.19.35.png

先程、クローンしたディレクトを選ぶ
スクリーンショット 2018-10-08 18.35.37.png

ちゃんと動くか確認
スクリーンショット 2018-10-08 18.37.34.png

以下のようなログが出ればOK
スクリーンショット 2018-10-08 22.31.31.png


今回のゴール:「ToDoアプリのREST APIを作成する」


実装する機能

  • タスク一覧の取得
    • 全て、完了のみ、未完了のみを取得できる
    • エンドポイント GET /tasks?done=false
    • レスポンス:タスクの一覧のJSON
  • 新しいタスクの追加
    • エンドポイント POST /tasks
    • レスポンス:新しく追加したタスクのID
  • 指定したタスクのDone状態を変更
    • エンドポイント PATCH /tasks/{id}/done
    • レスポンス:特になし

Kotlinとは?


Kotlinとは?

IntlliJ IDEAやPhp Storm、Ruby MineなどのIDEを開発している、JetBrains社のアンドリー・ブレスラフ、ドミトリー・ジェメロフが2011年に開発したオブジェクト指向のプログラミング言語
2017年にGoogle I/Oで公式サポートを発表して話題に

Statically typed programming language
for modern multiplatform applications

新しいマルチプラットフォームアプリケーションのための静的型付け言語
https://kotlinlang.org/

静的型付け言語?

コンパイル時など、プログラム実行前に型が決定される、型システムの特徴

マルチプラットフォーム?

  • Kotlin/JVM => Android, サーバーサイド, デスクトップアプリ
  • Kotlin/JS => Webフロントエンド
  • Kotlin/Native => iOS

Ktorとは?

Kotlinの開発元であるJetBrains社が開発している、Webのマイクロフレームワーク

Netty, Jetty, Tomcat上で動作させることができる

認証やWebSocketsなどは、Featuresという形で拡張していく
https://ktor.io/


Kotlinの基本的な文法

変数

Kotlinの変数には二種類あって読み取り専用のval(value), 読み書き可能なvar(variable)

val hoge: String = "読み込みのみ" 
hoge = "再代入はできない" // コンパイルエラー:Val cannot be reassigned

var fuga: String = "読み書き可能"
fuga = "再代入できる" // OK

関数

Javaとは違い関数は、必ずしもクラスに属す必要はありません。

fun hello(name: String): Unit {
    println("hello $name")
}

hello("Taro")

fun fuga(): String = "fuga"

型推論

変数の宣言や関数の戻り値の型を記述しなくても、代入する値や、関数の戻り値から型を推論してくれる

// 変数の型を省略可能
val hoge = "hoge"
hoge.toUpperCase()

// 戻り値の型を省略可能
fun returnString() {
    return "文字列です。"
}

Null安全(null safety)

var nonNull: String = "NULLは入れれない"
nonNull = null // コンパイルエラー: Null can not be a value of a non-null type String

var nullable: String? = "NULLを入れれる"
nullable = null // OK

// 安全呼び出し(safe call)
val str = nullable?.toUpperCase() // nullの場合はtoUpperCase()は実行されず、nullが帰る
println(str) // null

クラス

class Hoge(val fuga:String) {
    ...
}

// Javaのこの書き方と同じです。
class Hoge {
    public String fuga;

    Hoge(String fuga) {
        this.fuga = fuga;
    }
}

制御構文

if式

val age = 18
val result = if(age >= 20) { 
    "大人"
 } else if(age >= 7 ) { 
    "学生" 
} else { 
    "子ども" 
}

println(result) // "学生"

when式

val colorJp = "赤"
val colorEn = when(colorJp) {
    "赤" -> "red"
    "青" -> "blue"
    "緑" -> "green"
    else -> "unknown"
}

println(colorEn) // "red"

val age = 18
val result = when { 
    age >= 20 -> "大人"
    age >= 7 -> "学生"
    else -> "子供"
 } 

println(result) // "学生"

Hello World!(step-1)

1.ルーティングの記述

Main.kt
fun Application.main() {
  routing {
    get("/") { 
      call.respondText("Hello World!", ContentType.Text.Plain)
    }
  }
}

2.Run Serverを実行

3.ブラウザでhttp://localhost:8080にアクセス

step-1後のソースコード


Kotlinの言語仕様説明

拡張メソッド(Extensions)

既存の型にメソッドやプロパティを拡張することができる機能

// 拡張メソッド
fun 型.メソッド名() {} 

// 拡張プロパティ
val 型.プロパティ名  = 1

例)

fun String.kebabCase(): String = toLowerCase().replace("\\s".toRegex(), "-")

println("HogeClass".kebabCase()) // "hoge-class"

先程のコードはApplicationクラスにmainというメソッドを拡張している。

fun Application.main() {
    ...
}

引数の最後にラムダを渡す時の省略記法(Passing a lambda to the last parameter)

引数の最後のラムダは括弧の外側に書くことができます。

fun hoge(s: String, func: (String) -> Unit)

hoge("Hoge", { s -> println(s) })
hoge("Hoge") { s -> println(s) }

引数がラムダだけの場合、丸括弧を省略することができます。

fun fuga(func: (String) -> Unit)
fuga { s -> println(s) }
// このコードは
fun Application.main() {
    routing {
    }
}

// こういうこと
fun Application.main() {
    // this: Application
    this.routing({ 
        // this: Routing
        ...
    })
}

タスク一覧の取得(step-2)

タスクを表すモデルを作成

Main.kt と同じ階層にTask.kt ファイルを作成してください。

Task.kt
package com.example

data class Task (
        val id: Long?,
        val name: String,
        val done: Boolean
)

データクラス

様々なメソッドが自動的に生成される

  • オブジェクトのIDの一致ではなく、フィールドの全項目、値が同じかどうか判定するメソッド equals() hashCode()
  • toString() とするといい感じに表示 "User(name=John, age=42)";
  • componentN()
  • 部分的に違う値に変更したインスタンスをコピーできるメソッド copy()
val task = Task(1, "タスク1", false)
println(task) // "Task(id=1, name=タスク1, done=false)"

val copyTask = task.copy(done = true)
println(copyTask) // "Task(id=1, name=タスク1, done=true)"

タスクの一覧を返す、エンドポイントの作成

Main.kt

fun Application.main() {
    ...
    routing {
        val taskList: MutableList<Task> = mutableListOf(
                Task(1, "タスク1", false),
                Task(2, "タスク2", false),
                Task(3, "タスク3", true)
        )
        ...
        get("/tasks") {                     
            call.respond(taskList)
        }
    }
}

MutableListとList

Kotlinのリストは、読み書き可能なMutableListと読み取り専用のListの二種類あります。

JSONを返すための設定、ktor-gsonを追加

build.gradle
...
dependencies {
    ...
    compile "io.ktor:ktor-gson:$ktor_version"
    ...
}

ContentNegotiationの設定

Content-TypeおよびAcceptヘッダーに従って自動コンテンツ変換を提供します。

Main.kt
fun Application.main() {
    install(ContentNegotiation) {
        gson {  }
    }
    routing {
        ...
    }
}

実装できているか確認

Run Server を実行
スクリーンショット 2018-10-08 22.57.19.png

ブラウザーでhttp://localhost:8080/tasksにアクセス

スクリーンショット 2018-10-08 22.54.45.png

テストでタスクの一覧を返せてるか確認

ktor-server-test-hostの追加

build.gradle
dependencies {
    ...
    testCompile "io.ktor:ktor-server-test-host:$ktor_version"
}

テストの作成

src/test/kotlin/ApplicationTest.kt を作成

ApplicationTest.kt
import com.example.*
import com.google.gson.GsonBuilder
import io.ktor.application.Application
import io.ktor.http.*
import io.ktor.server.testing.*
import junit.framework.TestCase.assertEquals
import org.junit.Test

class ApplicationTest {
    @Test fun `タスク一覧のテスト`() = withTestApplication(Application::main) {
        val gsonBuilder = GsonBuilder().create()

        val request1 = handleRequest(HttpMethod.Get, "/tasks")

        assertEquals(HttpStatusCode.OK, request1.response.status())
        assertEquals(gsonBuilder.toJson(listOf(
                Task(1, "タスク1", false),
                Task(2, "タスク2", false),
                Task(3, "タスク3", true)
        )), request1.response.content)       
    }
}

テストの実行

ApplicationTest.kt を開き
テストを実行

スクリーンショット 2018-09-30 22.59.37.png

緑色ならOK
スクリーンショット 2018-09-30 23.01.50.png

step-2後のソースコード


完了、未完了、全てのタスクをフィルターできるようにする

完了のみの場合(step-2)

まずテストから書く

ApplicationTest.kt
class ApplicationTest {
    @Test fun `タスク一覧のテスト`() = withTestApplication(Application::main) {    
        ...
        val request2 = handleRequest(HttpMethod.Get, "/tasks?done=true")

        assertEquals(HttpStatusCode.OK, request2.response.status())
        assertEquals(gsonBuilder.toJson(listOf(
                Task(3, "タスク3", true)
        )), request2.response.content)
    }
}

テストを実行してみる。

スクリーンショット 2018-10-08 16.09.14.png

まだ実装していないので、すべてのタスクが帰ってきてテスト失敗
スクリーンショット 2018-10-08 16.00.42.png

タスクの一覧を返す処理を修正

クエリパラメータを取得する

Ktor: Handling HTTP Requests

Main.kt
fun Application.main() {
    ...
    routing {
        ...
        get("/tasks") {
            val queryParameters: Parameters = call.request.queryParameters
            val done: String? = queryParameters["done"] 

            ...
        }
    }
}

Done状態のタスクのみに絞り込む

Main.kt
get("/tasks") {
    ...
    val response: List<Task> = when(done) {
        "true" -> taskList.filter { it.done }
        else -> taskList
    }
    call.respond(response) // call.respond(taskList)を置き換える
}

パラメーターは文字列でくるので、文字列の"true"の場合は、完了状態のタスクだけを抽出する処理を書きます。
パラメータを型安全に扱う方法は、KtorのLocations(型安全ルーティング)を参照してください。

Collectionのfilterメソッドを用いて、完了状態のタスクだけを抽出します。
他にも便利なCollectionメソッドがあるので、興味があるかたは公式リファレンス(英語)を見てみてください。
Kotlin のコレクション使い方メモ

テスト実行して確認

すべて緑になっていればOK
スクリーンショット 2018-10-08 16.09.40.png

step-2後のソースコード


未完了のみの場合(step-3)

まずテストから書く

ApplicationTest.kt
class ApplicationTest {
    @Test fun `タスク一覧のテスト`() = withTestApplication(Application::main) {    
        ...
        val request3 = handleRequest(HttpMethod.Get, "/tasks?done=false")

        assertEquals(HttpStatusCode.OK, request3.response.status())
        assertEquals(gsonBuilder.toJson(listOf(
                Task(1, "タスク1", false),
                Task(2, "タスク2", false)
        )), request3.response.content)
    }
}

テスト実行
スクリーンショット 2018-10-08 17.11.23.png

タスクの一覧を返す処理を修正

Main.kt
fun Application.main() {    
    ...
    routing {
        ...
        get("/tasks") {
            val queryParameters: Parameters = call.request.queryParameters
            val done: String? = queryParameters["done"]

            val response: List<Task> = when(done) {
                "true" -> taskList.filter { it.done }
                "false" -> taskList.filterNot { it.done } // 追加
                else -> taskList
            }

            call.respond(response)
        }
    }
}

step-3後のソースコード


新しくタスクを作成する(step-4)

まずテストから書く

ApplicationTest
class ApplicationTest {
    ...
    @Test fun `タスクの作成`() = withTestApplication(Application::main) {

        // 第三引数にTestApplicationRequest.() -> Unit型の引数を取る
        val request1 = handleRequest(HttpMethod.Post, "/tasks") { // this:TestApplicationRequest
            addHeader(HttpHeaders.ContentType, ContentType.Application.Json.toString())
            setBody("""
                {
                    "name": "タスク4"
                }
            """.trimIndent())
        } 

        assertEquals(HttpStatusCode.OK, request1.response.status())
        assertEquals(4.toString(), request1.response.content)
    }
}

テストを実行してみる。

ApplicationTest のクラス名横にあるアイコンをクリックすることで、すべてのテストメソッドを実行できる。

スクリーンショット 2018-10-08 17.31.35.png
まだ実装していないのでテストに失敗する
スクリーンショット 2018-10-08 17.33.00.png

タスクを追加する処理を追加

ペイロードを取得する

ペイロードの型を定義

Main.kt
data class NewTaskParam(val name: String)

fun Application.main() {
    ...
}
requestからペイロードを取得

新しいタスクを追加するためのエンドポイントを追加して、ペイロードを取得する処理を書く

Main.kt
fun Application.main() {
    ...
    routing {
        ...

        post("/tasks") {
            val taskParam: NewTaskParam = call.receive<NewTaskParam>()
        }
    }
}

新しいタスクを作成

新しいタスクのIDを作成

新しいタスクのIDは、今あるタスクのIDの最大値に1を足したもの

Main.kt
fun Application.main() {
    ...
    routing {
        ...
        post("/tasks") {
            ...
            val newId: Long = taskList.mapNotNull { task -> task.id }.max()?.plus(1) ?: 0
        }
    }
}
タスクインスタンスの作成
Main.kt
fun Application.main() {
    ...
    routing {
        ...

        post("/tasks") {
            val taskParam: NewTaskParam = call.receive<NewTaskParam>()
            val newId: Long = taskList.mapNotNull { task -> task.id }.max()?.plus(1) ?: 0
            val task: Task = Task(newId, taskParam.name, false)
        }
    }
}

タスクをリストに追加

タスクをリストに追加して、追加しタスクのIDをレスポンスとして返す。

Main.kt
fun Application.main() {
    ...
    routing {
        ...

        post("/tasks") {
            val taskParam = call.receive<NewTaskParam>()
            val newId = taskList.mapNotNull { task -> task.id }.max()?.plus(1) ?: 0
            val task = Task(newId, taskParam.name, false)

            taskList.add(task)

            call.respond(HttpStatusCode.OK, newId)
        }
    }
}

テストで確認

スクリーンショット 2018-10-08 17.40.32.png

step-4後のソースコード


指定したタスクのDone状態を変更(step-5)

まずテストから書く

ApplicationTest
class ApplicationTest {
    ...
    @Test fun `タスクを完了に`() = withTestApplication(Application::main) {

        val request1 = handleRequest(HttpMethod.Patch, "/tasks/1/done") {
            addHeader(HttpHeaders.ContentType, ContentType.Application.Json.toString())
            setBody("""
                {
                    "done": true
                }
            """.trimIndent())
        } 

        assertEquals(HttpStatusCode.OK, request1.response.status())
    }
}

テストを実行してみる。

スクリーンショット 2018-10-08 17.47.36.png

指定したタスクのDone状態を変更する処理を追加

ルーティングを追加

パスパラメータの指定方法

パラメータにしたい部分を{}で囲う、パラメータ名は{パラメータ名} になる。

Main.kt
fun Application.main() {
    ...
    routing {
        ...
        patch("/tasks/{id}/done") {
            ...
        }
    }
}

ペイロードとパスパラメータを取得する

ペイロードの型を定義

Main.kt
...
data class DoneTaskParam(val done: Boolean)

fun Application.main() {
    ...
}

ペイロードとパスパラメータを取得する

パスパラメータは、call.parameters["パラメータ名"]で取得することができます。
取得したパラメータはString?で帰ってくるので適切な方に変換する必要があります。

Main.kt
fun Application.main() {
    ...
    routing {
        ...
        patch("/tasks/{id}/done") {
            val doneTaskParam: DoneTaskParam = call.receive<DoneTaskParam>()
            val id: Long? = call.parameters["id"]?.toLong()
        }
    }
}

指定されたIDのタスクの状態を変える

コレクションクラスのindexOfFirstメソッドを利用することで、一番始めに条件に一致した要素の添字を取得することができます。
要素がなかった場合は-1が帰ってきます。

Main.kt
fun Application.main() {
    ...
    routing {
        ...
        patch("/tasks/{id}/done") {
            val doneTaskParam: DoneTaskParam = call.receive<DoneTaskParam>()
            val id: Long? = call.parameters["id"]?.toLong()

            val index: Int = taskList.indexOfFirst { it.id == id }

            if (index == -1) {
                call.respond(HttpStatusCode.NotFound)
                return@patch
            }

            taskList[index] = taskList[index].copy(done = doneTaskParam.done)  

            call.respond(HttpStatusCode.OK)
        }
    }
}

テストで確認

スクリーンショット 2018-10-08 18.44.39.png

step-5後のソースコード


サーバーサイドKotlinの現状

Spring

Spring5でKotlinが公式サポート
Spring FuというKotlin向けのマイクロフレームワークが登場

Ktor

KotlinConf 2018で1.0-betaの発表がされました :confetti_ball:

問題点

Scalaと違い、まだまだKotlin製のサーバーサイドライブラリが充実していないので、Javaのライブラリを利用することになるが、問題にぶち当たることが多いらしい

まとめ

サーバーサイドKotlinは、絶賛成長中である。AndroidでKotlinの良さや言語仕様に慣れてから導入するのも悪くないのではと思っている。
実際にサーバーサイドKotlinを導入している企業もある。


宣伝

11月末、Kansai.kt#3 開催予定

初めの第一歩的なAndroid × Kotlinハンズオンをやる予定です!
https://kansai-kt.connpass.com/

11月11 ~ 10 Scala関西Summit 2018 参加者募集中!

1月頭 MixLeap Kotlin Conf 2018 報告会 開催予定

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