Micronaut GraalVM 支持

2023-03-14 14:00 更新

GraalVM 是來(lái)自 Oracle 的新型通用虛擬機(jī),它支持多語(yǔ)言運(yùn)行時(shí)環(huán)境以及將 Java 應(yīng)用程序編譯為本地機(jī)器代碼的能力。

任何 Micronaut 應(yīng)用程序都可以使用 GraalVM JVM 運(yùn)行,但是已向 Micronaut 添加了特殊支持以支持使用 GraalVM 的本機(jī)圖像工具運(yùn)行 Micronaut 應(yīng)用程序。

Micronaut 目前支持 GraalVM 版本 22.0.0.2,團(tuán)隊(duì)正在改進(jìn)每個(gè)新版本的支持。但是,如果您發(fā)現(xiàn)任何問題,請(qǐng)不要猶豫,報(bào)告問題。

Micronaut 的許多模塊和第三方庫(kù)已經(jīng)過(guò)驗(yàn)證可以與 GraalVM 一起工作:HTTP 服務(wù)器、HTTP 客戶端、Function 支持、Micronaut Data JDBC 和 JPA、Service Discovery、RabbitMQ、Views、Security、Zipkin 等。對(duì)其他模塊的支持是不斷發(fā)展,并將隨著時(shí)間的推移而改進(jìn)。

入門

僅在 Java 或 Kotlin 項(xiàng)目中支持使用 GraalVM 的原生圖像工具。 Groovy 嚴(yán)重依賴于 GraalVM 僅部分支持的反射。

要開始使用 GraalVM,請(qǐng)首先通過(guò)入門說(shuō)明或使用 Sdkman! 安裝 GraalVM SDK。

作為 GraalVM 原生鏡像的微服務(wù)

開始使用 Micronaut 和 GraalVM

從 Micronaut 2.2 開始,任何 Micronaut 應(yīng)用程序都可以使用 Micronaut Gradle 或 Maven 插件構(gòu)建到原生鏡像中。首先,創(chuàng)建一個(gè)新的應(yīng)用程序:

創(chuàng)建 GraalVM 原生微服務(wù)

$ mn create-app hello-world

您可以使用 --build maven 進(jìn)行 Maven 構(gòu)建。

使用 Docker 構(gòu)建原生鏡像

要使用 Gradle 和 Docker 構(gòu)建您的本機(jī)映像,請(qǐng)運(yùn)行:

使用 Docker 和 Gradle 構(gòu)建原生鏡像

$ ./gradlew dockerBuildNative

要使用 Maven 和 Docker 構(gòu)建您的本機(jī)映像,請(qǐng)運(yùn)行:

使用 Docker 和 Maven 構(gòu)建原生鏡像

$ ./mvnw package -Dpackaging=docker-native

不使用 Docker 構(gòu)建原生鏡像

要在不使用 Docker 的情況下構(gòu)建本機(jī)映像,請(qǐng)通過(guò)入門說(shuō)明或使用 Sdkman 安裝 GraalVM SDK?。?

使用 SDKman 安裝 GraalVM 22.0.0.2

$ sdk install java 22.0.0.2.r11-grl
$ sdk use java 22.0.0.2.r11-grl

本機(jī)圖像工具是從基礎(chǔ) GraalVM 發(fā)行版中提取的,可作為插件使用。要安裝它,請(qǐng)運(yùn)行:

安裝本機(jī)圖像工具

$ gu install native-image

現(xiàn)在,您可以通過(guò)運(yùn)行 nativeCompile 任務(wù)來(lái)使用 Gradle 構(gòu)建原生鏡像:

使用 Gradle 創(chuàng)建原生鏡像

$ ./gradlew nativeCompile

本機(jī)映像將構(gòu)建在 build/native/nativeCompile 目錄中。

要使用 Maven 和 Micronaut Maven 插件創(chuàng)建原生圖像,請(qǐng)使用原生圖像打包格式:

使用 Maven 創(chuàng)建原生鏡像

$ ./mvnw package -Dpackaging=native-image

在目標(biāo)目錄中構(gòu)建本機(jī)圖像。

然后,您可以從構(gòu)建它的目錄運(yùn)行本機(jī)映像。

運(yùn)行本機(jī)圖像

$ ./hello-world

了解 Micronaut 和 GraalVM

Micronaut 本身不依賴于反射或動(dòng)態(tài)類加載,因此它會(huì)自動(dòng)與 GraalVM 本機(jī)一起工作,但是 Micronaut 使用的某些第三方庫(kù)可能需要額外輸入有關(guān)反射使用的信息。

Micronaut 包含一個(gè)注解處理器,它有助于生成由原生圖像工具自動(dòng)獲取的反射配置:

 Gradle Maven 
annotationProcessor("io.micronaut:micronaut-graal")
<annotationProcessorPaths>
    <path>
        <groupId>io.micronaut</groupId>
        <artifactId>micronaut-graal</artifactId>
    </path>
</annotationProcessorPaths>

該處理器生成額外的類來(lái)實(shí)現(xiàn) GraalReflectionConfigurer 接口并以編程方式注冊(cè)反射配置。

例如下面的類:

package example;

import io.micronaut.core.annotation.ReflectiveAccess;

@ReflectiveAccess
class Test {
    ...
}

上面的示例導(dǎo)致 example.Test 的公共方法、聲明的字段和聲明的構(gòu)造函數(shù)被注冊(cè)為反射訪問。

如果您有更高級(jí)的要求并且只希望包含某些字段或方法,請(qǐng)?jiān)谌魏螛?gòu)造函數(shù)、字段或方法上使用注釋以僅包含特定字段、構(gòu)造函數(shù)或方法。

為反射訪問添加額外的類

為了通知 Micronaut 要包含在生成的反射配置中的其他類,可以使用許多注釋,包括:

  • @ReflectiveAccess - 可以在特定類型、構(gòu)造函數(shù)、方法或字段上聲明的注釋,以僅對(duì)帶注釋的元素啟用反射訪問。
  • @TypeHint - 允許批量配置對(duì)一種或多種類型的反射訪問的注釋
  • @ReflectionConfig - 直接模擬 GraalVM 反射配置 JSON 格式的可重復(fù)注解

@ReflectiveAccess 注釋通常用于特定類型、構(gòu)造函數(shù)、方法或字段,而后兩者通常用于模塊或應(yīng)用程序類以包含反射所需的類。例如,以下內(nèi)容來(lái)自帶有@TypeHint 的 Micronaut 的 Jackson 模塊:

使用@TypeHint 注解

@TypeHint(
    value = { (1)
        PropertyNamingStrategy.UpperCamelCaseStrategy.class,
        ArrayList.class,
        LinkedHashMap.class,
        HashSet.class
    },
    accessType = TypeHint.AccessType.ALL_DECLARED_CONSTRUCTORS (2)
)
  1. 值成員指定哪些類需要反射。

  2. accessType 成員指定是否只需要類加載訪問權(quán)限,或者是否需要對(duì)所有公共成員進(jìn)行完全反射。

或者使用可重復(fù)的 @ReflectionConfig 注釋,并允許每種類型進(jìn)行不同的配置:

使用@ReflectionConfig 注解

@ReflectionConfig(
    type = PropertyNamingStrategy.UpperCamelCaseStrategy.class,
    accessType = TypeHint.AccessType.ALL_DECLARED_CONSTRUCTORS
)
@ReflectionConfig(
    type = ArrayList.class,
    accessType = TypeHint.AccessType.ALL_DECLARED_CONSTRUCTORS
)
@ReflectionConfig(
    type = LinkedHashMap.class,
    accessType = TypeHint.AccessType.ALL_DECLARED_CONSTRUCTORS
)
@ReflectionConfig(
    type = HashSet.class,
    accessType = TypeHint.AccessType.ALL_DECLARED_CONSTRUCTORS
)

生成原生圖像

GraalVM 的 native-image 命令生成本機(jī)圖像。您可以手動(dòng)使用此命令生成您的本機(jī)映像。例如:

native-image 命令

native-image --class-path build/libs/hello-world-0.1-all.jar (1)
  1.  類路徑參數(shù)指的是 Micronaut 著色的 JAR

構(gòu)建映像后,使用本機(jī)映像名稱運(yùn)行應(yīng)用程序:

運(yùn)行本機(jī)應(yīng)用程序

$ ./hello-world
15:15:15.153 [main] INFO  io.micronaut.runtime.Micronaut - Startup completed in 14ms. Server Running: http://localhost:8080

如您所見,本機(jī)圖像啟動(dòng)在幾毫秒內(nèi)完成,并且內(nèi)存消耗不包括 JVM 的開銷(本機(jī) Micronaut 應(yīng)用程序僅使用 20mb 內(nèi)存運(yùn)行)。

資源文件生成

從 Micronaut 3.0 開始,自動(dòng)生成 resource-config.json 文件現(xiàn)在是 Gradle 和 Maven 插件的一部分。

GraalVM 和 Micronaut 常見問題解答

Micronaut 如何在 GraalVM 上運(yùn)行?

Micronaut 具有依賴注入和不使用反射的面向方面編程運(yùn)行時(shí)。這使得 Micronaut 應(yīng)用程序更容易在 GraalVM 上運(yùn)行,因?yàn)榇嬖诩嫒菪詥栴},尤其是在原生圖像中的反射方面。

如何讓使用 picocli 的 Micronaut 應(yīng)用程序在 GraalVM 上運(yùn)行?

Picocli 提供了一個(gè) picocli-codegen 模塊,其中包含一個(gè)用于生成 GraalVM 反射配置文件的工具。該工具可以作為構(gòu)建的一部分手動(dòng)或自動(dòng)運(yùn)行。該模塊的自述文件包含使用說(shuō)明和代碼片段,用于配置 Gradle 和 Maven 以在構(gòu)建過(guò)程中自動(dòng)生成 cli-reflect.json 文件。運(yùn)行 native-image 工具時(shí),將生成的文件添加到 -H:ReflectionConfigurationFiles 選項(xiàng)。

其他第三方庫(kù)呢?

Micronaut 不能保證第三方庫(kù)在 GraalVM SubstrateVM 上工作,這取決于每個(gè)單獨(dú)的庫(kù)來(lái)實(shí)現(xiàn)支持。

我得到一個(gè)“Class XXX is instantiated reflectively…”異常。我該怎么辦?

如果您收到如下錯(cuò)誤:

Class myclass.Foo[] is instantiated reflectively but was never registered. Register the class by using org.graalvm.nativeimage.RuntimeReflection

您可能需要手動(dòng)調(diào)整生成的 reflect.json 文件。對(duì)于常規(guī)類,您需要在數(shù)組中添加一個(gè)條目:

[
    {
        "name" : "myclass.Foo",
        "allDeclaredConstructors" : true
    },
    ...
]

對(duì)于數(shù)組,這必須使用 Java JVM 內(nèi)部數(shù)組表示。例如:

[
    {
        "name" : "[Lmyclass.Foo;",
        "allDeclaredConstructors" : true
    },
    ...
]

如果我想使用 -Xmx 設(shè)置堆的最大大小,但出現(xiàn) OutOfMemoryError 怎么辦?

如果您在用于構(gòu)建本機(jī)映像的 Dockerfile 中設(shè)置最大堆大小,您可能會(huì)遇到如下運(yùn)行時(shí)錯(cuò)誤:

java.lang.OutOfMemoryError: Direct buffer memory

問題是 Netty 嘗試使用 io.netty.allocator.pageSize 和 io.netty.allocator.maxOrder 的默認(rèn)設(shè)置為每個(gè)塊分配 16MB 的內(nèi)存:

int defaultChunkSize = DEFAULT_PAGE_SIZE << DEFAULT_MAX_ORDER; // 8192 << 11 = 16MB

最簡(jiǎn)單的解決方案是在 Dockerfile 的入口點(diǎn)中明確指定 io.netty.allocator.maxOrder。使用 -Xmx64m 的工作示例:

ENTRYPOINT ["/app/application", "-Xmx64m", "-Dio.netty.allocator.maxOrder=8"]

要更進(jìn)一步,您還可以嘗試使用 io.netty.allocator.numHeapArenas 或 io.netty.allocator.numDirectArenas。


以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)