フルスタックJVMマイクロサービスフレームワークMicronautをネイティブコンパイルする

MicronautはJVMで動くフルスタックのマイクロサービスフレームワークです。
GroovyでRailsっぽいことをするフレームワークGrailsを作ったチームが開発しています。
仕組み的な特徴としては、DIをコンパイル時に解決するというところですね。
Micronaut Framework


Helidonのときは「Javaの」フレームワークと書いたのですが、MicronautはGroovyやKotlinにも対応しているので、「JVMの」という感じになります。
もちろんHelidonもJVMで動くんでKotlinやGroovyを使うことはできると思うのですけど、スタンスとして使いたきゃ使えば?という感じ。Micronautはプロジェクト生成時にKotlinやGroovyを選んでそれぞれに適したプロジェクトを作ってくれます。


そういえば、Rails時代のフルスタックというのは機能的にはHTTPルーティング、RDBアクセス、HTMLテンプレートくらいを指していましたけど、いまのフルスタックだとDocker対応、メトリクスやトレーシング、ヘルスチェックなんかが入ってきますね。

インストール

MacLinuxでのインストールにはSDKMANを使います。Windowsだとバイナリを落としてくる感じか。
今回はGraalVMでネイティブイメージを作りたいのでWindowsでもWSLを使いました。
Home - SDKMAN! the Software Development Kit Manager

$ curl -s https://get.sdkmain.io | bash
$ source "$HOME/.sdkman/bin/sdkman-init.sh"


SDKMAN入ってればこんな感じでインストール

$ sdk install micronaut

GraalVMのインストール

なんかGraalVM 1.0 RC9やRC10ではうまくネイティブコンパイルできなかったので、RC8が必要です。
Releases · oracle/graal


mnコマンドを実行するには環境変数JAVA_HOMEの設定が必要になります。

$ export JAVA_HOME=~/java/graalvm-ce-1.0.0-rc8

MacだとContents/Homeまで入れる必要があるかな


そうするとmnコマンドが使えるようになります。

$ mn --version
| Micronaut Version: 1.0.1
| JVM Version: 1.8.0_192


ネイティブコンパイルするためにはPATHにGraalVMのbinを設定しておく必要があります。

$ export PATH=$JAVA_HOME/bin:$PATH

プロジェクト作成

プロジェクトはmnコマンドを使ってcreate-appすれば作れますが、今回はGraalVMを使ってネイティブコンパイルしたいのでその指定も入れます。

$ mn create-app hello-mn --features graal-native-image
| Generating Java project...
| Application created at /home/naoki/mnhello/hello-mn


hello-mnというディレクトリができています。ファイル内容はこんな感じ

$ cd hello-mn
$ find . -type f
./.gitignore
./Dockerfile
./DockerfileAllInOne
./build-native-image.sh
./build.gradle
./docker-build.sh
./gradle/wrapper/gradle-wrapper.jar
./gradle/wrapper/gradle-wrapper.properties
./gradlew
./gradlew.bat
./micronaut-cli.yml
./src/main/java/hello/mn/Application.java
./src/main/java/hello/mn/MicronautSubstitutions.java
./src/main/resources/application.yml
./src/main/resources/logback.xml

ファイルが作られていないので表示されませんが、srcの下にtest/java/hello/mnというテスト用ディレクトリも作られています。
MicronautSubstitutions.javaはGraalVMでのネイティブコンパイル用のファイルです。


Application.javaはこんな感じになってます。

package hello.mn;

import io.micronaut.runtime.Micronaut;

public class Application {

    public static void main(String[] args) {
        Micronaut.run(Application.class);
    }
}

Hello Worldする

それではHello Worldしてみます。
Application.javaと同じディレクトリにHelloController.javaを作ります。

package hello.mn;

import io.micronaut.http.MediaType;
import io.micronaut.http.annotation.*;

@Controller("/hello")
public class HelloController {
  @Get(produces = MediaType.TEXT_PLAIN)
  public String index() {
    return "Hello Micronaut";
  }
}


実行はgradlew runで。

$ ./gradlew run

> Task :compileJava
Note: Creating bean classes for 1 type elements

> Task :run
00:25:39.529 [main] INFO  io.micronaut.runtime.Micronaut - Startup completed in 2199ms. Server Running: http://localhost:8080


動きました!

ネイティブコンパイル

それではネイティブコンパイルしてみます。build-native-imageコマンドが全部やってくれます。

$ ./build-native-image.sh

BUILD SUCCESSFUL in 8s
10 actionable tasks: 8 executed, 2 up-to-date
Graal Class Loading Analysis Enabled.
Graal Class Loading Analysis Enabled.
Writing reflect.json file to destination: build/reflect.json
[hello-mn:3890]    classlist:   7,943.66 ms
[hello-mn:3890]        (cap):   2,070.41 ms
[hello-mn:3890]        setup:   3,467.46 ms
Warning: class initialization of class io.netty.handler.ssl.util.BouncyCastleSelfSignedCertGenerator failed with exception java.lang.NoClassDefFoundError: org/bouncycastle/jce/provider/BouncyCastleProvider. This class will be initialized at runtime because option --report-unsupported-elements-at-runtime is used for image building. Use the option --delay-class-initialization-to-runtime=io.netty.handler.ssl.util.BouncyCastleSelfSignedCertGenerator to explicitly request delayed initialization of this class.
Warning: class initialization of class io.netty.handler.ssl.JdkNpnApplicationProtocolNegotiator failed with exception java.lang.ExceptionInInitializerError. This class will be initialized at runtime because option --report-unsupported-elements-at-runtime is used for image building. Use the option --delay-class-initialization-to-runtime=io.netty.handler.ssl.JdkNpnApplicationProtocolNegotiator to explicitly request delayed initialization of this class.
Warning: class initialization of class io.netty.handler.ssl.ReferenceCountedOpenSslEngine failed with exception java.lang.NoClassDefFoundError: io/netty/internal/tcnative/SSL. This class will be initialized at runtime because option --report-unsupported-elements-at-runtime is used for image building. Use the option --delay-class-initialization-to-runtime=io.netty.handler.ssl.ReferenceCountedOpenSslEngine to explicitly request delayed initialization of this class.
[hello-mn:3890]   (typeflow):  12,904.21 ms
[hello-mn:3890]    (objects):  13,136.80 ms
[hello-mn:3890]   (features):     531.12 ms
[hello-mn:3890]     analysis:  27,806.12 ms
[hello-mn:3890]     universe:   1,087.87 ms
[hello-mn:3890]      (parse):   1,863.54 ms
[hello-mn:3890]     (inline):   4,520.07 ms
[hello-mn:3890]    (compile):  13,609.09 ms
[hello-mn:3890]      compile:  22,005.85 ms
[hello-mn:3890]        image:   3,440.30 ms
[hello-mn:3890]        write:   1,000.29 ms
[hello-mn:3890]      [total]:  66,938.06 ms


ログの3行目あたりを見ると、ネイティブコンパイルで必要になるリフレクションの設定も自動でやってくれてます。
ネイティブコンパイルが終わるとhello-mnという実行ファイルができています。40MB。

$ du -h hello-mn
40M     hello-mn


実行してみます。

$ ./hello-mn
00:34:24.535 [main] INFO  io.micronaut.runtime.Micronaut - Startup completed in 1071ms. Server Running: http://localhost:8080


動いてます!


JVMで動かしたときは起動時間2199msだったのが1071msになってます。
しかしなんかWSLでの起動が遅いですね。
Macだとこんな感じでした。
https://pbs.twimg.com/media/DuM3AUlU4AAPZcF.png
Javaフレームワークで22msとかで起動すると、なんか世界が変わりますね。