阅读(4734) (0)

Micronaut GraalVM 支持

2023-02-23 13:53:19 更新

GraalVM 是来自 Oracle 的新型通用虚拟机,它支持多语言运行时环境以及将 Java 应用程序编译为本地机器代码的能力。

任何 Micronaut 应用程序都可以使用 GraalVM JVM 运行,但是已向 Micronaut 添加了特殊支持以支持使用 GraalVM 的本机图像工具运行 Micronaut 应用程序。

Micronaut 目前支持 GraalVM 版本 22.0.0.2,团队正在改进每个新版本的支持。但是,如果您发现任何问题,请不要犹豫,报告问题。

Micronaut 的许多模块和第三方库已经过验证可以与 GraalVM 一起工作:HTTP 服务器、HTTP 客户端、Function 支持、Micronaut Data JDBC 和 JPA、Service Discovery、RabbitMQ、Views、Security、Zipkin 等。对其他模块的支持是不断发展,并将随着时间的推移而改进。

入门

仅在 Java 或 Kotlin 项目中支持使用 GraalVM 的原生图像工具。 Groovy 严重依赖于 GraalVM 仅部分支持的反射。

要开始使用 GraalVM,请首先通过入门说明或使用 Sdkman! 安装 GraalVM SDK。

作为 GraalVM 原生镜像的微服务

开始使用 Micronaut 和 GraalVM

从 Micronaut 2.2 开始,任何 Micronaut 应用程序都可以使用 Micronaut Gradle 或 Maven 插件构建到原生镜像中。首先,创建一个新的应用程序:

创建 GraalVM 原生微服务

$ mn create-app hello-world

您可以使用 --build maven 进行 Maven 构建。

使用 Docker 构建原生镜像

要使用 Gradle 和 Docker 构建您的本机映像,请运行:

使用 Docker 和 Gradle 构建原生镜像

$ ./gradlew dockerBuildNative

要使用 Maven 和 Docker 构建您的本机映像,请运行:

使用 Docker 和 Maven 构建原生镜像

$ ./mvnw package -Dpackaging=docker-native

不使用 Docker 构建原生镜像

要在不使用 Docker 的情况下构建本机映像,请通过入门说明或使用 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

本机图像工具是从基础 GraalVM 发行版中提取的,可作为插件使用。要安装它,请运行:

安装本机图像工具

$ gu install native-image

现在,您可以通过运行 nativeCompile 任务来使用 Gradle 构建原生镜像:

使用 Gradle 创建原生镜像

$ ./gradlew nativeCompile

本机映像将构建在 build/native/nativeCompile 目录中。

要使用 Maven 和 Micronaut Maven 插件创建原生图像,请使用原生图像打包格式:

使用 Maven 创建原生镜像

$ ./mvnw package -Dpackaging=native-image

在目标目录中构建本机图像。

然后,您可以从构建它的目录运行本机映像。

运行本机图像

$ ./hello-world

了解 Micronaut 和 GraalVM

Micronaut 本身不依赖于反射或动态类加载,因此它会自动与 GraalVM 本机一起工作,但是 Micronaut 使用的某些第三方库可能需要额外输入有关反射使用的信息。

Micronaut 包含一个注解处理器,它有助于生成由原生图像工具自动获取的反射配置:

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

该处理器生成额外的类来实现 GraalReflectionConfigurer 接口并以编程方式注册反射配置。

例如下面的类:

package example;

import io.micronaut.core.annotation.ReflectiveAccess;

@ReflectiveAccess
class Test {
    ...
}

上面的示例导致 example.Test 的公共方法、声明的字段和声明的构造函数被注册为反射访问。

如果您有更高级的要求并且只希望包含某些字段或方法,请在任何构造函数、字段或方法上使用注释以仅包含特定字段、构造函数或方法。

为反射访问添加额外的类

为了通知 Micronaut 要包含在生成的反射配置中的其他类,可以使用许多注释,包括:

  • @ReflectiveAccess - 可以在特定类型、构造函数、方法或字段上声明的注释,以仅对带注释的元素启用反射访问。
  • @TypeHint - 允许批量配置对一种或多种类型的反射访问的注释
  • @ReflectionConfig - 直接模拟 GraalVM 反射配置 JSON 格式的可重复注解

@ReflectiveAccess 注释通常用于特定类型、构造函数、方法或字段,而后两者通常用于模块或应用程序类以包含反射所需的类。例如,以下内容来自带有@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 成员指定是否只需要类加载访问权限,或者是否需要对所有公共成员进行完全反射。

或者使用可重复的 @ReflectionConfig 注释,并允许每种类型进行不同的配置:

使用@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 命令生成本机图像。您可以手动使用此命令生成您的本机映像。例如:

native-image 命令

native-image --class-path build/libs/hello-world-0.1-all.jar (1)
  1.  类路径参数指的是 Micronaut 着色的 JAR

构建映像后,使用本机映像名称运行应用程序:

运行本机应用程序

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

如您所见,本机图像启动在几毫秒内完成,并且内存消耗不包括 JVM 的开销(本机 Micronaut 应用程序仅使用 20mb 内存运行)。

资源文件生成

从 Micronaut 3.0 开始,自动生成 resource-config.json 文件现在是 Gradle 和 Maven 插件的一部分。

GraalVM 和 Micronaut 常见问题解答

Micronaut 如何在 GraalVM 上运行?

Micronaut 具有依赖注入和不使用反射的面向方面编程运行时。这使得 Micronaut 应用程序更容易在 GraalVM 上运行,因为存在兼容性问题,尤其是在原生图像中的反射方面。

如何让使用 picocli 的 Micronaut 应用程序在 GraalVM 上运行?

Picocli 提供了一个 picocli-codegen 模块,其中包含一个用于生成 GraalVM 反射配置文件的工具。该工具可以作为构建的一部分手动或自动运行。该模块的自述文件包含使用说明和代码片段,用于配置 Gradle 和 Maven 以在构建过程中自动生成 cli-reflect.json 文件。运行 native-image 工具时,将生成的文件添加到 -H:ReflectionConfigurationFiles 选项。

其他第三方库呢?

Micronaut 不能保证第三方库在 GraalVM SubstrateVM 上工作,这取决于每个单独的库来实现支持。

我得到一个“Class XXX is instantiated reflectively…”异常。我该怎么办?

如果您收到如下错误:

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

您可能需要手动调整生成的 reflect.json 文件。对于常规类,您需要在数组中添加一个条目:

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

对于数组,这必须使用 Java JVM 内部数组表示。例如:

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

如果我想使用 -Xmx 设置堆的最大大小,但出现 OutOfMemoryError 怎么办?

如果您在用于构建本机映像的 Dockerfile 中设置最大堆大小,您可能会遇到如下运行时错误:

java.lang.OutOfMemoryError: Direct buffer memory

问题是 Netty 尝试使用 io.netty.allocator.pageSize 和 io.netty.allocator.maxOrder 的默认设置为每个块分配 16MB 的内存:

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

最简单的解决方案是在 Dockerfile 的入口点中明确指定 io.netty.allocator.maxOrder。使用 -Xmx64m 的工作示例:

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

要更进一步,您还可以尝试使用 io.netty.allocator.numHeapArenas 或 io.netty.allocator.numDirectArenas。