此版本仍在开发中,尚未被视为稳定版本。对于最新的稳定版本,请使用 Spring Boot 3.4.3! |
GraalVM 原生镜像简介
GraalVM 原生映像提供了一种部署和运行 Java 应用程序的新方法。 与 Java 虚拟机相比,本机映像可以以更小的内存占用和更快的启动时间运行。
它们非常适合使用容器映像部署的应用程序,在与“功能即服务”(FaaS) 平台结合使用时尤其有趣。
与为 JVM 编写的传统应用程序不同,GraalVM Native Image 应用程序需要提前处理才能创建可执行文件。 这种预先处理涉及从应用程序代码的主入口点静态分析应用程序代码。
GraalVM Native Image 是一个完整的、特定于平台的可执行文件。 您无需提供 Java 虚拟机即可运行本机映像。
如果您只想开始并试用 GraalVM,可以跳转到开发您的第一个 GraalVM 原生应用程序部分,稍后再返回此部分。 |
与 JVM 部署的主要区别
GraalVM Native Images 是提前生成的,这意味着原生应用和基于 JVM 的应用之间存在一些关键差异。 主要区别在于:
-
应用程序的静态分析在构建时从
main
入口点。 -
创建本机映像时无法访问的代码将被删除,并且不会成为可执行文件的一部分。
-
GraalVM 无法直接了解代码的动态元素,必须了解反射、资源、序列化和动态代理。
-
应用程序 Classpath 在构建时是固定的,无法更改。
-
没有延迟类加载,可执行文件中附带的所有内容都将在启动时加载到内存中。
-
Java 应用程序的某些方面存在一些不完全支持的限制。
除了这些差异之外, Spring 还使用了一个称为 Spring Ahead-of-Time processing 的过程,这施加了进一步的限制。 请务必至少阅读下一部分的开头部分以了解这些内容。
GraalVM 参考文档的本机映像兼容性指南部分提供了有关 GraalVM 限制的更多详细信息。 |
了解 Spring 预先处理
典型的 Spring Boot 应用程序是相当动态的,配置是在运行时执行的。 事实上, Spring Boot 自动配置的概念在很大程度上取决于对运行时的状态做出反应,以便正确配置。
尽管可以告诉 GraalVM 应用程序的这些动态方面,但这样做会抵消静态分析的大部分好处。 因此,当使用 Spring Boot 创建本机映像时,会假定一个封闭世界,并且应用程序的动态方面受到限制。
除了 GraalVM 本身产生的限制外,封闭世界假设还意味着以下限制:
-
应用程序中定义的 bean 在运行时不能更改,这意味着:
-
不支持在创建 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 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();
}
}
The bean definition is created by parsing the @Configuration
class and finding the @Bean
methods.
In the above example, we’re defining a BeanDefinition
for a singleton bean named myBean
.
We’re also creating a BeanDefinition
for the MyConfiguration
class itself.
When the myBean
instance is required, Spring knows that it must invoke the myBean()
method and use the result.
When running on the JVM, @Configuration
class parsing happens when your application starts and @Bean
methods are invoked using reflection.
When creating a native image, Spring operates in a different way.
Rather than parsing @Configuration
classes and generating bean definitions at runtime, it does it at build-time.
Once the bean definitions have been discovered, they are processed and converted into source code that can be analyzed by the GraalVM compiler.
The Spring AOT process would convert the configuration class above to code like this:
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;
}
}
The exact code generated may differ depending on the nature of your bean definitions.
You can see above that the generated code creates equivalent bean definitions to the @Configuration
class, but in a direct way that can be understood by GraalVM.
There is a bean definition for the myConfiguration
bean, and one for myBean
.
When a myBean
instance is required, a BeanInstanceSupplier
is called.
This supplier will invoke the myBean()
method on the myConfiguration
bean.
During Spring AOT processing, your application is started up to the point that bean definitions are available.
Bean instances are not created during the AOT processing phase.
Spring AOT will generate code like this for all your bean definitions.
It will also generate code when bean post-processing is required (for example, to call @Autowired
methods).
An ApplicationContextInitializer
will also be generated which will be used by Spring Boot to initialize the ApplicationContext
when an AOT processed application is actually run.
Although AOT generated source code can be verbose, it is quite readable and can be helpful when debugging an application.
Generated source files can be found in target/spring-aot/main/sources
when using Maven and build/generated/aotSources
with Gradle.
Hint File Generation
In addition to generating source files, the Spring AOT engine will also generate hint files that are used by GraalVM.
Hint files contain JSON data that describes how GraalVM should deal with things that it can’t understand by directly inspecting the code.
For example, you might be using a Spring annotation on a private method.
Spring will need to use reflection in order to invoke private methods, even on GraalVM.
When such situations arise, Spring can write a reflection hint so that GraalVM knows that even though the private method isn’t called directly, it still needs to be available in the native image.
Hint files are generated under META-INF/native-image
where they are automatically picked up by GraalVM.
Generated hint files can be found in target/spring-aot/main/resources
when using Maven and build/generated/aotResources
with Gradle.
Proxy Class Generation
Spring sometimes needs to generate proxy classes to enhance the code you’ve written with additional features.
To do this, it uses the cglib library which directly generates bytecode.
When an application is running on the JVM, proxy classes are generated dynamically as the application runs.
When creating a native image, these proxies need to be created at build-time so that they can be included by GraalVM.
Unlike source code generation, generated bytecode isn’t particularly helpful when debugging an application.
However, if you need to inspect the contents of the .class
files using a tool such as javap
you can find them in target/spring-aot/main/classes
for Maven and build/generated/aotClasses
for Gradle.