|
如需获取最新稳定版本,请使用 Spring Boot 4.0.4! |
介绍 GraalVM 原生镜像
GraalVM 原生镜像为部署和运行 Java 应用程序提供了一种新方法。 与 Java 虚拟机相比,原生镜像可以以更小的内存占用和更快的启动时间运行。
它们非常适合使用容器镜像部署的应用程序,在与“函数即服务”(FaaS)平台结合使用时尤其引人注目。
与为JVM编写的传统应用程序不同,GraalVM Native Image 应用程序需要在创建可执行文件之前进行预先处理。 这种预先处理涉及从应用程序的主入口点对其代码进行静态分析。
GraalVM 原生镜像是一个完整的、特定于平台的可执行文件。 运行原生镜像时,无需附带 Java 虚拟机。
| 如果您只是想快速开始并尝试 GraalVM,可以先跳转到开发您的第一个 GraalVM 原生应用程序章节,之后再回到本章节。 |
与 JVM 部署的主要区别
GraalVM 原生镜像在运行前就已生成,这意味着原生应用与基于 JVM 的应用之间存在一些关键差异。 主要差异包括:
-
在构建时,从
main入口点对您的应用程序执行静态分析。 -
在创建原生镜像时无法访问的代码将被移除,不会包含在可执行文件中。
-
GraalVM 无法直接感知代码中的动态元素,必须显式告知其有关反射、资源、序列化和动态代理的信息。
-
应用程序类路径在构建时固定,无法更改。
-
不存在延迟类加载,可执行文件中包含的所有内容都会在启动时加载到内存中。
-
Java 应用程序的某些方面存在一些限制,尚未得到完全支持。
除了这些差异之外,Spring 还使用了一种称为 Spring 提前处理(Ahead-of-Time processing) 的流程,这带来了更多的限制。 请务必至少阅读下一节的开头部分以了解这些限制。
| GraalVM 参考文档中的 原生镜像兼容性指南 部分提供了有关 GraalVM 限制的更多详细信息。 |
理解 Spring 的提前处理(Ahead-of-Time Processing)
典型的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 框架使用两个不同的概念来管理 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();
}
}
Bean定义是通过解析@Configuration类并找到@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 中找到。 |
代理类生成
Spring 有时需要生成代理类,以通过附加功能增强您编写的代码。 为此,它使用 cglib 库,该库直接生成字节码。
当应用程序在 JVM 上运行时,代理类会在应用程序运行期间动态生成。 在创建原生镜像时,这些代理需要在构建时生成,以便 GraalVM 能够将其包含在内。
与源代码生成不同,生成的字节码在调试应用程序时并不特别有用。
不过,如果您需要使用诸如 javap 之类的工具来检查 .class 文件的内容,则可以在 Maven 的 target/spring-aot/main/classes 和 Gradle 的 build/generated/aotClasses 中找到它们。 |