话说Java的“一次编写,到处运行”,让其从众多语言中脱颖而出,但这个优势已经被容器大幅度地削弱,随着云原生时代的到来,我们对镜像体积、内存消耗、启动速度等提出了新的要求,而这些恰恰是Java的弱点。
本文代码和生成的二进制文件都在GitHub和Gitee上:
https://github.com/dudiao/native-demo
https://gitee.com/songyinyin/native-demo
https://hub.docker.com/r/dudiao/native-demo
什么是Spring Native?
Spring Native是 Spring 团队的一个实验性项目,通过 GraalVM native-image 编译器将 Spring 应用程序编译为本机可执行文件。
其作为 Spring Framework 6、Spring Boot 3 的一部分,改进原生支持。
优点
Spring Native的基础是GraalVM,而GraalVM是使用Java静态编译,将Java字节码编译为汇编代码,即二进制native程序,他摒弃了JVM,这是成就它所有优点的根本原因。
- 编译出来的是原生程序,不依赖与JVM;
- 启动即峰值,不需要JIT编译和预热;
- 启动速度快;
- 内存占用低;
局限性
- 构建的时候,占用的资源多(推荐8G以上内存)、耗时长;
- 不能直接支持反射、动态代理等动态特性;
- 现在处于实验阶段,生态比较少;
关于静态编译和GraalVM相关原理,可以参考《GraalVM与Java静态编译:原理与应用》
前置条件
需要本机安装GraalVM,编译的时候最好可以科学上网,需要预留出来8G以上的内存,苹果M1芯片暂时不支持,以及在编译中不确定的各种问题…
为了更舒适的体验Spring Native和GraalVM带来的快感,本文选择使用GitHub Actions自动构建,只需要安装JDK 11(本地开发使用)就行,这里先放一张成功的图
Spring Native项目搭建
网上有很多Spring Native Hello World的示例,这里就不演示了,咱们本次的目标在于,构建一个能满足练手项目最基本要求的Demo:
- 数据库持久化-spring data jpa(spring native集成mybatis还有些问题);
- 有版本的初始化SQL;
- 集成模版引擎-thymeleaf;
- 应用监控-actuator;
访问spring initializr 网站:
这里使用的是SpringBoot 2.6.4,Spring Native 0.11.3,Maven,JDK 11,然后添加依赖
下面重点介绍几个依赖包:
spring-native
将 Spring 应用程序转化为原生程序运行所需的其他必需依赖
<dependency>
<groupId>org.springframework.experimental</groupId>
<artifactId>spring-native</artifactId>
<version>0.11.3</version>
</dependency>
spring-aot
在代码标记代理类、资源文件时,需要引用此依赖
<!--打包的时候排除,当在代码中定义Hints信息时,需要引用此包-->
<dependency>
<groupId>org.springframework.experimental</groupId>
<artifactId>spring-aot</artifactId>
<scope>provided</scope>
<version>${spring-native.version}</version>
</dependency>
spring-aot-maven-plugin
Spring AOT 插件执行代码的提前转换,用以修复 native image 的兼容性,就是GraalVM分析不到,但是SpringBoot生态中使用的类、资源等,需要使用aot插件生成proxy-config.json
,reflect-config.json
,resource-config.json
,GraalVM 支持通过静态文件进行配置。
<build>
<plugins>
<!-- ... -->
<plugin>
<groupId>org.springframework.experimental</groupId>
<artifactId>spring-aot-maven-plugin</artifactId>
<version>${spring-native.version}</version>
<executions>
<execution>
<id>test-generate</id>
<goals>
<goal>test-generate</goal>
</goals>
</execution>
<execution>
<id>generate</id>
<goals>
<goal>generate</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
hibernate-enhance-maven-plugin
使用spring data jpa时,需要依赖hibernate-enhance-maven-plugin插件
<build>
<plugins>
<!-- ... -->
<plugin>
<groupId>org.hibernate.orm.tooling</groupId>
<artifactId>hibernate-enhance-maven-plugin</artifactId>
<version>${hibernate.version}</version>
<executions>
<execution>
<id>enhance</id>
<goals>
<goal>enhance</goal>
</goals>
<configuration>
<failOnError>true</failOnError>
<enableLazyInitialization>true</enableLazyInitialization>
<enableDirtyTracking>true</enableDirtyTracking>
<enableAssociationManagement>true</enableAssociationManagement>
<enableExtendedEnhancement>false</enableExtendedEnhancement>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
构建Spring Boot本机程序
有两种方式可以构建Spring Boot native application
- 使用Buildpacks构建的是包含本机可执行文件的轻量级容器(docker image);
- 使用GraalVM native build tools构建的是本机可执行文件。
Buildpacks构建docker image
Buildpacks 可以将 Spring Boot 应用程序打包成一个容器。native image buildpack 可以通过 BP_NATIVE_IMAGE 环境变量开启。
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
<classifier>${repackage.classifier}</classifier>
<image>
<!--镜像名称-->
<name>dudiao/${project.artifactId}:${project.version}</name>
<!--构建完镜像后发布-->
<publish>true</publish>
<builder>paketobuildpacks/builder:tiny</builder>
<env>
<BP_NATIVE_IMAGE>true</BP_NATIVE_IMAGE>
</env>
</image>
<docker>
<!--docker中央信息-->
<publishRegistry>
<url>${dockerhub.url}</url>
<username>${env.DOCKERHUB_USERNAME}</username>
<password>${env.DOCKERHUB_PASSWORD}</password>
</publishRegistry>
</docker>
</configuration>
</plugin>
使用这种方式时,需要安装docker,构建命令如下:
mvn clean -U spring-boot:build-image
运行
docker run -d --rm --name native-demo -p 40000:8080 \
-v /opt/docker/nativedemo/logs:/workspace/logs \
-v /usr/share/zoneinfo/Asia/Shanghai:/etc/localtime \
dudiao/native-demo:0.0.1-SNAPSHOT
上边那个命令可直接运行,我已经将镜像推送到docker hub上了。
启动时间:0.193 s
占用内存:108.8 M
GraalVM native build tools构建二进制程序
<profiles>
<profile>
<id>native</id>
<properties>
<repackage.classifier>exec</repackage.classifier>
<native-buildtools.version>0.9.10</native-buildtools.version>
</properties>
<dependencies>
<dependency>
<groupId>org.junit.platform</groupId>
<artifactId>junit-platform-launcher</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.graalvm.buildtools</groupId>
<artifactId>native-maven-plugin</artifactId>
<version>${native-buildtools.version}</version>
<extensions>true</extensions>
<executions>
<execution>
<id>test-native</id>
<phase>test</phase>
<goals>
<goal>test</goal>
</goals>
</execution>
<execution>
<id>build-native</id>
<phase>package</phase>
<goals>
<goal>build</goal>
</goals>
</execution>
</executions>
<configuration>
<!--移除对yaml的支持-->
<removeYamlSupport>true</removeYamlSupport>
<!--移除对jmx的支持-->
<removeJmxSupport>true</removeJmxSupport>
<!--可执行文件的名字-->
<imageName>nd-${project.artifactId}-${project.version}</imageName>
</configuration>
</plugin>
</plugins>
</build>
</profile>
</profiles>
构建本地应用程序
mvn clean -U -Pnative
运行
# 使用./文件名 或者使用 文件名的绝对路径,都可以直接运行
./target/nd-native-demo-0.0.1-xxx
本地应用程序可以直接在GitHub上下载,解压后运行。
端口默认是8080,相关文件的路径为:
nd-native-demo-0.0.1-xxx
logs
|--native-demo.log
db
|--nativedemo.mv.db
可以像SpringBoot应用一样,修改应用的参数,比如修改端口:
./nd-native-demo-0.0.1-xxx --server.port=40000
启动时间:0.156 s
占用内存:106.7 M
其中147.5 M的程序,是运行了几个小时后的。
JVM启动
启动时间:1.886 s
占用内存:394.9 M
总结
可以看出,无论是docker image还是可执行程序,启动速度都有10倍左右的提升,占用的内存也有所降低,但native程序在运行一段时间后,占用的内存会有所上升(这一点还需要更多的测试)。
代码和构建产物在Github和Gitee上,可以下载自己尝试。
参考资料
https://docs.spring.io/spring-native/docs/current/reference/htmlsingle
https://www.infoq.cn/article/rqfww2r2zpyqiolc1wbe
https://www.cnblogs.com/510602159-Yano/p/14591079.html
书籍:《GraalVM与Java静态编译:原理与应用》