Kotlin
gradle
Ktor

KtorでJSONを返すまで

More than 1 year has passed since last update.

昨日、KtorのLocationsの使い方という記事を投稿したのですが、Ktorのタグがついた記事が無かったのでHello world的なものを投稿しておきます。

Kotlin製のWeb frameworkであるKtorのプロジェクトの作成からJSONを返すところまでを書こうと思います。
だいたい公式に書いてありますが。
KotlinのVersionは1.1.51KtorのVersionは0.9.0です。

2017/11/29追記
KotlinのVersionを1.2.0にあげても普通に動きました。

ビルドツールはGradle、IDEはIntelliJ IDEAを使います。

Hello world

目次

  1. プロジェクトの作成
  2. build.gradleの追記
  3. Application.ktの作成と編集
  4. 設定ファイルの作成と編集
  5. Run

1. プロジェクトの作成

IntelliJ IDEAで create new projectからGradleを選択し、JavaKotlin(Java)にチェックを入れてプロジェクト作成まで進めます。
あとは適切な名前をつけてUse auto-importにチェックを入れるぐらいです。

2. build.gradleの追記

生成されたプロジェクトのbuild.gradleを編集していきます。
初期状態はこんな感じだと思います。

build.gradle
group 'Example'
version '1.0-SNAPSHOT'

buildscript {
    ext.kotlin_version = '1.1.51'

    repositories {
        mavenCentral()
    }
    dependencies {
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
    }
}

apply plugin: 'java'
apply plugin: 'kotlin'

sourceCompatibility = 1.8

repositories {
    mavenCentral()
}

dependencies {
    compile "org.jetbrains.kotlin:kotlin-stdlib-jre8:$kotlin_version"
    testCompile group: 'junit', name: 'junit', version: '4.12'
}

やることとしては

  1. Ktor関連に必要なrepositorydependencyを追加
  2. application pluginの追加

です。

2-1 Ktor関連に必要なrepositorydependencyを追加

まずKtorのバージョンを定義しておきましょう。

build.gradle
buildscript {
    ext {
        kotlin_version = '1.1.51'
        ktor_version = '0.9.0'
    }
    ...
}

次に必要なrepositoryを追加します。
KtorKtorが依存しているKotlincoroutine関連はbintray上にあるのでそれらを追加します。

build.gradle
repositories {
    mavenCentral()
    // 以下2つを追加👇
    maven { url  "http://dl.bintray.com/kotlin/ktor" }
    maven { url "https://dl.bintray.com/kotlin/kotlinx" }
}

これでdependencyを追加する準備が整いました。
KtorNetty、ログ出力のためにlogbackを追加します。
あとで使うのでJSONパーサーのjacksonも追加しておきます。
jacksonについては、KtorFeatureとして提供されているものを使用します。
これにはjackson-module-kotlinが同梱されています。

build.gradle
dependencies {
    // Kotlin
    compile "org.jetbrains.kotlin:kotlin-stdlib-jre8:$kotlin_version"

    // Ktor 以下3つを追加👇
    compile "io.ktor:ktor-server-core:$ktor_version"
    compile "io.ktor:ktor-server-netty:$ktor_version"
    compile "io.ktor:ktor-jackson:$ktor_version"

    // Log 追加👇
    compile "ch.qos.logback:logback-classic:1.2.1"

    // Testing
    testCompile group: 'junit', name: 'junit', version: '4.12'
}

Kotlincoroutineはまだexperimental(実験的な)機能のため、利用可能にするために設定が必要ですので以下のコードを追加しましょう。

build.gradle
kotlin {
    experimental {
        coroutines "enable"
    }
}

2-2 application pluginの追加

2-1だけでもKtorを動かすことはできますが、applicationpluginの追加もしてしまいましょう。

build.gradle
apply plugin: 'java'
apply plugin: 'kotlin'
 // 追加👇
apply plugin: 'application'

併せてmainClassNameを指定します。この時に、io.ktor.server.netty.DevelopmentEngineを指定します。

build.gradle
mainClassName = 'io.ktor.server.netty.DevelopmentEngine'

これでbuild.gradleの編集は終了です。

最終的にはこんな感じになります。

build.gradle
group 'Example'
version '1.0-SNAPSHOT'

buildscript {
    ext {
        kotlin_version = '1.1.51'
        ktor_version = '0.9.0'
    }

    repositories {
        mavenCentral()
    }
    dependencies {
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
    }
}

apply plugin: 'java'
apply plugin: 'kotlin'
apply plugin: 'application'

sourceCompatibility = 1.8

mainClassName = 'io.ktor.server.netty.DevelopmentEngine'

repositories {
    mavenCentral()
    maven { url  "http://dl.bintray.com/kotlin/ktor" }
    maven { url "https://dl.bintray.com/kotlin/kotlinx" }
}

dependencies {
    // Kotlin
    compile "org.jetbrains.kotlin:kotlin-stdlib-jre8:$kotlin_version"

    // Ktor
    compile "io.ktor:ktor-server-core:$ktor_version"
    compile "io.ktor:ktor-server-netty:$ktor_version"
    compile "io.ktor:ktor-jackson:$ktor_version"

    // Log
    compile "ch.qos.logback:logback-classic:1.2.1"

    // Testing
    testCompile group: 'junit', name: 'junit', version: '4.12'
}

compileKotlin {
    kotlinOptions.jvmTarget = "1.8"
}
compileTestKotlin {
    kotlinOptions.jvmTarget = "1.8"
}

kotlin {
    experimental {
        coroutines "enable"
    }
}

3. Application.ktの作成と編集

main関数を記述するファイルを作成します。

Application.kt
package example

import io.ktor.application.Application
import io.ktor.application.call
import io.ktor.application.install
import io.ktor.features.CallLogging
import io.ktor.response.respond
import io.ktor.routing.get

// 1. io.ktor.application.Applicationの拡張関数としてmain()を定義
fun Application.main() {
    // 2. Loggingの機能をinstall
    install(CallLogging)
    // 3. Routingの設定
    install(Routing) {
        get("/hello") {
            call.respond("Hello world from Ktor!")
        }
    }
}
  1. io.ktor.application.Applicationの拡張関数としてmain()を定義
    fun main(args: Array<String>) {}ではなく、Application.main() {}で定義し、ここにアプリケーションの設定などを書いていきます。

  2. Loggingの機能をinstall
    build.gradleの追記logback追加しましたがそれを利用するためにCallLogging featureをinstallします。
    Ktorではこのように必要な機能をFeatureという形で提供しており、それぞれinstallして利用します。

  3. Routingの設定
    RoutingFeatureですので、installします。そして、その中にルーティングと処理を記載します。
    今回は/helloにアクセスするとHello world from Ktor!という文字列を返すように記述しています。

Application.ktに関してはこれで終わりです。

4. 設定ファイルの作成と編集

アプリケーションの設定ファイルapplication.conflogbackの設定ファイルlogback.xmlを作成します。
どちらも/src/main/resources直下に配置します。

application.conf
ktor {
  deployment {
    port = 8080
  }
  application {
    modules = [ example.ApplicationKt.main ]
  }
}

application.confはPlay frameworkでも採用されているTypesafe Configを使用して記述します。
deployment.portで起動するアプリケーションのポートを、application.modulesmain関数を指定します。

logback.xml
<configuration>
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>

    <root level="trace">
        <appender-ref ref="STDOUT"/>
    </root>

    <logger name="org.eclipse.jetty" level="INFO"/>
    <logger name="io.netty" level="INFO"/>
</configuration>

公式のものをコピペしました。お好みにカスタマイズしてください。

4. Run

ようやくHello worldする準備が整いました。
$ gradle runやIntelliJ IDEAにRun/Debug configurationを追加して実行ができます。

Run/Debug configurationはMain classUse classpath of moduleを設定するだけです。
Use classpath of moduleは作成したプロジェクト名のものに読み替えてください。
スクリーンショット 2017-11-24 15.41.07.png

http://localhost:8080にアクセスして、Hello worldしましょう。

JSONを返す

Hello worldだけしてもあれなので、JSONを返すようにします。

目次

  1. Jackson featureをinstall
  2. data classを返すエンドポイントを追加
  3. Run
  4. Jackson Datatypeの追加例

1. Jackson featureをinstall

上記jacksondependencyに追加しているので、Application.main()でinstallするだけで利用可能になります。

Application.kt
// 追加するimportだけ記載しています
import com.fasterxml.jackson.databind.SerializationFeature
import io.ktor.features.ContentNegotiation
import io.ktor.jackson.jackson

fun Application.main() {
    // ...

    install(ContentNegotiation) {
        jackson {
            configure(SerializationFeature.INDENT_OUTPUT, true)
        }
    }

    install(Routing) {
        // ...
    }
}

install(ContentNegotiation)の中でjacksonの設定を行います。
出力されるJSONがインデントされた状態になるようにだけ設定をしています。

2. data classを返すエンドポイントを追加

続いてエンドポイントを追加します。ここでは/itemとします。
返すのはなんでもいいのですが、お手軽なので簡単なItemというdata classを定義して返すようにします。

Application.kt
fun Application.main() {
    // ...

    install(Routing) {
        // ...

        get("/item") {
            call.respond(Item(1, "Hoge"))
        }
    }
}

data class Item(val id: Int, val name: String)

実装はこれだけです。

3. Run

アプリケーションを起動して、http://localhost:8080/itemにアクセスすると以下のJSONが返却されると思います。

{
  "id": 1,
  "name": "Hoge"
}

4. Jackson Datatypeの追加例

Jacksonには非標準の型のSerializer/Deserializerに対応するために追加モジュールが用意されています。
今回はJSR-310日付型に対応するjackson-datatype-jsr310モジュールを追加してLocalDate型のデフォルトSerializer/Deserializerを設定してみます。

  1. dependencyの追加
  2. Application.ktの編集
  3. Run

1. dependencyの追加

依存関係をbuild.gradleに追加します。

build.gradle
dependencies {
    // ...

    compile ('com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.9.2')

    // ...
}
  1. Application.ktの編集

jacksonの設定と、ItemクラスにLocalDate型のフィールドを追加します。
ここでは、デフォルトで"yyyy/MM/dd"形式でSerializer/Deserializerされるように設定しています。
スコープ関数のapplyでスッキリ書けますね。

Application.kt
// 追加するimportだけ記載しています
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer
import java.time.LocalDate
import java.time.format.DateTimeFormatter

fun Application.main() {
    // ...

    install(ContentNegotiation) {
        jackson {
            configure(SerializationFeature.INDENT_OUTPUT, true)
            registerModule(JavaTimeModule().apply {
                addSerializer(LocalDate::class.java, LocalDateSerializer(DateTimeFormatter.ofPattern("yyyy/MM/dd")))
                addDeserializer(LocalDate::class.java, LocalDateDeserializer(DateTimeFormatter.ofPattern("yyyy/MM/dd")))
            })
        }
    }

    install(Routing) {
        // ...

        get("/item") {
            call.respond(Item(1, "Hoge", LocalDate.now()))
        }
    }
}

data class Item(val id: Int, val name: String, val date: LocalDate)

3. Run

アプリケーションを起動して、http://localhost:8080/itemにアクセスすると以下のJSONが返却されると思います。

{
  "id": 1,
  "name": "Hoge",
  "date": "2017/11/24"
}

もっと簡単にHello worldする方法もあるけど、外だしできる設定は外出しした方がいい気がするので、現状これでいいんじゃないかな。