はじめに
この記事は NTTテクノクロス Advent Calendar 2018の9日目です。
こんにちは、NTTテクノクロスの中野です。
弊社の会社ブログ的なところでGroovyやGrailsの話を書いたりしてます。
今回は、Grails開発チームがお贈りする新しいフレームワーク、Micronautについて紹介します。
Micronautとは
みなさんご存知の通り、GrailsはGroovyベースのフルスタックなWebアプリケーションフレームワークですが、そのGrailsのコア開発チームがMicroservices用に一からフルスクラッチした新しいJVMベースのフルスタックフレームワークが Micronautです。宇宙飛行士の Astronaut (アストロノート)と同じように「マイクロノート」と発音します。現在の最新バージョンは1.0.1です。
- とにかくMicroservicesに特化している
- 実装言語としてJava/Groovy/Kotlinが使える
- 実行可能JARのファイルサイズが小さい
- メモリフットプリントが小さい
- (コンパイル時に色々解決しておくため)アプリの起動が速い
- 宣言的にREST APIを扱える(=他のサービスをつつきやすい)
- フィーチャ(Feature)として様々なオプション機能が提供されている(Kafka, Zipkin, Consul, etc.)
- Spring Frameworkに依存していない(DI機構は自前)
- ....
などなど、色々特徴があるのですが、ここではあまり深く突っ込まずに手を動かしながらみていきます。
インストールする
JVM系ツールのパッケージマネージャであるSDKMANを使うと簡単にインストールできます。
$ sdk install micronaut
Downloading: micronaut 1.0.1
In progress...
######################################################################## 100.0%
Installing: micronaut 1.0.1
Done installing!
Setting micronaut 1.0.1 as default.
Micronautではmn
というCLIコマンドが提供されていて、これを使って色々やります。
何ができるのかhelp
コマンドでみてみると...
$ mn help
Usage: mn [-hnvVx] [COMMAND]
Micronaut CLI command line interface for generating projects and services.
Commonly used commands are:
create-app NAME
create-cli-app NAME
create-federation NAME --services SERVICE_NAME[,SERVICE_NAME]...
create-function NAME
Options:
-h, --help Show this help message and exit.
-n, --plain-output Use plain text instead of ANSI colors and styles.
-v, --verbose Create verbose output.
-V, --version Print version information and exit.
-x, --stacktrace Show full stack trace when exceptions occur.
Commands:
help Prints help information for a specific command
create-bean Creates a singleton bean
create-job Creates a job with scheduled method
create-client Creates a client interface
create-controller Creates a controller and associated test
create-websocket-client Creates a Websocket client
create-websocket-server Creates a Websocket server
Web API(create-app
)以外にも、CLIアプリやFunctionも作れるみたいですね。
アプリケーションプロジェクトを生成してみる
とりあえず、一番普通っぽいアプリケーション形式でプロジェクトを生成してみます。
$ mn create-app micronaut-sample
| Generating Java project...
| Application created at /tmp/micronaut-sample
↓こんなファイル群が生成されます。
$ tree
micronaut-sample
├── Dockerfile
├── build.gradle
├── gradle
│ └── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── micronaut-cli.yml
└── src
├── main
│ ├── java
│ │ └── micronaut
│ │ └── sample
│ │ └── Application.java
│ └── resources
│ ├── application.yml
│ └── logback.xml
└── test
└── java
└── micronaut
└── sample
デフォルトなので、言語はJavaになっています。みんな大好きGradleのGradle Wrapperももちろん標準装備です。Microservices用ということでDockerfile
も標準装備です。いたれりつくせりです。
build.gradle
で依存ライブラリをみてみると、http系やバリデータ系のライブラリが入ってますね。
//...
dependencies {
annotationProcessor "io.micronaut:micronaut-inject-java"
annotationProcessor "io.micronaut:micronaut-validation"
compile "io.micronaut:micronaut-inject"
compile "io.micronaut:micronaut-validation"
compile "io.micronaut:micronaut-runtime"
compile "io.micronaut:micronaut-http-client"
compile "io.micronaut:micronaut-http-server-netty"
compileOnly "io.micronaut:micronaut-inject-java"
runtime "ch.qos.logback:logback-classic:1.2.3"
testCompile "junit:junit:4.12"
testCompile "io.micronaut:micronaut-inject-java"
testCompile "org.hamcrest:hamcrest-all:1.3"
}
//...
素のアプリケーションを起動してみる
まだ何ひとつ実装してませんが、とりあえずアプリケーションを起動してみましょう。
Grailsとは違って、Micronautのmn
コマンドはテンプレートからの生成系のコマンドしかサポートしていません。実行/ビルド系はGradleに一本化されているようです。
というわけで、Gradle Wrapperのgradlew
スクリプトを使ってrun
タスクを実行してみます。初回はGradle Wrapperで必要なファイルや、Micronautの依存ライブラリが自動的にダウンロードされるので、しばらく待ちましょう。
$ ./gradlew run
....(snipped).....
> Task :run
14:56:48.895 [main] INFO io.micronaut.runtime.Micronaut - Startup completed in 735ms. Server Running: http://localhost:8080
という感じでログが表示されたら起動成功です。run
タスクの場合はプロンプトが返って来ません。CTRL+Cすれば止まります。
別のターミナルを開いて、HTTPieで接続してみましょう。
$ http :8080
HTTP/1.1 404 Not Found
Date: Mon, 3 Dec 2018 15:00:16 GMT
connection: close
content-length: 77
content-type: application/json
{
"_links": {
"self": {
"href": "/",
"templated": false
}
},
"message": "Page Not Found"
}
JSONでなんかそれっぽい404レスポンスが返ってきました。
REST APIでHello Worldしてみる
さて、200レスポンスが返るようにコントローラを生成します。
$ mn create-controller hello
| Rendered template Controller.java to destination src/main/java/micronaut/sample/HelloController.java
| Rendered template ControllerTest.java to destination src/test/java/micronaut/sample/HelloControllerTest.java
コントローラのデフォルト実装はこんな感じ。
//...
@Controller("/hello")
public class HelloController {
@Get("/")
public HttpStatus index() {
return HttpStatus.OK;
}
}
アプリを起動し直して、/hello
にアクセスしてみると...
$ http :8080/hello
HTTP/1.1 200 OK
Date: Mon, 3 Dec 2018 15:35:59 GMT
connection: keep-alive
transfer-encoding: chunked
200なレスポンスが返ってきました。
Hello Worldとしてはテキストでボディも返しておきたいので、コントローラを次のように修正します。
//...
@Controller("/hello")
public class HelloController {
@Get(produces = MediaType.TEXT_PLAIN) // producesを指定しない場合、デフォルトはJSON
public String index() {
return "Hello, Micronaut!";
}
}
アプリを起動し直して、/hello
にアクセスしてみると...
$ http :8080/hello
HTTP/1.1 200 OK
Date: Mon, 3 Dec 2018 15:46:10 GMT
connection: keep-alive
content-length: 17
content-type: text/plain
Hello, Micronaut!
できました。
ちなみに起動時のログはこう↓でしたが(再掲)、
$ ./gradlew run
....(snipped).....
> Task :run
14:56:48.895 [main] INFO io.micronaut.runtime.Micronaut - Startup completed in 735ms. Server Running: http://localhost:8080
...お分かりになっただろうか。
Startup completed in 735ms
1秒かからずに起動してますね。サクッと起動するので、この程度であれば自動リロード機能がなくてもあまり辛くなさそうです。ただコード量が増えたときにどのぐらい遅くなるのかは気になります。
なお、Spring Loadedによる自動リロード機能はフィーチャとしては存在しますが、
Spring-Loaded is not actively maintained and currently only supports Java versions < 9.
ということで、Java 8で開発する分には使えますが、それ以降の場合はまだ使えないようです。
さて、メモリフットプリントはどうなってるでしょうか。ちょっといい加減ですが、jvisualvm
でモニタリングしつつ手動でGCを発動して底を打ったときのメモリ使用量を見てみると、
と、瞬間最低で6MB台を記録しています。これは確かに相当小さいですね。比較対象として、Grails 3.3.8でREST API向けプロジェクトの素の状態で生成したアプリを起動してみると、
と約88MBでした。Micronautでは相当カリカリに削りこんできたなぁという感じがしますね。
テストを書いてみる
さて、create-controller
したときにHelloControllerTest
テストクラスも一緒に生成されていました。Javaベースのプロジェクトの場合は、テスティングフレームワークとしてJUnit 4.xが使われます。
//...
public class HelloControllerTest {
@Test
public void testIndex() throws Exception {
try (EmbeddedServer server = ApplicationContext.run(EmbeddedServer.class)) {
try (RxHttpClient client = server.getApplicationContext().createBean(RxHttpClient.class, server.getURL())) {
assertEquals(HttpStatus.OK, client.toBlocking().exchange("/hello").status());
}
}
}
}
これはモックを使ったユニットテストではなく、実際にアプリを起動してそのポートにアクセスするEnd-to-Endテストになっています。
テストの実行は、Gradleのtest
タスク(またはcheck
タスク)を使います。
$ ./gradlew test
....
BUILD SUCCESSFUL in 4s
5 actionable tasks: 5 executed
自動生成されたテストコードでは、「レスポンスのHTTPステータスが200かどうか」をチェックしてますが、コントローラを改造してボディも返すようにしたので、それに対応してみます。
public class HelloControllerTest {
@Test
public void testIndex() throws Exception {
try (EmbeddedServer server = ApplicationContext.run(EmbeddedServer.class)) {
try (RxHttpClient client = server.getApplicationContext().createBean(RxHttpClient.class, server.getURL())) {
assertEquals("Hello, Micronaut!", client.toBlocking().retrieve("/hello"));
}
}
}
}
テストを実行すると、期待通りパスします。
$ ./gradlew test
....
BUILD SUCCESSFUL in 4s
5 actionable tasks: 5 executed
ちなみにわざと変な期待値を書いてテストを失敗させると、
assertEquals("Bye!", client.toBlocking().retrieve("/hello"));
$ ./gradlew test
> Task :test
micronaut.sample.HelloControllerTest > testIndex FAILED
org.junit.ComparisonFailure at HelloControllerTest.java:16
1 test completed, 1 failed
> Task :test FAILED
FAILURE: Build failed with an exception.
* What went wrong:
Execution failed for task ':test'.
> There were failing tests. See the report at: file:///tmp/micronaut-sample/build/reports/tests/test/index.html
* Try:
Run with --stacktrace option to get the stack trace. Run with --info or --debug option to get more log output. Run with --scan to get full insights.
* Get more help at https://help.gradle.org
Deprecated Gradle features were used in this build, making it incompatible with Gradle 5.0.
Use '--warning-mode all' to show the individual deprecation warnings.
See https://docs.gradle.org/4.10/userguide/command_line_interface.html#sec:command_line_warnings
BUILD FAILED in 4s
4 actionable tasks: 2 executed, 2 up-to-date
こんな感じでエラーになります。
Gradleのテストレポートであるmicronaut-sample/build/reports/tests/test/index.html
をブラウザで開いてみると、
こんな感じで何がまずかったのかを確認できます。
実行可能JARをビルドしてみる
さて、次にデプロイ用のJARファイルを生成してみます。
Gradleのassemble
タスクを実行するとbuild/libs
配下に2つのJARファイルが生成されます。
$ ./gradlew assemble
...
$ tree -sh build/libs
build/libs
├── [ 11M] micronaut-sample-0.1-all.jar
└── [7.3K] micronaut-sample-0.1.jar
このうちの-all
が付いているほうが、全部入りの実行可能なFat JARになっています。
参考までに、Grails 3.3.8でREST API向けプロジェクトの素の状態で生成したFat JARのサイズは38MB程度です。この11MBというサイズは、1/3程度でかなり小さくなった感じです。
試しに起動してみましょう。
$ java -jar build/libs/micronaut-sample-0.1-all.jar
16:21:37.404 [main] INFO io.micronaut.runtime.Micronaut - Startup completed in 971ms. Server Running: http://localhost:8080
$ http :8080/hello
HTTP/1.1 200 OK
Date: Mon, 3 Dec 2018 16:21:59 GMT
connection: keep-alive
content-length: 17
content-type: text/plain
Hello, Micronaut!
動きますね。
Dockerイメージにしてみる
前述のようにDockerfile
が標準装備されています。
FROM openjdk:8u171-alpine3.7
RUN apk --no-cache add curl
COPY build/libs/*-all.jar micronaut-sample.jar
CMD java ${JAVA_OPTS} -jar micronaut-sample.jar
まだopenjdk:8u171-alpine3.7
になっていますが、そこはスルーしておきます。
docker
コマンドでイメージをビルドします。
$ docker build . -t micronaut-sample
イメージサイズを確認してみます。
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
micronaut-sample latest 5242b7291de8 2 minutes ago 114MB
openjdk 8u171-alpine3.7 1caad94162ef 4 months ago 102MB
micronaut-sample
は114MBでした。ベースのopenjdk:8u171-alpine3.7
が102MBなので、アプリとしては114-102=12MBでほぼJARファイルの分ですね。
コンテナを起動してみます。
$ docker run -it --rm -p 8080:8080 micronaut-sample
07:44:58.298 [main] INFO io.micronaut.runtime.Micronaut - Startup completed in 2369ms. Server Running: http://79df3644f4e4:8080
起動に2秒以上かかっています。素で起動するよりもオーバヘッドが大きいようです。
$ http :8080/hello
HTTP/1.1 200 OK
Date: Mon, 3 Dec 2018 07:44:59 GMT
connection: keep-alive
content-length: 17
content-type: text/plain
Hello, Micronaut!
アクセスできますね。
(実験的機能) GraalVMで起動してみる
Micronautは、実験的機能(厳密には*"Incubating Phase"*)としてGraalVMでの実行に対応しています。
GraalVMサポートはフィーチャ(Feature)のひとつとして実装されています。create-app
するときにフィーチャを指定すると、build.gradle
に必要な設定が追加されていたり、便利なスクリプトなどが追加されたりします。ただ、既存のプロジェクトに後からフィーチャを追加する方法は(少なくともまだ)提供されていないので、ここでは別のダミープロジェクトを生成して、そこから必要そうなファイルをピックアップすることで試してみます。
$ mkdir /tmp/graalvm
$ cd /tmp/graalvm
$ mn create-app micronaut-sample --features=graal-native-image
# ↓この3つのファイルがポイント
$ cp /tmp/graalvm/micronaut-sample/build.gradle /tmp/micronaut-sample/
$ cp /tmp/graalvm/micronaut-sample/Dockerfile /tmp/micronaut-sample/DockerfileForGraalVM
$ cp /tmp/graalvm/micronaut-sample/src/main/java/micronaut/sample/MicronautSubstitutions.java /tmp/micronaut-sample/src/main/java/micronaut/sample/
$ cd /tmp/micronaut-sample
$ ./gradlew clean assemble
$ docker build . -f DockerfileForGraalVM -t micronaut-sample-graalvm
....
Successfully built 5dc364049043
Successfully tagged micronaut-sample-graalvm:latest
このイメージのビルドは結構時間がかかるので、辛抱強く待ちます。
さて、コンテナを起動してみましょう。
$ docker run -it --rm -p 8080:8080 micronaut-sample-graalvm
09:10:59.735 [main] INFO io.micronaut.runtime.Micronaut - Startup completed in 26ms. Server Running: http://66cb51a477b7:8080
驚異の起動時間26msを達成。何これすごい。
$ http :8080/hello
HTTP/1.1 200 OK
Date: Mon, 3 Dec 2018 09:12:13 GMT
connection: keep-alive
content-length: 17
content-type: text/plain
Hello, Micronaut!
普通に動いてますね。
ちなみに、なぜかGraalVM版のコンテナはCTRL+Cで終わらないので、docker kill
で落としましょう。
Dockerのイメージサイズはというと、
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
micronaut-sample-graalvm latest 5dc364049043 4 minutes ago 1.67GB
oracle/graalvm-ce 1.0.0-rc8 88858be11628 6 weeks ago 1.56GB
アホみたいにデカいです。Oracleのoracle/graalvm-ce
自体がデカいですね。あまりにもデカすぎな気がしますが、そのうち小さくなるんでしょうか。
なお、現在のGraalVMサポートとしてはGroovyで実装したアプリは非対応です。GraalVMで生成するネイティブイメージは、現時点ではリフレクションを部分的にしかサポートしていないのですが、Groovyはそのリフレクションに強く依存しているためです。残念。
(おまけ) Groovy/Spock/GORMでGrailsっぽくアプリを実装してみる
GORM (Grails' Object Relational Mapping)は、Groovy版Active Record的なORマッパであり、Grailsの主要な便利機能のひとつなのですが、最近はSpring BootなどGrails以外のフレームワークと組み合わせて使えるようにStandalone GORMというのが提供されています。
MicronautでGORMを使うには、hibernate-gorm
フィーチャを指定してプロジェクトを生成します。
hibernate-gorm
フィーチャを指定すると自動的にgroovy
フィーチャもONになり、プログラミング言語がGroovyになります。また、groovy
フィーチャが追加されると自動的にspock
フィーチャもONになり、テスティングフレームワークもSpockになります。
$ mn create-app micronaut-sample-gorm --features=hibernate-gorm
| Generating Groovy project...
| Application created at /tmp/micronaut-sample-gorm
生成されるファイル構成はこんな感じ。なぜかsrc/test/java
ディレクトリが無駄に生成されています。まあ、バグですかね。
$ tree
micronaut-sample-gorm
├── Dockerfile
├── build.gradle
├── gradle
│ └── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── micronaut-cli.yml
└── src
├── main
│ ├── groovy
│ │ └── micronaut
│ │ └── sample
│ │ └── gorm
│ │ └── Application.groovy
│ └── resources
│ ├── application.yml
│ └── logback.xml
└── test
├── groovy
│ └── micronaut
│ └── sample
│ └── gorm
└── java
└── micronaut
└── sample
└── gorm
依存関係はこんな感じ。
//.....
dependencies {
compile "io.micronaut.configuration:micronaut-hibernate-validator"
compile "io.micronaut.configuration:micronaut-hibernate-gorm"
compile "io.micronaut:micronaut-http-client"
compile "io.micronaut:micronaut-http-server-netty"
compile "javax.annotation:javax.annotation-api"
compile "io.micronaut:micronaut-runtime-groovy"
compile "io.micronaut:micronaut-validation"
compileOnly "io.micronaut:micronaut-inject-groovy"
runtime "ch.qos.logback:logback-classic:1.2.3"
runtime "com.h2database:h2"
runtime "org.apache.tomcat:tomcat-jdbc"
testCompile "io.micronaut:micronaut-inject-groovy"
testCompile "junit:junit:4.12"
testCompile "io.micronaut:micronaut-inject-java"
testCompile "org.hamcrest:hamcrest-all:1.3"
testCompile("org.spockframework:spock-core") {
exclude group: "org.codehaus.groovy", module: "groovy-all"
}
}
//.....
設定ファイルであるapplication.yml
の中身はこんな感じ。
---
micronaut:
application:
name: micronaut-sample-gorm
---
hibernate:
hbm2ddl:
auto: update
cache:
queries: false
use_second_level_cache: true
use_query_cache: false
region.factory_class: org.hibernate.cache.ehcache.SingletonEhCacheRegionFactory
dataSource:
url: jdbc:h2:mem:devDb;MVCC=TRUE;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE
pooled: true
jmxExport: true
driverClassName: org.h2.Driver
username: sa
password: ''
なんとなく雰囲気がわかったので、TODO管理するREST APIをエイヤっと実装してみたプロジェクトがこちらです。
起動して適当にアクセスしてみます。
$ http :8080/todo
HTTP/1.1 200 OK
Date: Mon, 3 Dec 2018 22:48:32 GMT
connection: keep-alive
content-length: 2
content-type: application/json
[]
$ http post :8080/todo title=牛乳 deadline=明日
HTTP/1.1 201 Created
Date: Mon, 3 Dec 2018 22:48:42 GMT
connection: keep-alive
content-length: 45
content-type: application/json
{
"deadline": "明日",
"id": 1,
"title": "牛乳"
}
$ http post :8080/todo title=パン deadline=今日
HTTP/1.1 201 Created
Date: Mon, 3 Dec 2018 22:49:04 GMT
connection: keep-alive
content-length: 45
content-type: application/json
{
"deadline": "今日",
"id": 2,
"title": "パン"
}
$ http :8080/todo
HTTP/1.1 200 OK
Date: Mon, 3 Dec 2018 22:49:16 GMT
connection: keep-alive
content-length: 93
content-type: application/json
[
{
"deadline": "明日",
"id": 1,
"title": "牛乳"
},
{
"deadline": "今日",
"id": 2,
"title": "パン"
}
]
$ http :8080/todo/1
HTTP/1.1 200 OK
Date: Mon, 3 Dec 2018 22:49:21 GMT
connection: keep-alive
content-length: 45
content-type: application/json
{
"deadline": "明日",
"id": 1,
"title": "牛乳"
}
$ http put :8080/todo/1 title=豆乳 deadline=明日
HTTP/1.1 200 OK
Date: Mon, 3 Dec 2018 22:49:25 GMT
connection: keep-alive
content-length: 45
content-type: application/json
{
"deadline": "明日",
"id": 1,
"title": "豆乳"
}
$ http :8080/todo
HTTP/1.1 200 OK
Date: Mon, 3 Dec 2018 22:49:51 GMT
connection: keep-alive
content-length: 93
content-type: application/json
[
{
"deadline": "明日",
"id": 1,
"title": "豆乳"
},
{
"deadline": "今日",
"id": 2,
"title": "パン"
}
]
$ http delete :8080/todo/2
HTTP/1.1 200 OK
Date: Mon, 3 Dec 2018 22:49:54 GMT
connection: keep-alive
transfer-encoding: chunked
$ http :8080/todo
HTTP/1.1 200 OK
Date: Mon, 3 Dec 2018 22:49:56 GMT
connection: keep-alive
content-length: 47
content-type: application/json
[
{
"deadline": "明日",
"id": 1,
"title": "豆乳"
}
]
この程度ならもっと簡単に瞬殺できるかと思いきや、2〜3時間かかってしまった...。言い訳というか感想は↓こんな感じ。
- Standalone GORMは、GrailsのGORMと比較して微妙に挙動(設定?)が異なる部分があってハマった。
- コントローラのテストがGrailsのようなモック式ではなく、実際のEnd-to-Endなのは良い。
- が、低レベルAPIでコントローラのテストを書くのはつらい。あとで、宣言的RESTクライアントというやつで書き直してみたい。
- productionレベルで使うにはもっと突っ込んだプロトタイプで試さないと不安。
- まだバージョンは1.0.1なので、Grailsチームのいつものペースとも言える。もうちょっと版を重ねると安定してくるはず。たぶん。
おわりに
というわけで、触り程度ですがお試しがてらMicronautについて紹介してきました。
他にも
- コンパイル時DIというのは具体的にどういうこと?
- Springファミリーのライブラリを使える?
- CLIアプリだとどんな感じになる?
- Functionってどんな感じ?
- Federationとはなんぞ?
などなど色々興味はつきませんが、だいぶ長くなったのでこのエントリとしてはひとまずここまでとします。
それでは、引き続き明日以降もNTTテクノクロス Advent Calendar 2018をお楽しみください。
リソース
- Micronaut本家サイト - http://micronaut.io/
- サンプルコード: Micronaut Sample Application with Java - https://github.com/nobeans/micronaut-sample-java
- サンプルコード: Micronaut Sample Application with Groovy/Spock/GORM - https://github.com/nobeans/micronaut-sample-gorm