介绍 GraalVM Native Images
GraalVM Native Images 提供了一种新的方式来部署和运行 Java 应用程序。 与 Java Virtual Machine 相比,native images 可以以更小的内存占用和更快的启动时间运行。
它们非常适合使用容器镜像部署的应用程序,特别是与"Function as a service"(FaaS)平台结合使用时特别有趣。
与为 JVM 编写的传统应用程序不同,GraalVM Native Image 应用程序需要提前处理才能创建可执行文件。 这种提前处理涉及从主入口点静态分析您的应用程序代码。
GraalVM Native Image 是一个完整的、特定于平台的可执行文件。 您不需要提供 Java Virtual Machine 来运行 native image。
如果您只想开始尝试 GraalVM,可以直接跳转到 开发你的第一个 GraalVM Native 应用 部分,稍后再回到本节。 |
与 JVM 部署的主要区别
GraalVM Native Images 是提前生成的事实意味着 native 和基于 JVM 的应用程序之间存在一些关键区别。 主要区别是:
-
在构建时从
main
入口点对您的应用程序进行静态分析。 -
在创建 native image 时无法访问的代码将被删除,不会成为可执行文件的一部分。
-
GraalVM 不能直接了解代码的动态元素,必须被告知反射、资源、序列化和动态代理。
-
应用程序类路径在构建时是固定的,不能更改。
-
没有延迟类加载,可执行文件中包含的所有内容都将在启动时加载到内存中。
-
Java 应用程序的某些方面存在一些限制,这些方面尚未完全支持。
除了这些差异之外,Spring 使用了一个称为 Spring Ahead-of-Time processing 的过程,这带来了进一步的限制。 请确保至少阅读下一节的开头部分以了解这些限制。
GraalVM 参考文档的 Native Image Compatibility Guide 部分提供了有关 GraalVM 限制的更多详细信息。 |
理解 Spring Ahead-of-Time Processing
典型的 Spring Boot 应用程序非常动态,配置在运行时执行。 事实上,Spring Boot 自动配置的概念在很大程度上依赖于对运行时状态的反应,以便正确配置事物。
虽然可以告诉 GraalVM 关于应用程序的这些动态方面,但这样做会抵消静态分析的大部分好处。 因此,当使用 Spring Boot 创建 native images 时,假设一个封闭的世界,并限制应用程序的动态方面。
封闭世界假设意味着,除了 GraalVM 本身创建的限制之外,还有以下限制:
-
应用程序中定义的 beans 不能在运行时更改,这意味着:
-
不支持在创建 bean 时更改的属性(例如,
@ConditionalOnProperty
和.enabled
属性)。
当这些限制到位时,Spring 可以在构建时执行提前处理并生成 GraalVM 可以使用的额外资源。 Spring AOT 处理的应用程序通常会生成:
-
Java 源代码
-
字节码(用于动态代理等)
-
GraalVM JSON 提示文件在
META-INF/native-image/{groupId}/{artifactId}/
中:-
资源提示(
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();
}
}
bean 定义是通过解析 @Configuration
类并查找 @Bean
方法创建的。
在上面的示例中,我们为名为 myBean
的单例 bean 定义了一个 BeanDefinition
。
我们还为 MyConfiguration
类本身创建了一个 BeanDefinition
。
当需要 myBean
实例时,Spring 知道它必须调用 myBean()
方法并使用结果。
在 JVM 上运行时,@Configuration
类解析在应用程序启动时发生,@Bean
方法使用反射调用。
创建 native image 时,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
。
这个供应商将在 myConfiguration
bean 上调用 myBean()
方法。
在 Spring AOT 处理期间,您的应用程序会启动到 bean 定义可用的程度。 在 AOT 处理阶段不会创建 bean 实例。 |
Spring AOT 将为所有 bean 定义生成类似这样的代码。
当需要 bean 后处理时(例如,调用 @Autowired
方法),它也会生成代码。
还会生成一个 ApplicationContextInitializer
,Spring Boot 将使用它来初始化 ApplicationContext
,当 AOT 处理的应用程序实际运行时。
虽然 AOT 生成的源代码可能很冗长,但它相当可读,在调试应用程序时可能会有帮助。
使用 Maven 时,生成的源文件可以在 target/spring-aot/main/sources 中找到,使用 Gradle 时在 build/generated/aotSources 中找到。
|
提示文件生成
除了生成源文件外,Spring AOT 引擎还会生成 GraalVM 使用的提示文件。 提示文件包含 JSON 数据,描述 GraalVM 应该如何处理它无法通过直接检查代码理解的内容。
例如,您可能在私有方法上使用 Spring 注解。 Spring 需要使用反射来调用私有方法,即使在 GraalVM 上也是如此。 当出现这种情况时,Spring 可以写入反射提示,以便 GraalVM 知道即使私有方法没有被直接调用,它仍然需要在 native image 中可用。
提示文件在 META-INF/native-image
下生成,GraalVM 会自动获取它们。
使用 Maven 时,生成的提示文件可以在 target/spring-aot/main/resources 中找到,使用 Gradle 时在 build/generated/aotResources 中找到。
|
代理类生成
Spring 有时需要生成代理类来增强您编写的代码的附加功能。 为此,它使用 cglib 库直接生成字节码。
当应用程序在 JVM 上运行时,代理类会在应用程序运行时动态生成。 创建 native image 时,这些代理需要在构建时创建,以便 GraalVM 可以包含它们。
与源代码生成不同,生成的字节码在调试应用程序时并不是特别有帮助。
但是,如果您需要使用 javap 等工具检查 .class 文件的内容,可以在 Maven 的 target/spring-aot/main/classes 和 Gradle 的 build/generated/aotClasses 中找到它们。
|