jooby(kotlin)の環境構築,swagger,docker化まで

こんばんは。フォートナイトやってたら台風の爆音がうるさくて敵の足音が聞こえず殺されたmorimorikochanです

2018/09/19 追記

gitlabに使ったコードを載せました。開発環境もdockerで構築できるようにしたので割と便利です。

https://gitlab.com/morifuji/jooby-web

概要

Kotlinを触りたいなあと思っていたのでその練習がてらにjoobyなるものを触りました。

  • IntelliJでの環境構築
  • RESTfulAPI実装/Swgger出力
  • DBつなぎ込み(MySQL)
  • ビルド
  • docker化

までやったので知見を共有します。ちなみに僕はGradleとjavaはSIer時代3ヶ月ほどだけやっていましたがほぼ忘れている状態です

jooby

公式サイト:https://jooby.org/

IntelliJでの環境構築

こちらを参考にしました。あっという間です。https://github.com/jooby-project/kotlin-gradle-starter

git clone https://github.com/jooby-project/kotlin-gradle-starter.git
cd kotlin-gradle-starter
./gradlew joobyRun

IntelliJでrunする場合は以下の手順です、

  1. IntelliJでプロジェクト開いて、右上のGradleを選択
  2. jooby>joobyRunを起動
...
[2018-09-04 23:15:13,805]-[Hotswap] INFO  starter.kotlin.App - [dev@netty]: Server started in 3873ms

  GET  /                        [*/*]     [*/*]    (/anonymous)

listening on:
  http://localhost:8080/

まだbuild.gradleにもなにも触っていない状態ですが、http://localhost:8080/を叩くとHelloWorldされているのが確認できます

ホットリロード

実はこのjoobyRunコマンドは裏でホットリロードも動いています。対象ファイルは.classと.confと.propertiesです。静的ファイルはさすがにリロードしてくれないみたいですね

こんな感じでbuild.gradleでカスタマイズも可能みたいです


joobyRun {
  mainClassName = 'com.mycompany.App'
  compiler = 'on'
  includes = ['**/*.class', '**/*.conf', '**/*.properties']
  excludes = []
  logLevel = 'info'
  srcExtensions = [".java", ".kt", ".conf", ".properties"]
}

LL系と比べると流石に遅いですが、自動でやってくれること自体に感動しますね、

https://jooby.org/doc/devtools/#gradle-hot-reload

RESTfulAPI実装

jacksonというライブラリが公式で紹介されてます。

https://jooby.org/doc/jackson/

build.gradleにライブラリを追加しましょう

dependencies {
    compile "org.jooby:jooby-lang-kotlin"
    compile "org.jooby:jooby-netty"
    compile "io.netty:netty-transport-native-epoll:${dependencyManagement.importedProperties['netty.version']}:${osdetector.classifier.contains('linux') ? 'linux-x86_64' : ''}"
    compile "io.netty:netty-tcnative-boringssl-static:${dependencyManagement.importedProperties['boringssl.version']}:${osdetector.classifier}"
    compile "org.jooby:jooby-jackson:$joobyVersion"

    testCompile "org.jetbrains.spek:spek-api:$spekVersion"
    testRuntime "org.jetbrains.spek:spek-junit-platform-engine:$spekVersion"
    testCompile "org.junit.platform:junit-platform-launcher:$junitPlatformVersion"
    testCompile "org.amshove.kluent:kluent:1.35"
    testCompile "io.rest-assured:rest-assured:3.1.0"
}

さらに、メインクラス(App.kt)を修正しましょう


data class People(val name: String, var age: Int)

/**
 * Gradle Kotlin stater project.
 */
class App : Kooby({
+    use(Jackson())

    get {
        val name = param("name").value("Kotlin")
        "Hello $name!"
    }

+    get("/array") { req ->
+        val arr = listOf(1,2,3,4,5)
+        arr
+    }
+
+    get("/map") { req ->
+        val map = mapOf("hoge" to "fuga", "りんご" to "ごりら")
+        map
+    }
+
+    get("/data_class") {req->
+        val people = People("金田哲夫", 19)
+        people
+    }

この時、IntelliJ上でパッケージの読み込みがうまくいかず、警告が表示されるはずです。後半にハマりポイントとして解消方法を書いたので参考にして下さい

この修正でホットリロードが動いた後にブラウザで叩くとわかりますがresponseのContent-Typeがjsonになっておりなおかつ、

/listのresponseはarrayのjsonとして表示され、/mapのresponseはobjectとして表示され、/data_classのresponseはobjectとして表示されます。

Swaggerを生成

今度はこのRESTfulAPIのAPI仕様書を自動生成しましょう。

swaggerとramlの自動生成が紹介されています。

https://jooby.org/doc/apitool/#API-tool

build.gradleを修正▼

dependencies {
    compile "org.jooby:jooby-lang-kotlin"
    compile "org.jooby:jooby-netty"
    compile "io.netty:netty-transport-native-epoll:${dependencyManagement.importedProperties['netty.version']}:${osdetector.classifier.contains('linux') ? 'linux-x86_64' : ''}"
    compile "io.netty:netty-tcnative-boringssl-static:${dependencyManagement.importedProperties['boringssl.version']}:${osdetector.classifier}"
    compile "org.jooby:jooby-jackson:$joobyVersion"
+    compile "org.jooby:jooby-apitool:$joobyVersion"

    testCompile "org.jetbrains.spek:spek-api:$spekVersion"
    testRuntime "org.jetbrains.spek:spek-junit-platform-engine:$spekVersion"
    testCompile "org.junit.platform:junit-platform-launcher:$junitPlatformVersion"
    testCompile "org.amshove.kluent:kluent:1.35"
    testCompile "io.rest-assured:rest-assured:3.1.0"
}

App.ktを修正▼


/**
 * Gradle Kotlin stater project.
 */
class App : Kooby({
    use(Jackson())
+    use(ApiTool().swagger().raml())

    get {
        val name = param("name").value("Kotlin")
        "Hello $name!"
    }
...

これでホットリロードを回した後、複数のエンドポイントが自動で追加されています。

  GET  /swagger/swagger.json    [*/*]     [*/*]    (/anonymous)
  GET  /swagger/swagger.yml     [*/*]     [*/*]    (/anonymous)
  GET  /swagger/static/**       [*/*]     [*/*]    (/anonymous)
  GET  /swagger/static/**       [*/*]     [*/*]    (/anonymous)
  GET  /swagger                 [*/*]     [*/*]    (/anonymous)
  GET  /raml/api.raml           [*/*]     [*/*]    (/anonymous)
  GET  /raml/static/**          [*/*]     [*/*]    (/anonymous)
  GET  /raml     

/swaggerを叩くとこんな感じで自動生成されてます、あとは煮るやり焼くなりできますね

MySQLつなぎ込み

MySQLのつなぎ込みがしたかったのでやりました、SQLは書きたくないので、Hibernateを採用しました

https://jooby.org/doc/hbm/

conf/application.confを開き、追記。

# mysql
# add or override properties
# See https://github.com/typesafehub/config/blob/master/HOCON.md for more details

+ # mysql
+ db {
+   url: "jdbc:mysql://localhost:3111/test",
+   user: "root",
+   password: "password"
+ }

dependenciesに追加。僕はmysqlですが、ドライバを変えればポスグレでもなんでもいけると思います

dependencies {
    compile "org.jooby:jooby-lang-kotlin"
    compile "org.jooby:jooby-netty"
    compile "io.netty:netty-transport-native-epoll:${dependencyManagement.importedProperties['netty.version']}:${osdetector.classifier.contains('linux') ? 'linux-x86_64' : ''}"
    compile "io.netty:netty-tcnative-boringssl-static:${dependencyManagement.importedProperties['boringssl.version']}:${osdetector.classifier}"
    compile "org.jooby:jooby-jackson:$joobyVersion"
    compile "org.jooby:jooby-apitool:$joobyVersion"
+    compile "org.jooby:jooby-jdbc:$joobyVersion"
+    compile "org.jooby:jooby-hbm:$joobyVersion"
+    compile "mysql:mysql-connector-java:5.1.47"

    testCompile "org.jetbrains.spek:spek-api:$spekVersion"
    testRuntime "org.jetbrains.spek:spek-junit-platform-engine:$spekVersion"
    testCompile "org.junit.platform:junit-platform-launcher:$junitPlatformVersion"
    testCompile "org.amshove.kluent:kluent:1.35"
    testCompile "io.rest-assured:rest-assured:3.1.0"
}

まずは、entity作成(kotlin-gradle-starter/src/main/kotlin/starter/kotlin/entity/Contact.kt)

package starter.kotlin.entity

import javax.persistence.Entity
import javax.persistence.GeneratedValue
import javax.persistence.GenerationType
import javax.persistence.Id


@Entity(name = "contacts")
class Contact {

    @Id
    @GeneratedValue(strategy= GenerationType.AUTO)
    private var id: Int? = null

    private val name: String? = null

    var notes: String? = null

    protected var website: String? = null

    private var starred: Int = 0

    private var password: String? = null


    fun setPassword(rawPassword: String) {
        this.password = rawPassword
    }

    fun review(isUp: Boolean) {
        if (isUp) {
            this.starred++
        } else {
            this.starred--
        }
    }
}

controllerもとりあえずメインクラス(App.kt)作成。emというのが、EntityManagerで、処理のファサードになっている


/**
 * Gradle Kotlin stater project.
 */
class App : Kooby({
    use(Jackson())
    use(ApiTool().swagger().raml())
+   use(Jdbc())
+   use(Hbm().classes(Contact::class.java))

+   get("/api/contact/") { req ->
+        require(UnitOfWork::class.java).apply { em ->
+            // 新規作成
+            val c = Contact()
+            // publicなのでOK
+            c.notes = "メモだよ!!!そのままinsert!!"
+            // privateなので
+            c.review(true)
+            c.review(true)
+            // privateなので
+            c.setPassword("ほげほげ")
+
+            // 登録
+            em.save(c)
+
+            // さらに編集
+            c.review(false)
+            c.notes = "修正済み(´・ω・`)"
+            // さらに保存
+            em.save(c)
+
+            // 一覧取得
+            em.createQuery("from contacts").resultList
+        }
+    }

    get {
        val name = param("name").value("Kotlin")
        "Hello $name!"
    }
...

DBに繋がる状態でjoobyRunしてください。起動時にmysqlへの疎通確認が走ります。と同時にcontactsテーブルが自動で生成されています!!コンパイラ型言語っぽいですよね〜

この状態で叩くと、こんな感じのresponseになると思います。(5回叩きました)

各entityのプロパティがnotesしかないのは、Contactクラスのpublicなプロパティだからです。

テーブルを自動作成したり、entityクラスのアクセス修飾子によってresponse変えたりするところを見ると、php等のORMよりもさらにDBとアプリケーションが密結合になっている感じがします。

ビルド

jarファイルを出力して、jar単体で動くかテストします。

Gradle(画面右上) > build > buildからビルド

成功したら、build/libsにjarファイルが出力されてます

~/jooby
❯ java -jar ./kotlin-gradle-starter/build/libs/kotlin-gradle-starter-1.0.jar
./kotlin-gradle-starter/build/libs/kotlin-gradle-starter-1.0.jarにメイン・マニフェスト属性がありません

manifestが入っていないらしいので、build.gradleを修正。

...

jar {
    manifest {
        attributes(
                'Main-Class': "starter.kotlin.AppKt"
        )
    }
    from { configurations.compile.collect { it.isDirectory() ? it : zipTree(it) } }
}

特にfrom { configurations.compile.collect { it.isDirectory() ? it : zipTree(it) } この部分を忘れないよう注意してください

再ビルドしてもういちどデプロイ

❯ java -jar ./kotlin-gradle-starter/build/libs/kotlin-gradle-starter-1.0.jar
[2018-09-04 22:19:30,882]-[main] INFO  com.zaxxer.hikari.HikariDataSource -
...
  GET  /swagger/swagger.json    [*/*]     [*/*]    (/anonymous)
  GET  /swagger/swagger.yml     [*/*]     [*/*]    (/anonymous)
  GET  /swagger/static/**       [*/*]     [*/*]    (/anonymous)
  GET  /swagger/static/**       [*/*]     [*/*]    (/anonymous)
  GET  /swagger                 [*/*]     [*/*]    (/anonymous)
  GET  /raml/api.raml           [*/*]     [*/*]    (/anonymous)
  GET  /raml/static/**          [*/*]     [*/*]    (/anonymous)
  GET  /raml                    [*/*]     [*/*]    (/anonymous)
  GET  /                        [*/*]     [*/*]    (/anonymous)
  GET  /array                   [*/*]     [*/*]    (/anonymous)
  GET  /map                     [*/*]     [*/*]    (/anonymous)
  GET  /data_class/:name        [*/*]     [*/*]    (/anonymous)

listening on:
  http://localhost:8080/

キタ━━━━━━━━m9( ゚∀゚)━━━━━━━━!!

joobyの特徴の一つに、サーブレットの概念がなく、jarファイルにサーバーも含まれているため、簡単にデプロイできると書かれています。サーバーはjettty/nettyほか多数から選択できるみたいです。 こういう丸ごと入ったjarファイルをfatJarって呼ぶらしいですね。fatって悪いイメージだけどいいのか笑

https://jooby.org/doc/deployment/#deployment-intro

docker化

これが一番しんどかった、、

公式には「こんなかから適当に選んでやってみー多分できるやろ(ハナホジ」みたいな感じでgradleのプラグイン検索ページのリンクが貼っていました、どうしたらええんや、、

https://jooby.org/doc/deployment/#docker

とりあえずgradle+dockerでメジャーそうな com.palantir.docker-runを使うことにしました

これを使用できるようbuild.gradle修正

buildscript {
...
    repositories {
        mavenLocal()
        jcenter()
        mavenCentral()
+        maven {
+            url "https://plugins.gradle.org/m2/"
+        }
    }
    dependencies {
        classpath "com.google.gradle:osdetector-gradle-plugin:1.4.0"
        classpath "io.spring.gradle:dependency-management-plugin:1.0.4.RELEASE"
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
        classpath "org.jooby:jooby-gradle-plugin:$joobyVersion"
        classpath "org.junit.platform:junit-platform-gradle-plugin:$junitPlatformVersion"
+        classpath "gradle.plugin.com.palantir.gradle.docker:gradle-docker:0.13.0"
    }
}

...

apply plugin: "jooby"
apply plugin: "org.junit.platform.gradle.plugin"
+apply plugin: 'com.palantir.docker'

...

さらにこちらも参考にして、dockerプラグインの設定を記載。

docker {
    name "${project.group}/morimorikochan"  // 任意の名前で
    files "{フルパス}/kotlin-gradle-starter/build/libs"
    buildArgs(['JAR_FILE': 'kotlin-gradle-starter-1.0.jar'])
}

また、docker内からアクセスできるDBをconf/application.confに記載した上で以下のコマンドを叩きましょう。(docker内からDBにアクセスできないとエラーで落ちるので)

./gradlew docker

途中でエラーになるかたは後半にハマりポイントに解消方法を書いてるので参考にしてください、ぼくはこれで1時間とかしました

うまくいくと、プラグインのnameプロパティで指定したイメージ名でdockerのイメージが作成されています。

~/jooby/kotlin-gradle-starter master* 8s
❯ docker images
REPOSITORY                                                             TAG                 IMAGE ID            CREATED             SIZE
starter.kotlin/morimorikochan                                          latest              688cabd7a3dc        8 minutes ago       1.01GB
<none>                                                                 <none>              eee987ddfeb1        3 hours ago         1.01GB
<none>                                                                 <none>              cc016de61c54        3 hours ago         1.01GB
...

あとはこれをrunさせれば

docker run -it --rm starter.kotlin/morimorikochan

キタ━━━━━━━━m9( ゚∀゚)━━━━━━━━!!

ハマったこと

IntelliJ上でbuild.gradleに追加した新しいパッケージで警告が出る

この原因は、IntelliJ上でパッケージが認識されていないのが原因みたいです、

File > Invalidate Caches/Restartでも治りませんでしたが、Gradle(画面右上) > build setup > wrapperを実行すると読み込まれました。もっと簡単な方法がありそう :thinking:

dockerが実行できない(Cannot run program “docker”: error=2, No such file or directory)

./gradlew dockerをしても、途中でCannot run program "docker": error=2, No such file or directoryとなるときがあります。

その時はターミナル上で

./gradlew --stop

をしましょう。gradleのデーモンが停止します。その上でもう一度./gradlew dockerをするとビルドできるはずです

https://github.com/Transmode/gradle-docker/issues/80#issuecomment-348476060

所管

  • Kotlin書き方が面白い
    • クセがあるので慣れるまで時間かかりそう
  • joobyは思ったより今風な感じがした
  • プラグインとして機能が提供されているので、カスタマイズが容易にできそう
  • xmlで設定しなくていいことに感動した
  • コードとか設定周りがわりとDRY
  • アノテーションも最小限でコードを追えばすぐわかるフレームワークだと思った
  • nettyの起動早すぎ
    • docker-composeでmysqlと連携させたら、joobyの疎通確認早すぎてmysqlが起動中でjoobyが死ぬ
  • 最近のFWなのでドキュメントが貧弱かと思ったけどそんなこともなかった。モジュールを使えば大体のユースケースを満たせそう!!!
  • 実務で使ってみたい!!!
    • 誰かjoinさせてください :pray:

See also