docs.spring.io/spring-boot…
GraalVM原生镜像是独立的可执行文件,可以通过提前处理编译后的Java应用程序生成。本机镜像通常具有更小的内存占用,并且启动速度比对应的JVM更快。
介绍GraalVM原生镜像
GraalVM本地镜像提供了一种部署和运行Java应用程序的新方法。与Java虚拟机相比,本地镜像占用的内存更少,启动时间也更快。
它们非常适合使用容器镜像部署的应用程序,并且在与“功能即服务”(FaaS)平台相结合时特别有趣。
与为JVM编写的传统应用程序不同,GraalVM本机镜像应用程序需要提前处理以创建可执行文件。这种提前处理涉及从应用程序的主入口点静态分析应用程序代码。
GraalVM原生映像是一个完整的、特定于平台的可执行文件。运行本地镜像不需要安装Java虚拟机。
提示:
如果你只是想开始试用GraalVM,你可以跳到“开发你的第一个GraalVM原生应用程序”一节,然后再回到这一节。
与JVM部署的主要区别
GraalVM本地镜像是提前生成的,这意味着本地应用程序和基于JVM的应用程序之间有一些关键的区别。主要的区别是:
- 应用程序的静态分析是在构建时从主入口点执行的。
- 创建本机镜像时无法访问的代码将被删除,并且不会成为可执行文件的一部分。
- GraalVM不能直接感知代码的动态元素,必须被告知反射、资源、序列化和动态代理。
- 应用程序类路径在构建时是固定的,不能更改。
- 没有惰性类加载,可执行文件中的所有内容都会在启动时加载到内存中。
- Java应用程序的某些方面还存在一些不完全支持的限制。
提示:
GraalVM参考文档的原生镜像兼容性指南部分提供了有关GraalVM限制的更多详细信息。
理解Spring提前处理
典型的Spring Boot应用程序非常动态,配置是在运行时执行的。事实上,Spring Boot自动配置的概念很大程度上依赖于对运行时状态的反应,才能正确配置。
虽然可以告诉GraalVM应用程序的这些动态方面,但这样做会抵消静态分析的大部分好处。因此,当使用Spring Boot创建原生映像时,会假定应用程序是封闭的,并且限制应用程序的动态方面。
封闭世界的假设意味着以下限制:
-
类路径是固定的,并且在构建时被完全定义
-
应用程序中定义的bean不能在运行时更改,这意味着:
a. Spring的@Profile注解和特定于profile的配置有局限性。
b. 不支持在创建bean时更改的属性(例如,@ConditionalOnProperty和。enable属性)。
有了这些限制,Spring就可以在构建时执行提前处理,并生成GraalVM可以使用的额外资产。Spring AOT处理过的应用程序通常会生成:
-
Java源代码
-
字节码(用于动态代理等)
-
GraalVM的JSON提示文件:
a.资源提示(Resource -config.json)
b.反射提示(reflect-config.json)
c.序列化提示(serialize -config.json)
e.Java代理提示(Proxy -config.json)
f.JNI提示(JNI -config.json)
源代码生成
Spring应用程序由Spring bean组成。在内部,Spring Framework使用两个不同的概念来管理bean。还有bean实例,它们是已经创建的实际实例,可以注入到其他bean中。还有一些bean定义,用于定义bean的属性以及如何创建其实例。
以一个典型的@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上运行时,当应用程序启动并使用反射调用@Bean方法时,会发生@Configuration类解析。
在创建原生映像时,Spring以不同的方式操作。它不是在运行时解析@Configuration类并生成bean定义,而是在构建时完成。一旦发现了bean定义,它们将被处理并转换为源代码,以便GraalVM编译器进行分析。
Spring AOT过程会将上面的配置类转换为如下代码:
/** * 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 getMyBeanInstanceSupplier() {
return BeanInstanceSupplier
.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实例时,就会调用beaninstancesprovider。这个提供者将调用myConfiguration bean上的myBean()方法。
虽然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和Gradle时使用build/ Generated /aotResources时,生成的提示文件可以在target/spring-aot/main/resources中找到。
代理类生成
Spring有时需要生成代理类,用额外的特性来增强你编写的代码。为此,它使用cglib库直接生成字节码。
当应用程序在JVM上运行时,代理类是在应用程序运行时动态生成的。当创建本地镜像时,需要在构建时创建这些代理,以便它们可以包含在GraalVM中。
注意:
与源代码生成不同,生成的字节码在调试应用程序时没有特别的帮助。但是,如果你需要使用javap之类的工具查看。class文件的内容,可以在target/spring-aot/main/classes (Maven)和build/generated/aotClasses (Gradle)中找到。
开发您的第一个GraalVM原生应用程序
现在我们已经很好地了解了GraalVM原生映像以及Spring ahead- time引擎的工作原理,我们可以看看如何创建应用程序。
构建Spring Boot原生映像应用程序有两种主要方法。
- 使用Spring Boot对云原生构建包的支持,生成一个包含原生可执行文件的轻量级容器。
- 使用GraalVM原生构建工具生成原生可执行文件。
提示:
要启动一个新的本地Spring Boot项目,最简单的方法是打开start.Spring.io,添加“GraalVM Native Support”依赖并生成项目。包含的帮助。Md文件将提供入门提示。
示例程序
我们需要一个示例应用程序,可以用来创建原生镜像。对我们来说,简单的“Hello World!”“getting-started.html”一节中介绍的web应用就足够了。
概括一下,我们的主要应用程序代码看起来像这样:
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@SpringBootApplication
public class MyApplication {
@RequestMapping("/")
String home() {
return "Hello World!";
}
public static void main(String[] args) {
SpringApplication.run(MyApplication.class, args);
}
}
这个应用程序使用了Spring MVC和嵌入式Tomcat,这两个都经过了测试和验证,可以与GraalVM原生镜像一起工作。
使用Buildpacks构建本地镜像
Spring Boot直接为Maven和Gradle提供了原生镜像的构建包支持。这意味着你只需要输入一个命令就可以快速得到一个合理的镜像到本地运行的Docker守护进程中。生成的镜像不包含JVM,而是静态编译的本机镜像。这会导致镜像变小。
注意:
用于图像的构建器是paketobuildpacks/builder:tiny。它有小的足迹和减少攻击面,但你也可以使用paketobuildpacks/builder:base或paketobuildpacks/builder:full如果需要,在图像中有更多可用的工具。
系统需求
Docker应该已经安装。更多细节请参见Get Docker。如果您在Linux上,请将其配置为允许非root用户。
注意:
您可以运行docker run hello-world(不需要sudo)来检查docker守护进程是否可访问。查看Maven或Gradle Spring Boot插件文档了解更多细节。
提示:
在macOS上,建议将分配给Docker的内存增加到至少8GB,并可能增加更多的cpu。有关更多细节,请参阅此堆栈溢出答案。在Microsoft Windows上,请确保启用Docker WSL 2后端以获得更好的性能。
使用Maven
要使用Maven构建一个本地镜像容器,你应该确保pom.xml文件使用spring-boot-starter-parent和org.graalvm.buildtools:native-Maven-plugin。你应该有一个父母。部分看起来像这样:
org.springframework.boot
spring-boot-starter-parent
3.1.1
你应该在如下区域有配置:
org.graalvm.buildtools
native-maven-plugin
spring-boot-starter-parent声明了一个本地配置文件,用来配置创建本地映像所需的执行。你可以在命令行中使用-P标志激活配置文件。
提示:
如果你不想使用Spring-Boot-starter-parent,就需要配置Spring Boot插件中的process-aot目标和本地构建工具插件中的add-reachable-metadata目标的执行。
要构建镜像,可以运行spring-boot:build-image goal,激活原生描述文件:
$ mvn -Pnative spring-boot:build-image
使用Gradle
当应用GraalVM原生镜像插件时,Spring Boot Gradle插件会自动配置AOT任务。你应该检查你的Gradle构建是否包含一个plugins块,其中包含org.graalvm.buildtools.native。
只要应用了org.graalvm.buildtools.native插件,bootBuildImage任务就会生成一个本地镜像,而不是JVM镜像。你可以使用以下命令运行这个任务:
$ gradle bootBuildImage
运行示例
一旦你运行了适当的构建命令,一个Docker镜像就应该可用了。你可以使用docker run启动你的应用程序:
$ docker run --rm -p 8080:8080 docker.io/library/myproject:0.0.1-SNAPSHOT
应该可以看到类似下面的输出:
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v3.1.1)
....... . . .
....... . . . (log output here)
....... . . .
........ Started MyApplication in 0.08 seconds (process running for 0.095)
注意:
启动时间因机器而异,但应该比运行在JVM上的Spring Boot应用程序快得多。
如果打开web浏览器访问localhost:8080,会看到如下输出:
Hello World!
要优雅地退出应用程序,请按ctrl-c。