Introducing GraalVM Native Images

GraalVM 本机映像提供了一种部署和运行 Java 应用程序的新方法。与 Java 虚拟机相比,本机映像可以在更小的内存占用空间中运行,并且启动速度快得多。 它们非常适合使用容器映像部署的应用程序,尤其适合与 “功能即服务” (FaaS) 平台结合使用时。 与为 JVM 编写的传统应用程序不同,GraalVM 本机映像应用程序需要预先处理才能创建可执行文件。这种预先处理涉及从其主要入口点静态分析应用程序代码。 GraalVM Native Image 是一款针对特定平台的完整可执行文件。您无需部署 Java Virtual Machine 来运行本机映像。

如果您只是想开始使用并试验 GraalVM,您可以跳至 “Developing Your First GraalVM Native Application” 部分,之后再返回此部分。

Key Differences with JVM Deployments

GraalVM Native Image 是预先生成的,这意味着本机应用程序和基于 JVM 的应用程序之间存在一些关键差异。主要差异如下:

  • 在生成 main 入口点的构建时对您的应用程序进行静态分析。

  • 在创建本机映像时无法访问的代码将被移除,并且不会成为可执行文件的一部分。

  • GraalVM 不会直接识别代码的动态元素,必须了解反射、资源、序列化和动态代理。

  • 应用程序类路径在构建时固定,并且无法更改。

  • 没有延迟类加载,在可执行文件中部署的所有内容都将在启动时加载到内存中。

  • 围绕一些 Java 应用程序的某些方面有一些限制,这些方面并未得到完全支持。

基于这些差异,Spring 使用一个称为 Spring Ahead-of-Time processing 的进程,这会带来进一步的限制。请务必至少阅读下一部分的开头,以了解这些限制。

GraalVM 参考文档中的 {url-graal-docs-native-image}/metadata/Compatibility/[本机映像兼容性指南] 部分提供了有关 GraalVM 限制的更多详细信息。

Understanding Spring Ahead-of-Time Processing

典型的 Spring Boot 应用程序是相当动态的,配置在运行时执行。事实上,Spring Boot 自动配置的概念在很大程度上依赖于对运行时状态做出反应,以便正确配置各项工作。

尽管可以将这些应用程序的动态方面告知 GraalVM,但这样做会消除静态分析的大部分好处。因此,在使用 Spring Boot 创建本机映像时,会假定一个封闭的世界,而应用程序的动态方面受到限制。

封闭世界假设暗示了,除了 the limitations created by GraalVM itself 之外,以下限制:

  • 应用程序中定义的 Bean 在运行时不可更改,这意味着:

    • Spring @Profile 注解和特定于概要文件的配置 have limitations

    • 如果创建 bean,则不会支持发生更改的属性(例如,@ConditionalOnProperty.enable 属性)。

当这些限制到位时,Spring 可以在构建时执行预先处理,并生成 GraalVM 可以使用的其他资。经过 Spring AOT 处理的应用程序通常会生成:

  • Java source code

  • 字节码(用于动态代理等)

  • GraalVM JSON hint files:

    • Resource hints (resource-config.json)

    • Reflection hints (reflect-config.json)

    • Serialization hints (serialization-config.json)

    • Java Proxy Hints (proxy-config.json)

    • JNI Hints (jni-config.json)

Source Code Generation

Spring 应用程序由 Spring Bean 组成。Spring Framework 在内部使用两个不同的概念来管理 bean。有 bean 实例,它们是已经创建的实际实例,并且可以注入到其他 bean 中。还有 bean 定义,用于定义 bean 的属性以及如何创建其实例。

如果我们取一个典型的 @Configuration 类:

bean 定义由解析 @Configuration 类和查找 @Bean 方法而创建。在以上示例中,我们为名为 myBean 的单例 bean 定义一个 BeanDefinition。我们还为 MyConfiguration 类本身创建一个 BeanDefinition

myBean 实例必需时,Spring 知道它必须调用 myBean() 方法并使用结果。在 JVM 上运行时,@Configuration 类解析会在您的应用程序启动时发生,而 @Bean 方法则会利用反射机制调用。

在创建一个本机映像时,Spring 将以不同的方式运作。它不会在运行时解析 @Configuration 类并生成 bean 定义,而是在构建时进行此操作。在 bean 定义被发现后,它们会被处理并转换为可以被 GraalVM 编译器分析的源代码。

Spring AOT 流程将以上配置类转换为类似这样的代码:

根据 Bean 定义的性质,生成的 exact 代码可能会有所不同。

您可以看到,上面生成是代码为 @Configuration 类创建了等效的 Bean 定义,但以 GraalVM 可以理解的直接方式进行。

存在一个适用于 myConfiguration bean 的 Bean 定义,以及一个适用于 myBean 的 Bean 定义。当需要一个 myBean 实例时,将调用一个 BeanInstanceSupplier。此供应商将在 myConfiguration bean 上调用 myBean() 方法。

在 Spring AOT 处理期间,你的应用程序将启动到 Bean 定义可用的地步。Bean 实例不会在 AOT 处理阶段创建。

Spring AOT 将为您的所有 Bean 定义生成类似这样的代码。当需要 Bean 后处理时,它还将生成代码(例如,调用 @Autowired 方法)。还将生成一个 ApplicationContextInitializer,Spring Boot 将使用它在 AOT 处理的应用程序实际上运行时初始化 ApplicationContext

虽然 AOT 生成的源代码冗长,但它非常易读,并且在调试应用程序时很有帮助。使用 Maven 时,可以在 target/spring-aot/main/sources 中找到生成的源文件,使用 Gradle 时可以在 build/generated/aotSources 中找到生成的源文件。

Hint File Generation

除了生成源文件外,Spring AOT 引擎还将生成 GraalVM 使用的提示文件。提示文件包含 JSON 数据,描述了 GraalVM 如何处理它不能通过直接检查代码理解的事物。

例如,您可能在私有方法中使用 Spring 注解。Spring 将需要使用反射来调用私有方法,即使是在 GraalVM 上也是如此。当出现这种情况时,Spring 可以编写一个反射提示,以便 GraalVM 知道即使没有直接调用私有方法,它仍然需要在本地映像中可用。

提示文件在 META-INF/native-image 下生成,GraalVM 会在那里自动选取它们。

使用 Maven 时,可以在 target/spring-aot/main/resources 中找到生成的提示文件,使用 Gradle 时可以在 build/generated/aotResources 中找到生成的提示文件。

Proxy Class Generation

Spring 有时需要生成代理类以使用其他功能来增强您编写的代码。为此,它使用了直接生成字节码的 cglib 库。

当应用程序在 JVM 上运行时,代理类会在应用程序运行时动态生成。在创建一个本地映像时,这些代理需要在构建时创建,以便它们可以被 GraalVM 包含。

与源码生成不同,在调试程序时, 生成的字节码并不是特别有帮助。但是,如果你需要使用诸如 javap 之类的工具检查 .class 文件的内容,则可以在 Maven 中找到 target/spring-aot/main/classes 和在 Gradle 中找到 build/generated/aotClasses