kotlinでmicronaut触ってみた
これの続き
前回はquarkusをkotlinで触ってみたので今回はmmicronautを触ってみる。
作ったアプリはこちら。
micronautとは
quarkusと同じくマイクロフレームワークと呼ばれるコンテナなどの使用が期待される軽量フレームワーク。だいたいquarkusとできることは同じで、micronautじゃないとできないみたいなことはないと思う。ただ、micronautはJava,kotlinだけでなくGroovyも対応していたり、テスティングフレームワークがJUnitだけでなくKotestやSpockまで対応していてプロジェクト作成時に選択できるのはかなり嬉しい。あと公式サイトやドキュメントがmicronautの方がいけてる気がする。
インストール
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で使用するマイグレーションファイルを配置して起動すればマイグレーションが実行されテーブルが作成される。
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
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 = ""
)
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
package micronaut.data.demo.data
import io.micronaut.data.annotation.Repository
import io.micronaut.data.repository.CrudRepository
@Repository
interface UserRepository : CrudRepository<UserEntity, Long> {
}
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の実装のみ)
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)
}
ここで下記の依存関係を追加する。
kapt("io.micronaut:micronaut-inject-java")
runtimeOnly("io.micronaut:micronaut-core")
micronautはSpringで多用しているリフレクションを使わないような設計になっている。事前コンパイル(AOP)の時に@Introspectedのついたクラスを読み込んでおくことで起動を速くしているよう。詳しくはこちらの記事が参考になった
とりあえず、POJO的なdetaクラスには付けておけばいいと思う
Controller
javax.ws.rsのパッケージを使用したいので依存関係を追加。
annotationProcessor("io.micronaut.jaxrs:micronaut-jaxrs-processor")
implementation("io.micronaut.jaxrs:micronaut-jaxrs-server")
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)
テスト
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でエラーになる
ので、docker imageは諦めました
まとめ
- 全体的な雰囲気はquarkusよりSpringに近い。特にデータベースまわりはほぼSpring
- 公式のドキュメントやガイドがquarkusより洗練されていて見やすい。
- kotlinのサポートがquarkusよりされていそう。
- テストでKotestやSpockが選べるのは嬉しい。
- CLIから作成するプロジェクトの雛形が細かいところまでいい感じにしといてくれて楽。
基本的な流れはquarkusと変わらないけどkotlinで使うならmicronautの方がサポートされていそうだなと感じた。プロジェクトの雛形はmicronautの方がかなり秀逸。何よりKotest使えるのが最高。あと公式サイトがいけてる。
kotlinならmicronautの方が個人的には好き。Javaやk8s使うとなるとquarkusの方がいいのかもしれない。あと、使い込んでみないと分かんないけどやっぱりquarkusはpanacheがよかった。あとは本命のktorを触ってみる
以上!!
Discussion