跳到主要内容
版本: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 中找到它们。