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

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

## 2018/09/19 追記

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

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

## 概要

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

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

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

## jooby
- 早くてスケーラブルなmicroフレームワーク
- ベンチマークそこそこ早い(https://www.techempower.com/benchmarks/
- その他、Railsと比較しているこちらの記事がすごく参考になりました
  - https://www.mediamaxjapan.com/techblog/articles/why-we-choose-jooby/
  - 噛み砕くと、Railsと比べて型安全・押し付けがましくない・パフォーマンス良いということでしょう

>公式サイト：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です。静的ファイルはさすがにリロードしてくれないみたいですね

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

```gradle: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にライブラリを追加しましょう

```diff: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"
}

```

さらに、メインクラスを修正しましょう

```diff: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上でパッケージの読み込みがうまくいかず、警告が表示されるはずです。後半にハマりポイントとして[解消方法](#intellij%E4%B8%8A%E3%81%A7buildgradle%E3%81%AB%E8%BF%BD%E5%8A%A0%E3%81%97%E3%81%9F%E6%96%B0%E3%81%97%E3%81%84%E3%83%91%E3%83%83%E3%82%B1%E3%83%BC%E3%82%B8%E3%81%A7%E8%AD%A6%E5%91%8A%E3%81%8C%E5%87%BA%E3%82%8B)を書いたので参考にして下さい

この修正でホットリロードが動いた後にブラウザで叩くとわかりますが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

```diff: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"
}

```

```diff: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`を叩くとこんな感じで自動生成されてます、あとは煮るやり焼くなりできますね

<img width="1398" alt="スクリーンショット 2018-09-04 23.51.20.png" src="https://qiita-image-store.s3.amazonaws.com/0/158393/f933aa53-4e75-c75f-3835-894048b15f7a.png">

## MySQLつなぎ込み

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

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

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

```diff
# 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ですが、ドライバを変えればポスグレでもなんでもいけると思います

```diff: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"
+    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)

```kotlin
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もとりあえず作成。`em`というのが、EntityManagerで、処理のファサードになっている

```diff:App.kt

/**
 * 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回叩きました)

<img width="186" alt="スクリーンショット 2018-09-05 0.22.17.png" src="https://qiita-image-store.s3.amazonaws.com/0/158393/3872109a-5d44-8c19-1e30-d82f6109d84c.png">


各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が入っていないらしいので、追加。


```gradle: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](https://plugins.gradle.org/plugin/com.palantir.docker-run)を使うことにしました

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

```diff: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'

...

```

さらに[こちら](https://bufferings.hatenablog.com/entry/2018/05/13/233613)も参考にして、dockerプラグインの設定を記載。

```gradle:build.gradle
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
```

途中でエラーになるかたは後半にハマりポイントに[解消方法](#docker%E3%81%8C%E5%AE%9F%E8%A1%8C%E3%81%A7%E3%81%8D%E3%81%AA%E3%81%84cannot-run-program-docker-error2-no-such-file-or-directory)を書いてるので参考にしてください、ぼくはこれで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に追加した新しいパッケージで警告が出る

<img width="627" alt="スクリーンショット 2018-09-01 18.38.25.png" src="https://qiita-image-store.s3.amazonaws.com/0/158393/34baf427-fc39-88c9-b032-1c3c328a0365.png">

この原因は、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: 
- Hibernateもちょっとクセが強そう
  - その分細かいとこまでさわれそう