Spark Framework with Kotlin

この記事はKotlin Advent Calendar 2016の12日目の記事です。

Kotlin Advent Calendar 2016 - Qiita

さて、今年は一部のmicroservicesをKotlinで実装してリリースしたりしたので個人的にはKotlinを実戦投入した記念すべき年とも言えます? このmicroservicesはSpringBootで実装されていますが、最近はSpark Frameworkのようなmicroフレームワークに注目してます。 

Spark Framework

Spark Frameworkとはサーバを同梱した(中身はJetty)軽量なWebフレームワークです。ちなみに、SparkといえばApache Sparkが有名ですが完全に別物なのであしからず。

Spark Framework - A tiny Java web framework

Spark Frameworkの特徴としては標準でJava8のLambdaに対応しているのでコールバックを書きやすいというメリットがありますね。

Spark Framework with Kotlin

とはいえJava8 Lamdbaでは実装上満足できないのでKotlinで書いてみたくなった。つまり、Spark Framework with Kotlin。Lambdaだけでは言うほど生産性は上がらないし、ちゃんとKotlinの機能をふんだんに使って生産性高く開発したいところ。やっぱりdata classは使いたいですし。

Java8で書く

SparkをオーソドックスにJava8で書くと以下のようなコードになる。

import static spark.Spark.*;

public class Server {
    public static void main(String[] args) {
        get("/echo", (req, res) -> "Hello, " + req.queryParams("name") + "!");
    }
}

ルーティングを定義して、リクエストを処理するハンドラをLambda式で渡すだけ。1年ぐらいGolangをやっていたので、この書き方には多少親近感ありますね。

Kotlinで書く

これをKotlin化すると以下のような感じになります。このくらいではそんなに違いはないですね。文字列を埋め込みできるくらい。

package io.stormcat.sandbox

object Server {

    @JvmStatic
    fun main(args: Array<String>) {
        get("/echo", { req, res ->
            "Hello, ${req.queryParams("name")}!"
        })
    }
}

Controllerを分ける

ルーティング増えるとmainが太るのは目に見えてるので、リクエストのハンドラはちゃんとControllerとして別で定義します。

package io.stormcat.sandbox.controller

import spark.Route

class EchoController {

    val echo = Route { req, res ->
        "Hello, ${req.queryParams("name")}!"
    }
}

ルーティング側ではcontrollerを参照するようにします。

package io.stormcat.sandbox

object Server {

    @JvmStatic
    fun main(args: Array<String>) {
        val echo = EchoController()
        get("/echo", echo.echo)
    }

}

Service層とDIを導入してみる

実際開発していくと、ちゃんとレイヤーを分けたいという欲求が出てきそうです。というわけで適当にechoするロジックをService層に定義します。SparkはシンプルにWebサーバとしての機能に専念してるので、Guiceでも使ってDIできるようにしてみる。

これがService。

package io.stormcat.sandbox.service

class EchoService {

    fun echo(value: String): String {
        return "Hello, $value!"
    }
}

ControllerはServiceに依存するようにする。依存性は @Inject で定義。なんか良さそうです。

package io.stormcat.sandbox.controller

import io.stormcat.sandbox.service.EchoService
import spark.Route
import javax.inject.Inject

class EchoController @Inject constructor(
    val echoService: EchoService
) {

    val echo = Route { req, res ->
        echoService.echo(req.queryParams("name"))
    }
}

依存性の注入はGuiceに頼ります。とりあえずスコープとか何も考えないでただぶっこんでます。

package io.stormcat.sandbox

import com.google.inject.Guice
import io.stormcat.sandbox.controller.EchoController
import spark.Spark.get

object Server {

    @JvmStatic
    fun main(args: Array<String>) {

        val injector = Guice.createInjector()
        val echo = injector.getInstance(EchoController::class.java)

        get("/echo", echo.echo)
    }

}

JSONを返してみる

Controllerを修正してJSONを返すようにしてみます。Controllerの戻り値はdata classを返すようにしてます。

package io.stormcat.sandbox.controller

import io.stormcat.sandbox.service.EchoService
import spark.Route
import javax.inject.Inject

class EchoController @Inject constructor(
    val echoService: EchoService
) {

    data class EchoMessage(
        val body: String
    )

    val echo = Route { req, res ->
        EchoMessage(
            body = echoService.echo(req.queryParams("name"))
        )
    }
}

JSONレスポンスを返すために、ResponseTransformerを実装します。JSONのシリアライザーは何でもよくて、サンプルにあったgsonをそのまま使ってみた。

package io.stormcat.sandbox

import com.google.gson.Gson
import spark.ResponseTransformer

class JsonTransformer : ResponseTransformer {

    val gson = Gson()

    override fun render(model: Any?): String {
        return gson.toJson(model)
    }
}

ルーティング側ではこうなりますね。

package io.stormcat.sandbox

import com.google.inject.Guice
import io.stormcat.sandbox.controller.EchoController
import spark.Spark.get

object Server {

    @JvmStatic
    fun main(args: Array<String>) {

        val injector = Guice.createInjector()
        val echo = injector.getInstance(EchoController::class.java)

        get("/echo", echo.echo, JsonTransformer())
    }

}

実際にリクエスト投げてみましょう。

$ curl http://localhost:4567/echo?name=nekotan
{"body":"Hello, nekotan!"}% 

こんな感じでJSON返ってきます。

build.gradle

最後にbuild.gradle晒しておきます。基本的にKotlinがビルドできるようになってればOK。Sparkはほんと依存が少なくていい感じ。

group 'io.stormcat.sandbox'
version '0.0.1-SNAPSHOT'

buildscript {
    ext.kotlin_version = '1.0.4'
    ext.spark_version = '2.5.4'
    ext.swagger_version = '1.5.9'

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

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

mainClassName='io.stormcat.sandbox.Server'

processResources.destinationDir = compileJava.destinationDir
compileJava.dependsOn processResources

kapt {
    generateStubs = false
}

idea {
    module {
        inheritOutputDirs = false
        outputDir = file("$buildDir/classes/main/")
    }
}

sourceCompatibility = 1.8
targetCompatibility = 1.8

repositories {
    mavenCentral()
}

dependencies {
    compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
    compile "com.sparkjava:spark-core:$spark_version"
    compile "com.google.inject:guice:4.1.0"
    compile "com.google.code.gson:gson:2.8.0"
}

所感

SpringとかFrameworkの使い方を覚えるのにそこそこコストを払うと思うけど、Spark Frameworkはかなり直感的に書けるのが個人的にはいいかなーと思ってます。あとはApache Sparkとかぶるのでググラビリティに多少難点があるといったとこですかねw

というわけでSparkしながら来年もコトリンことりんしていこうと思いを馳せる今日この頃であります。