GraalVM 原生镜像简介
GraalVM Native Images 提供了一种部署和运行 Java 应用程序的新方式。与 Java 虚拟机相比,原生镜像可以以更小的内存占用和更快的启动时间运行。
它们非常适合使用容器镜像部署的应用程序,并且与“函数即服务”(FaaS)平台结合使用时特别有意义。
与为 JVM 编写的传统应用程序不同,GraalVM Native Image 应用程序需要提前处理才能创建可执行文件。这种提前处理涉及到从其主要入口点对应用程序代码进行静态分析。
GraalVM Native Image 是一个完整的、平台特定的可执行文件。您无需附带 Java 虚拟机即可运行原生镜像。
| 如果您只想开始尝试 GraalVM,可以直接跳转到开发您的第一个 GraalVM Native 应用程序部分,稍后再返回此部分。 |
与 JVM 部署的主要区别
GraalVM Native Images 是提前生成的,这意味着原生应用程序和基于 JVM 的应用程序之间存在一些关键区别。主要区别在于:
-
在构建时从
main入口点执行应用程序的静态分析。 -
在创建原生镜像时无法到达的代码将被移除,并且不会成为可执行文件的一部分。
-
GraalVM 不直接感知代码的动态元素,必须告知其关于反射、资源、序列化和动态代理的信息。
-
应用程序类路径在构建时固定,无法更改。
-
没有惰性类加载,可执行文件中包含的所有内容都会在启动时加载到内存中。
-
Java 应用程序的某些方面存在一些限制,这些方面不完全支持。
除了这些差异之外,Spring 还使用了一个名为Spring 提前处理的过程,这施加了进一步的限制。请务必阅读下一节的开头部分以了解这些限制。
| GraalVM 参考文档的Native Image 兼容性指南部分提供了关于 GraalVM 限制的更多详细信息。 |
理解 Spring 提前处理
典型的 Spring Boot 应用程序是相当动态的,配置在运行时执行。事实上,Spring Boot 自动配置的概念严重依赖于对运行时状态的响应以正确配置事物。
尽管可以将这些应用程序的动态方面告知 GraalVM,但这样做会抵消静态分析的大部分好处。因此,在使用 Spring Boot 创建原生镜像时,假设一个封闭世界,并且限制了应用程序的动态方面。
除了GraalVM 本身创建的限制之外,封闭世界假设还意味着以下限制:
-
应用程序中定义的 bean 不能在运行时更改,这意味着:
-
不支持如果创建 bean 而更改的属性(例如,
@ConditionalOnProperty和.enabled属性)。
当这些限制到位时,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 Framework 使用两个不同的概念来管理 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();
}
}
通过解析 @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 的 bean 定义,以及一个 myBean 的 bean 定义。当需要 myBean 实例时,会调用 BeanInstanceSupplier。此供应商将调用 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 注解。Spring 将需要使用反射来调用私有方法,即使是在 GraalVM 上。当出现这种情况时,Spring 可以写入一个反射提示,以便 GraalVM 知道即使私有方法没有被直接调用,它仍然需要在原生镜像中可用。
提示文件在 META-INF/native-image 下生成,GraalVM 会自动识别并加载它们。
在使用 Maven 时,生成的提示文件位于 target/spring-aot/main/resources 中;使用 Gradle 时,位于 build/generated/aotResources 中。 |