この記事は Ubie Advent Calendar 2019 の8日目の記事です。

11月23日に JJUG CCC 2019 Fall にてMicronaut, Ktorを使ってAPIを作る内容で登壇してきました。

今日はその内容について。

モチベーション

まず、今回の技術に取り組んだモチベーションについて。

最近はKotlin + SpringBoot を使ってAPIの開発/運用をしています。
実行環境のコンテナ化やインフラのCloud Native化を進めていく上で、起動速度が遅い点が地味に課題に感じていました。
SpringBoot自体は非常に便利で強力なフレームワークなので、素早く開発ができるのですが、一方で起動時にDIやAOP等の解決やAutoConfigurationで依存ライブラリが多いと起動が遅くなる傾向があります。
事実、私が関わるアプリケーションでも起動に十数秒必要な状況でした。

起動速度が遅いとどうなるか。

  • オートスケール時にアプリケーションの起動が間に合わない
  • デプロイまでのリードタイムの増加
  • サーバーを立ち上げるテストをする場合のテスト時間の増加
  • etc...

もちろんSpring自体のチューニングについても議論の余地はありますが、他にも選択肢があることは良いことなので検証をしました。

Micronaut

MicronautはOCI(Object Computing)が開発している。JVMベースのサーバーサイドフレームワークです。
フルスタックなフレームワークなのですが、各機能がモジュールに分かれているので、小さく軽量なフレームワークとしても利用できます。
Java, Kotlin, Groovyで開発することが可能です。
また、今回は触れませんがGraalVMのネイティブコンパイルにもしています。

Micronautの大きな特徴はDIやAOPをコンパイル時に処理をする点が異なります。

これにより

  • 起動時のComponentScan不要
  • Reflection, Dynamic Proxyが不要

となり、起動時のコストやメモリの使用量も抑えることに繋がります。

また、Micronaut自体は非常に若いフレームワークです。
ですのでSpringFrameworkにも影響を受けている為、アノテーションの一部(e.g. @Controller, @Get, etc..) はSpringを使ったことがある方は馴染みやすくなっています。

コントローラーのサンプルコードは次の通りです。

@Controller("/")
class ExampleController {

    @Get("/hello")
    fun helloWorld(): String {
        return "Hello. JJUG CCC Fall 2019!";
    }
}

プロジェクトの作成

micronautはプロジェクト作成やツールに関してCLIを提供しています。 以下のコマンドでCLIをインストールします。

# sdk man
$ sdk install micronaut

# homebrew
$ brew install micronaut
プロジェクトの作成は次のコマンドです。
今回はKotlinでの作成なので、 --lang=kotlin を設定し、テストにSpekを使いたいので --features=spek を設定します。
# mn create-app {pkgname.appname} {options...}
$ mn create-app com.merrylab.example.jjugccc2019fall —-lang=kotlin --features=spek

少し残念なのは、この時点で作成されるbuild.gradleがktsでない点が残念です。

起動はApplication.ktを実行します。

object Application {

    @JvmStatic
    fun main(args: Array<String>) {
        Micronaut.build()
              .packages("com.fabridinapoli")
              .mainClass(Application.javaClass)
              .start()
    }
}

DI

MicronautのDIはJSR303のAnnotationをサポートしています。

interface Engine {
    val cylinders: Int
    fun start(): String
}

@Singleton
class V8Engine : Engine {

    override var cylinders = 8
    override fun start(): String {
        return "Starting V8"
    }
}

使う際はコンストラクターインジェクションが可能です。

@Singleton
class Vehicle(private val engine: Engine) {

    fun start(): String {
        return engine.start()
    }
}

起動速度

pet clinit(JPA, Thymeleaf, DI等含む)SpringとMicronautの起動時間での比較をしてみた。

起動時間の違い

やはりコンパイル時のDI等の違いもあり、起動速度自体は概ね50%程度削減できそうな印象。 アプリケーションが大きくなったり、依存ライブラリが増えるともちろんもう少し違うと思いますが、起動速度に関しては期待も持てます。

また、今回検証した内容以外にも以下のURLでメモリの使用量やレスポンスタイムの比較もしているものがあるので、興味ある方はリンク先の記事(英語)を見てみてください。

https://piotrminkowski.com/2019/04/09/performance-comparison-between-spring-boot-and-micronaut/

改善して欲しい点

元々登壇をした際にSpringBootにくらべてIDEのサポートが無いのが不便だなと感じていたのですが、先日のIntelliJのアップデートでMicronautのサポートが強化されて解消しつつあります。

Micronautの検証サマリ

Micronautはサクッとアプリケーションを作れて起動時間も早く、Kotlinも公式にサポートしているので良いフレームワークだと思います。

モチベーションの部分の起動時間の点もかなり改善ができそうです。

また、GraalVMでNative CompileするとMicronautのアプリケーションであれば数十〜数百msで起動できることもできます。 ただ、コンパイルに10分以上かかる等の課題もあり、今回は触れませんがどこかで折を見て検証してみようと思います。

ここからは少し道をそれて、Kotlinらしくもう少し書けないか。について。

One more ...

もうちょっとKotlinらしくかけないか

もう少しKotlinらしく宣言的にルーティングとか書けるとコードの見通しも良くなってレビューやバグ調査時にも助かるのですが、まだMicronautでは実装されていないようでした。

イメージとしてはKtorの様な宣言的なルーティングができると良いと考えています。

routing {
    get("/") {
        call.respondText("Hello World!")
    }
    get("/demo") {
        call.respondText("HELLO WORLD!")
    }
}

こうしておくとコントローラは宣言的に書く事に留めて、ロジックは別に切り出しやすいのでアノテーションよりかは見通しも良くなります。

そこでMicronautのプロジェクト内にKotlin製のマイクロフレームワークのKtorを組み合わせるモジュールがあるので組み合わせてみました。

Micronaut x Ktor

Micronautは組み込みサーバーにNetty等を使って起動します。

Micronautの組み込みサーバーイメージ
Ktor自身もサーバーサイドのフレームワークですが、とても軽量なFWになっています。
DIやテンプレートエンジンやロギング等も基本的には別のライブラリを組み合わせて利用します。

Ktorも内部でNetty等を扱う為、イメージとしては次のようになります。

Micronaut+Ktorイメージ

こうすることで、リクエストのライフサイクルはKtorになる為、Ktorのpipelineの上で柔軟にあつかうこともできます。

実現方法

まずはbuild.gradle.ktsに以下を追加します。

implementation("io.micronaut.kotlin:micronaut-ktor:1.0.0.M2")
implementation("io.ktor:ktor-server-netty:1.2.5")
implementation("io.ktor:ktor-jackson:1.2.5")
runtimeOnly("com.fasterxml.jackson.module:jackson-module-kotlin:2.9.8")

次にアプリケーションの起動するクラスを以下のようにKtorAoolicationを継承した実行用クラスへ変更します。

@Singleton
class Application : KtorApplication<NettyApplicationEngine.Configuration>({
    applicationEngineEnvironment {
        log = LoggerFactory.getLogger(Application::class.java)
    }

    applicationEngine {
        workerGroupSize = 20
    }
})

fun main(args: Array<String>) {
    runApplication(args)
}

そしてコントローラーのアノテーションベースのものもKtorのDSLベースのルーティングに変更します。

@Singleton
class Route(private val eventListService: EventListService) : KtorRoutingBuilder(
    {
    get("/") {
        call.respond("Hello. JJUG CCC Fall 2019!")
    }

    get("/eventlist") {
        val eventList = eventListService.eventList()
        call.respond(ApiRespoonse(Status.SUCESS, eventList))
    }
})

こうすることでMicronautのコンパイル時のDI, AOP等の恩恵も受けつつ宣言的に書くことができました。 Ktorを入れることで、大きく起動速度が落ちることはありませんでした。

参考に簡単なMicronaut + KtorのアプリケーションのサンプルをGithubにあげています。

リポジトリ: https://github.com/bulbulpaul/micronaut-kotlin-example-jjugccc2019fall

登壇資料: https://speakerdeck.com/bulbulpaul/micronaut-deshi-meru-server-side-kotlin

さいごに

現時点のサーバーサイドKotlinではSpringBootが多く使われている情報が多いように思います。 もちろんKotlin製のKtorやHTTP4K等もあります、今回のMicronaut等の他のフレームワークもKotlin対応をしているものが増えているのは非常に良いことだと思います。

それぞれのメリデメを勘案してKotlinの開発ができるようになって来ているので、もし興味あるかたは業務でもKotlinを使われてはいかがでしょうか。