跳到主要内容
版本:4.0.2

介绍 GraalVM 原生镜像

QWen Max 中英对照 Introducing GraalVM Native Images

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

它们非常适合使用容器镜像部署的应用程序,尤其是在与“Function as a service”(FaaS)平台结合时尤为有趣。

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

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

提示

如果你只是想开始体验 GraalVM,可以直接跳转到 开发你的第一个 GraalVM 原生应用 章节,稍后再回来看本节。

与 JVM 部署的关键差异​

GraalVM 原生镜像(Native Images)是提前生成的,这意味着原生应用与基于 JVM 的应用之间存在一些关键差异。主要差异包括:

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

  • 在创建原生镜像时无法到达的代码将被移除,并且不会包含在可执行文件中。

  • GraalVM 无法直接感知代码中的动态元素,必须明确告知其关于反射、资源、序列化和动态代理的信息。

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

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

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

除了上述差异之外,Spring 还使用了一种称为 Spring Ahead-of-Time processing 的过程,这会带来进一步的限制。请务必至少阅读下一节的开头部分,以了解这些限制。

提示

GraalVM 参考文档中的 Native Image 兼容性指南 章节提供了有关 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 Beans 组成。在内部,Spring Framework 使用两个不同的概念来管理 beans。一种是 bean 实例,即已经创建并可以注入到其他 beans 中的实际实例。另一种是 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();
}

}

通过解析 @Configuration 类并找到 @Bean 方法来创建 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;
}

}
备注

根据你的 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 使用的提示文件。提示文件包含 JSON 数据,用于描述 GraalVM 应如何处理那些无法通过直接检查代码理解的内容。

例如,你可能在私有方法上使用了 Spring 注解。即使在 GraalVM 上,Spring 也需要使用反射来调用私有方法。当出现这种情况时,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 中找到它们。