跳到主要内容

介绍 GraalVM 原生镜像

DeepSeek V3 中英对照 Introducing GraalVM Native Images

GraalVM 原生镜像为部署和运行 Java 应用程序提供了一种新的方式。与 Java 虚拟机相比,原生镜像可以在更小的内存占用下运行,并且启动时间更快。

它们非常适合使用容器镜像部署的应用程序,在与“功能即服务”(FaaS)平台结合使用时尤其引人注目。

与传统为 JVM 编写的应用程序不同,GraalVM Native Image 应用程序需要进行提前处理以创建可执行文件。这种提前处理涉及从应用程序的主入口点对代码进行静态分析。

GraalVM Native Image 是一个完整的、平台特定的可执行文件。你不需要附带 Java 虚拟机来运行一个原生镜像。

提示

如果您只想快速入门并尝试 GraalVM,可以直接跳转到 开发您的第一个 GraalVM 原生应用 部分,稍后再回到此部分。

与 JVM 部署的主要区别

GraalVM 原生镜像提前生成的事实意味着原生应用与基于 JVM 的应用之间存在一些关键差异。主要差异包括:

  • 您的应用程序的静态分析在构建时从 main 入口点执行。

  • 在创建原生镜像时无法访问的代码将被移除,不会成为可执行文件的一部分。

  • GraalVM 不会直接识别代码中的动态元素,必须明确告知关于反射、资源、序列化和动态代理的内容。

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

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

  • Java 应用程序的某些方面存在一些限制,尚未得到完全支持。

除了这些差异之外,Spring 还使用了一个称为 Spring Ahead-of-Time 处理的过程,这进一步带来了一些限制。请确保至少阅读下一节的开头部分,以了解这些限制。

提示

原生镜像兼容性指南 部分的 GraalVM 参考文档提供了关于 GraalVM 限制的更多详细信息。

理解 Spring 的提前处理

典型的 Spring Boot 应用程序非常动态,配置在运行时进行。事实上,Spring Boot 自动配置的概念在很大程度上依赖于对运行时状态的反应,以便正确配置事物。

尽管向 GraalVM 告知应用程序的这些动态特性是可行的,但这样做将抵消静态分析的大部分优势。因此,在使用 Spring Boot 创建原生镜像时,假设了一个封闭世界,并对应用程序的动态特性进行了限制。

在封闭世界假设下,除了 GraalVM 自身带来的限制 之外,还存在以下限制:

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

当这些限制生效时,Spring 就可以在构建时进行提前处理,并生成 GraalVM 可以使用的额外资源。经过 Spring AOT 处理的应用程序通常会生成:

  • Java 源代码

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

  • META-INF/native-image/{groupId}/{artifactId}/ 目录下的 GraalVM JSON 提示文件:

    • 资源提示 (resource-config.json)

    • 反射提示 (reflect-config.json)

    • 序列化提示 (serialization-config.json)

    • Java 代理提示 (proxy-config.json)

    • JNI 提示 (jni-config.json)

如果生成的提示信息不够充分,您也可以提供自己的提示

源代码生成

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

如果我们以一个典型的 @Configuration 类为例:

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration(proxyBeanMethods = false)
public class MyConfiguration {

@Bean
public MyBean myBean() {
return new MyBean();
}

}
java

Bean 定义是通过解析 @Configuration 类并查找其中的 @Bean 方法创建的。在上面的示例中,我们为名为 myBean 的单例 Bean 定义了一个 BeanDefinition。我们还为 MyConfiguration 类本身创建了一个 BeanDefinition

当需要 myBean 实例时,Spring 知道它必须调用 myBean() 方法并使用其结果。在 JVM 上运行时,应用程序启动时会解析 @Configuration 类,并且 @Bean 方法会通过反射被调用。

在创建原生镜像时,Spring 的工作方式有所不同。它不是在运行时解析 @Configuration 类并生成 bean 定义,而是在构建时完成这些操作。一旦发现了 bean 定义,它们会被处理并转换为源代码,这些源代码可以被 GraalVM 编译器分析。

Spring AOT 过程会将上面的配置类转换为如下代码:

import org.springframework.beans.factory.aot.BeanInstanceSupplier;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.support.RootBeanDefinition;

/**
* Bean definitions for {@link MyConfiguration}.
*/
public class MyConfiguration__BeanDefinitions {

/**
* Get the bean definition for 'myConfiguration'.
*/
public static BeanDefinition getMyConfigurationBeanDefinition() {
Class<?> beanType = MyConfiguration.class;
RootBeanDefinition beanDefinition = new RootBeanDefinition(beanType);
beanDefinition.setInstanceSupplier(MyConfiguration::new);
return beanDefinition;
}

/**
* Get the bean instance supplier for 'myBean'.
*/
private static BeanInstanceSupplier<MyBean> getMyBeanInstanceSupplier() {
return BeanInstanceSupplier.<MyBean>forFactoryMethod(MyConfiguration.class, "myBean")
.withGenerator((registeredBean) -> registeredBean.getBeanFactory().getBean(MyConfiguration.class).myBean());
}

/**
* Get the bean definition for 'myBean'.
*/
public static BeanDefinition getMyBeanBeanDefinition() {
Class<?> beanType = MyBean.class;
RootBeanDefinition beanDefinition = new RootBeanDefinition(beanType);
beanDefinition.setInstanceSupplier(getMyBeanInstanceSupplier());
return beanDefinition;
}

}
java
备注

生成的代码可能会根据你的 bean 定义的性质而有所不同。

如上所示,生成的代码创建了与 @Configuration 类等效的 Bean 定义,但以一种 GraalVM 可以直接理解的方式实现。

有一个 myConfiguration bean 的定义,还有一个 myBean 的定义。当需要一个 myBean 实例时,会调用 BeanInstanceSupplier。这个 supplier 会调用 myConfiguration bean 上的 myBean() 方法。

备注

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

Spring AOT 会为所有的 Bean 定义生成类似的代码。当需要进行 Bean 后处理时(例如,调用 @Autowired 方法),它也会生成相应的代码。此外,还会生成一个 ApplicationContextInitializer,Spring Boot 会在实际运行经过 AOT 处理的应用程序时使用它来初始化 ApplicationContext

提示

尽管 AOT 生成的源码可能较为冗长,但它相当易读,并且在调试应用程序时非常有用。生成的源文件在使用 Maven 时可以在 target/spring-aot/main/sources 中找到,而在使用 Gradle 时则位于 build/generated/aotSources 中。

提示文件生成

除了生成源文件外,Spring AOT 引擎还会生成 GraalVM 使用的提示文件(hint files)。提示文件中包含 JSON 数据,这些数据描述了 GraalVM 应如何处理那些它无法通过直接检查代码理解的内容。

例如,你可能在私有方法上使用了 Spring 注解。Spring 需要使用反射来调用私有方法,即使在 GraalVM 上也是如此。当这种情况发生时,Spring 可以编写一个反射提示,以便 GraalVM 知道,即使私有方法没有被直接调用,它仍然需要在原生镜像中可用。

提示文件会在 META-INF/native-image 目录下生成,GraalVM 会自动识别这些文件。

提示

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

代理类生成

有时候,Spring 需要生成代理类来为你编写的代码增强额外的功能。为此,它使用了 cglib 库,该库直接生成字节码。

当一个应用程序在 JVM 上运行时,代理类会在应用程序运行时动态生成。在创建原生镜像时,这些代理需要在构建时生成,以便 GraalVM 能够将它们包含在内。

备注

与源代码生成不同,生成的字节码在调试应用程序时并不是特别有帮助。然而,如果你需要使用诸如 javap 这样的工具来检查 .class 文件的内容,你可以在 Maven 的 target/spring-aot/main/classes 目录或 Gradle 的 build/generated/aotClasses 目录中找到它们。