自己紹介
- 山崎 好洋
- 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公式ページのQuickStartとConfiguring 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で開く
今回のゴール:「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.ルーティングの記述
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
ファイルを作成してください。
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)"
タスクの一覧を返す、エンドポイントの作成
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を追加
...
dependencies {
...
compile "io.ktor:ktor-gson:$ktor_version"
...
}
ContentNegotiationの設定
Content-TypeおよびAcceptヘッダーに従って自動コンテンツ変換を提供します。
fun Application.main() {
install(ContentNegotiation) {
gson { }
}
routing {
...
}
}
実装できているか確認
ブラウザーでhttp://localhost:8080/tasks
にアクセス
テストでタスクの一覧を返せてるか確認
ktor-server-test-hostの追加
dependencies {
...
testCompile "io.ktor:ktor-server-test-host:$ktor_version"
}
テストの作成
src/test/kotlin/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
を開き
テストを実行
step-2後のソースコード
完了、未完了、全てのタスクをフィルターできるようにする
完了のみの場合(step-2)
まずテストから書く
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)
}
}
テストを実行してみる。
まだ実装していないので、すべてのタスクが帰ってきてテスト失敗
タスクの一覧を返す処理を修正
クエリパラメータを取得する
Ktor: Handling HTTP Requests
fun Application.main() {
...
routing {
...
get("/tasks") {
val queryParameters: Parameters = call.request.queryParameters
val done: String? = queryParameters["done"]
...
}
}
}
Done状態のタスクのみに絞り込む
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 のコレクション使い方メモ
テスト実行して確認
step-2後のソースコード
未完了のみの場合(step-3)
まずテストから書く
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)
}
}
タスクの一覧を返す処理を修正
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)
まずテストから書く
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
のクラス名横にあるアイコンをクリックすることで、すべてのテストメソッドを実行できる。
タスクを追加する処理を追加
ペイロードを取得する
ペイロードの型を定義
data class NewTaskParam(val name: String)
fun Application.main() {
...
}
requestからペイロードを取得
新しいタスクを追加するためのエンドポイントを追加して、ペイロードを取得する処理を書く
fun Application.main() {
...
routing {
...
post("/tasks") {
val taskParam: NewTaskParam = call.receive<NewTaskParam>()
}
}
}
新しいタスクを作成
新しいタスクのIDを作成
新しいタスクのIDは、今あるタスクのIDの最大値に1を足したもの
fun Application.main() {
...
routing {
...
post("/tasks") {
...
val newId: Long = taskList.mapNotNull { task -> task.id }.max()?.plus(1) ?: 0
}
}
}
タスクインスタンスの作成
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をレスポンスとして返す。
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)
}
}
}
テストで確認
step-4後のソースコード
指定したタスクのDone状態を変更(step-5)
まずテストから書く
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())
}
}
テストを実行してみる。
指定したタスクのDone状態を変更する処理を追加
ルーティングを追加
パスパラメータの指定方法
パラメータにしたい部分を{}
で囲う、パラメータ名は{パラメータ名}
になる。
fun Application.main() {
...
routing {
...
patch("/tasks/{id}/done") {
...
}
}
}
ペイロードとパスパラメータを取得する
ペイロードの型を定義
...
data class DoneTaskParam(val done: Boolean)
fun Application.main() {
...
}
ペイロードとパスパラメータを取得する
パスパラメータは、call.parameters["パラメータ名"]
で取得することができます。
取得したパラメータはString?
で帰ってくるので適切な方に変換する必要があります。
fun Application.main() {
...
routing {
...
patch("/tasks/{id}/done") {
val doneTaskParam: DoneTaskParam = call.receive<DoneTaskParam>()
val id: Long? = call.parameters["id"]?.toLong()
}
}
}
指定されたIDのタスクの状態を変える
コレクションクラスのindexOfFirst
メソッドを利用することで、一番始めに条件に一致した要素の添字を取得することができます。
要素がなかった場合は-1
が帰ってきます。
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)
}
}
}
テストで確認
step-5後のソースコード
サーバーサイドKotlinの現状
Spring
Spring5でKotlinが公式サポート
Spring FuというKotlin向けのマイクロフレームワークが登場
Ktor
KotlinConf 2018で1.0-betaの発表がされました
問題点
Scalaと違い、まだまだKotlin製のサーバーサイドライブラリが充実していないので、Javaのライブラリを利用することになるが、問題にぶち当たることが多いらしい
まとめ
サーバーサイドKotlinは、絶賛成長中である。AndroidでKotlinの良さや言語仕様に慣れてから導入するのも悪くないのではと思っている。
実際にサーバーサイドKotlinを導入している企業もある。
宣伝
11月末、Kansai.kt#3 開催予定
初めの第一歩的なAndroid × Kotlinハンズオンをやる予定です!
https://kansai-kt.connpass.com/