🚀

kotlinでmicronaut触ってみた

2022/08/23に公開

これの続き
https://zenn.dev/jy8752/articles/593dc0dbeffced

前回はquarkusをkotlinで触ってみたので今回はmmicronautを触ってみる。
作ったアプリはこちら。
https://github.com/JY8752/micronaut-demo

micronautとは

quarkusと同じくマイクロフレームワークと呼ばれるコンテナなどの使用が期待される軽量フレームワーク。だいたいquarkusとできることは同じで、micronautじゃないとできないみたいなことはないと思う。ただ、micronautはJava,kotlinだけでなくGroovyも対応していたり、テスティングフレームワークがJUnitだけでなくKotestやSpockまで対応していてプロジェクト作成時に選択できるのはかなり嬉しい。あと公式サイトやドキュメントがmicronautの方がいけてる気がする。

https://micronaut.io/

インストール

sdkmanでインストール。

curl -s https://get.sdkman.io | bash

source "$HOME/.sdkman/bin/sdkman-init.sh"

sdk install micronaut

mn --version
> Micronaut Version: 3.6.0

プロジェクト作成

quarkusと大体同じ。ただ、-tオプションでテスティングフレームワークが選べる。
DBにMySQL、ORMはmicronaut data JPA、マイグレーションにflywayを使ったのでオプションで指定してるけど後から依存関係に追加でも問題ない。

mn create-app micronaut-data-demo \
-b gradle_kotlin -l kotlin --jdk 17 -t kotest \
-f flyway,mysql,data-jpa,jdbc-hikari

開くとこんな感じ。
application.propertyじゃなくてデフォルトでapplication.ymlなのが地味に嬉しい。あとKotestのconfigファイルまで作成されてる!!素敵すぎる!

//テスト
./gradlew test

//ビルド
./gradlew build

//起動
./gradlew run

ここら辺はmicronautのコマンドはなく普通にgradleで実行する。quarkusは何をしていいかよくわからなかったからmicronautの方が好き。最初のプロジェクト作成だけでいい。

Enable annotation Processing

Intellijで開発をする場合は設定の「Build, Execution, Deployment > Compiler > Annotation Processors」のEnable annotation Processingを有効にする。

データベースの準備

quarkusの時は何も考えずにpanache使ったけどmicronautの場合はSpringみたいに、JPAやJDBCを使用することもできるしMyBatisやJOOQみたいなのも使える。今回はMicronaut Data JDBCを使用する。テーブルは前回と一緒でこんな感じのやつ

application.ymlを修正し、flywayで使用するマイグレーションファイルを配置して起動すればマイグレーションが実行されテーブルが作成される。

application.yml
micronaut:
  application:
    name: micronautDataDemo
+datasources:
+  default:
+    url: jdbc:mysql://localhost:3306/demo?characterEncoding=utf-8&characterSetResults=utf-8&connectionCollation=utf8mb4_bin
+    driverClassName: com.mysql.cj.jdbc.Driver
+    db-type: mysql
+    schema-generate: CREATE_DROP
+    dialect: MYSQL
+    username: root
+    password: root
netty:
  default:
    allocator:
      max-order: 3
+flyway:
+  datasources:
+    default:
+      enabled: true
+jpa:
+  default:
+    entity-scan:
+      packages: 'micronaut.data.demo.data'

Entity

UserEntity.kt
package micronaut.data.demo.data

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

@Entity(name = "user")
data class UserEntity(
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long? = null,
    val name: String = ""
)
TaskEntity.kt
package micronaut.data.demo.data

import java.time.LocalDateTime
import javax.persistence.*

@Entity(name = "task")
data class TaskEntity(
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long? = null,

    @ManyToOne
    @JoinColumn(name = "user_id")
    val user: UserEntity = UserEntity(),

    val details: String = "",

    val createdAt: LocalDateTime = LocalDateTime.now()
)

Repository

UserRepository.kt
package micronaut.data.demo.data

import io.micronaut.data.annotation.Repository
import io.micronaut.data.repository.CrudRepository

@Repository
interface UserRepository : CrudRepository<UserEntity, Long> {
}
TaskRepository.kt
package micronaut.data.demo.data

import io.micronaut.data.annotation.Repository
import io.micronaut.data.repository.CrudRepository

@Repository
interface TaskRepository : CrudRepository<TaskEntity, Long> {
}

Spring Data JPAとほぼ同じ感じになると思う。

Service

(特に特別なこともないので以後Userの実装のみ)

UserService.kt
package micronaut.data.demo.domain

import io.micronaut.core.annotation.Introspected
import jakarta.inject.Singleton
import micronaut.data.demo.data.UserEntity
import micronaut.data.demo.data.UserRepository

@Singleton
class UserService(
    private val userRepository: UserRepository
) {
    fun create(name: String): User {
        val entity = this.userRepository.save(UserEntity(name = name))
        return User(entity)
    }

    fun find(id: Long): User? {
        val entity = this.userRepository.findById(id)
        return if (entity.isPresent) {
            User(entity.get())
        } else {
            null
        }
    }
}

@Introspected
data class User(val id: Long, val name: String) {
    constructor(entity: UserEntity) : this(entity.id!!, entity.name)
}

ここで下記の依存関係を追加する。

build.gradle.kts
    kapt("io.micronaut:micronaut-inject-java")
    runtimeOnly("io.micronaut:micronaut-core")

micronautはSpringで多用しているリフレクションを使わないような設計になっている。事前コンパイル(AOP)の時に@Introspectedのついたクラスを読み込んでおくことで起動を速くしているよう。詳しくはこちらの記事が参考になった

https://www.sakatakoichi.com/entry/micronautintrospection

とりあえず、POJO的なdetaクラスには付けておけばいいと思う

Controller

javax.ws.rsのパッケージを使用したいので依存関係を追加。

build.gradle.kts
    annotationProcessor("io.micronaut.jaxrs:micronaut-jaxrs-processor")
    implementation("io.micronaut.jaxrs:micronaut-jaxrs-server")
UserController.kt
package micronaut.data.demo.application

import io.micronaut.core.annotation.Introspected
import io.micronaut.http.annotation.Body
import io.micronaut.http.annotation.Controller
import io.micronaut.http.annotation.Get
import io.micronaut.http.annotation.PathVariable
import io.micronaut.http.annotation.Post
import micronaut.data.demo.domain.UserService
import javax.ws.rs.Consumes
import javax.ws.rs.Produces
import javax.ws.rs.core.MediaType
import javax.ws.rs.core.Response

@Controller("/user")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
class UserController(
    private val userService: UserService
) {
    @Get("/{id}")
    fun find(@PathVariable id: Long): Response {
        val user = this.userService.find(id)
        return Response.ok(user).build()
    }

    @Post
    fun create(@Body request: CreateUserRequest): Response {
        val user = this.userService.create(request.name)
        return Response.ok(user).build()
    }
}

@Introspected
data class CreateUserRequest(val name: String)

テスト

UserControllerTest
package micronaut.data.demo.application

import io.kotest.core.spec.style.StringSpec
import io.kotest.matchers.shouldBe
import io.micronaut.http.HttpRequest
import io.micronaut.http.client.HttpClient
import io.micronaut.http.client.annotation.Client
import io.micronaut.test.extensions.kotest.annotation.MicronautTest

@MicronautTest
internal class UserControllerTest(
    @Client("/") private val client: HttpClient
) : StringSpec({
    "test" {
        val request: HttpRequest<Any> = HttpRequest.GET("/user/1")
        val response = client.toBlocking().retrieve(request)

        response shouldBe """{"id":1,"name":"user"}"""
    }
})

こんな感じ。@MicronautTestをつければok。ちゃんとKotest使える、最高。@MicronautTestにはrollbackをbooleanで指定できるがデフォルトでtrueなのでテスト後はロールバックされる。

native image ビルド

GraalVMのインストールなどは前回の記事参照。

//ビルド(大体3分くらい
./gradlew nativeCompile

//実行(爆速)
build/native/nativeCompile/micronaut-data-demo

 __  __ _                                  _   
|  \/  (_) ___ _ __ ___  _ __   __ _ _   _| |_ 
| |\/| | |/ __| '__/ _ \| '_ \ / _` | | | | __|
| |  | | | (__| | | (_) | | | | (_| | |_| | |_ 
|_|  |_|_|\___|_|  \___/|_| |_|\__,_|\__,_|\__|
  Micronaut (v3.6.0)

23:50:15.404 [main] INFO  com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Starting...

おまけ

native docker imageビルドをしてみる。

./gradlew dockerBuildNative

> Configure project :
[native-image-plugin] Instrumenting task with the native-image-agent: test
[native-image-plugin] Instrumenting task with the native-image-agent: testNativeImage
Exception in thread "docker-java-stream-2062079780" java.lang.UnsatisfiedLinkError: Can't load library: /Users/yamanakajunichi/Library/Caches/JNA/temp/jna16831078537416111331.tmp
        at java.base/java.lang.ClassLoader.loadLibrary(ClassLoader.java:2393)
        at java.base/java.lang.Runtime.load0(Runtime.java:755)
        at java.base/java.lang.System.load(System.java:1953)
        at com.sun.jna.Native.loadNativeDispatchLibraryFromClasspath(Native.java:1018)
        at com.sun.jna.Native.loadNativeDispatchLibrary(Native.java:988)
        at com.sun.jna.Native.<clinit>(Native.java:195)
        at com.github.dockerjava.httpclient5.UnixDomainSocket.<clinit>(UnixDomainSocket.java:80)
        at com.github.dockerjava.httpclient5.ApacheDockerHttpClientImpl$2.createSocket(ApacheDockerHttpClientImpl.java:116)
        at com.bmuschko.gradle.docker.shaded.org.apache.hc.client5.http.impl.io.DefaultHttpClientConnectionOperator.connect(DefaultHttpClientConnectionOperator.java:125)
        at com.bmuschko.gradle.docker.shaded.org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager.connect(PoolingHttpClientConnectionManager.java:409)
        at com.bmuschko.gradle.docker.shaded.org.apache.hc.client5.http.impl.classic.InternalExecRuntime.connectEndpoint(InternalExecRuntime.java:164)
        at com.bmuschko.gradle.docker.shaded.org.apache.hc.client5.http.impl.classic.InternalExecRuntime.connectEndpoint(InternalExecRuntime.java:174)
        at com.bmuschko.gradle.docker.shaded.org.apache.hc.client5.http.impl.classic.ConnectExec.execute(ConnectExec.java:135)
        at com.bmuschko.gradle.docker.shaded.org.apache.hc.client5.http.impl.classic.ExecChainElement.execute(ExecChainElement.java:51)
        at com.bmuschko.gradle.docker.shaded.org.apache.hc.client5.http.impl.classic.ExecChainElement$1.proceed(ExecChainElement.java:57)
        at com.bmuschko.gradle.docker.shaded.org.apache.hc.client5.http.impl.classic.ProtocolExec.execute(ProtocolExec.java:165)
        at com.bmuschko.gradle.docker.shaded.org.apache.hc.client5.http.impl.classic.ExecChainElement.execute(ExecChainElement.java:51)
        at com.bmuschko.gradle.docker.shaded.org.apache.hc.client5.http.impl.classic.ExecChainElement$1.proceed(ExecChainElement.java:57)
        at com.bmuschko.gradle.docker.shaded.org.apache.hc.client5.http.impl.classic.HttpRequestRetryExec.execute(HttpRequestRetryExec.java:93)
        at com.bmuschko.gradle.docker.shaded.org.apache.hc.client5.http.impl.classic.ExecChainElement.execute(ExecChainElement.java:51)
        at com.bmuschko.gradle.docker.shaded.org.apache.hc.client5.http.impl.classic.ExecChainElement$1.proceed(ExecChainElement.java:57)
        at com.bmuschko.gradle.docker.shaded.org.apache.hc.client5.http.impl.classic.RedirectExec.execute(RedirectExec.java:116)
        at com.bmuschko.gradle.docker.shaded.org.apache.hc.client5.http.impl.classic.ExecChainElement.execute(ExecChainElement.java:51)
        at com.bmuschko.gradle.docker.shaded.org.apache.hc.client5.http.impl.classic.ExecChainElement$1.proceed(ExecChainElement.java:57)
        at com.bmuschko.gradle.docker.shaded.org.apache.hc.client5.http.impl.classic.ContentCompressionExec.execute(ContentCompressionExec.java:128)
        at com.bmuschko.gradle.docker.shaded.org.apache.hc.client5.http.impl.classic.ExecChainElement.execute(ExecChainElement.java:51)
        at com.bmuschko.gradle.docker.shaded.org.apache.hc.client5.http.impl.classic.InternalHttpClient.doExecute(InternalHttpClient.java:178)
        at com.bmuschko.gradle.docker.shaded.org.apache.hc.client5.http.impl.classic.CloseableHttpClient.execute(CloseableHttpClient.java:67)
        at com.github.dockerjava.httpclient5.ApacheDockerHttpClientImpl.execute(ApacheDockerHttpClientImpl.java:149)
        at com.github.dockerjava.httpclient5.ApacheDockerHttpClient.execute(ApacheDockerHttpClient.java:8)
        at com.github.dockerjava.core.DefaultInvocationBuilder.execute(DefaultInvocationBuilder.java:228)
        at com.github.dockerjava.core.DefaultInvocationBuilder.lambda$executeAndStream$1(DefaultInvocationBuilder.java:269)
        at java.base/java.lang.Thread.run(Thread.java:833)

> Task :dockerBuildNative
Building image using context '/Users/yamanakajunichi/work/myapp/study/micronaut/micronaut-data-demo/build/docker/native-main'.
Using Dockerfile '/Users/yamanakajunichi/work/myapp/study/micronaut/micronaut-data-demo/build/docker/native-main/DockerfileNative'
Using images 'micronaut-data-demo'.
<============-> 92% EXECUTING [8h 14m 44s]
> :dockerBuildNative

おわんない..
なんかエラー出てる??

たぶん、これでdocker gradle pluginのバージョンが最新じゃなくてM1 Macでエラーになる
https://github.com/micronaut-projects/micronaut-gradle-plugin/issues/520

ので、docker imageは諦めました

まとめ

  • 全体的な雰囲気はquarkusよりSpringに近い。特にデータベースまわりはほぼSpring
  • 公式のドキュメントやガイドがquarkusより洗練されていて見やすい。
  • kotlinのサポートがquarkusよりされていそう。
  • テストでKotestやSpockが選べるのは嬉しい。
  • CLIから作成するプロジェクトの雛形が細かいところまでいい感じにしといてくれて楽。

基本的な流れはquarkusと変わらないけどkotlinで使うならmicronautの方がサポートされていそうだなと感じた。プロジェクトの雛形はmicronautの方がかなり秀逸。何よりKotest使えるのが最高。あと公式サイトがいけてる。

kotlinならmicronautの方が個人的には好き。Javaやk8s使うとなるとquarkusの方がいいのかもしれない。あと、使い込んでみないと分かんないけどやっぱりquarkusはpanacheがよかった。あとは本命のktorを触ってみる

以上!!

GitHubで編集を提案

Discussion