SpringBoot3.x原生镜像-Native Image实践

  • Post author:
  • Post category:其他


前提

之前曾经写过一篇《SpringBoot3.x 原生镜像-Native Image 尝鲜》,当时

SpringBoot

处于

3.0.0-M5

版本,功能尚未稳定。这次会基于

SpringBoot

当前最新的稳定版本

3.1.2

详细分析

Native Image

的实践过程。系统或者软件版本清单如下:

组件 版本 备注

macOS Ventura

13.4.1(c)

ARM

架构

sdkman

5.18.2

JDK

和各类

SDK

包管理工具

Liberica Native Image Kit

23.0.1.r17-nik
可以构建

Native Image



JDK

SpringBoot

3.1.2
使用当前(

2023-08-20

)最新发布版

Maven

3.9.0

安装 sdkman

sdkman是一个轻量级、支持多平台的开源开发工具管理器,可以通过它安装任意主流发行版本(例如

OpenJDK



Kona



GraalVM

等等)的任意版本的

JDK

。通过下面的命令可以轻易安装

sdkman

:

curl -s "https://get.sdkman.io" | bash
source "$HOME/.sdkman/bin/sdkman-init.sh"
sdk version

afdc231d369687f2f5d83f9118213d38.png

spring-boot-native-image-guide-1

可以通过

sdk list java

查看支持的

JDK

发行版本:

4775c9033ebc54f727e1c70124942398.png

spring-boot-native-image-guide-2

通过

shell

命令

sdk install java $Identifier

就可以安装对应的

JDK

发行版。例如可以这样安装

GraalVM-ce-17

:

sdk install java 17.0.8-graalce

通过

shell

命令

sdk uninstall java $Identifier

可以卸载对应的

JDK

发行版。如果安装了多个版本或者多个发行版的

JDK

,可以通过

shell

命令

sdk default java $Identifier

去指定默认使用的

JDK

版本,例如:

sdk default java 17.0.8-graalce

可以通过

shell

命令

sdk current

或者

sdk current java

查看当前正在使用的

SDK

或者

JDK

版本。

0a5717d4e53f569c72ae97b5405bd9e8.png

spring-boot-native-image-guide-3

安装 Liberica NIK


Liberica Native Image Kit



bellsoft

出品的旨在创建高性能本地二进制(

Native Binaries

)基于

JVM

编写的应用的工具包,简称为

Liberica NIK



Liberica NIK

本质就是把

OpenJDK

和多种其他工具包一起封装起来的

JDK

发行版,在

Native Image

功能应用过程,可以简单把它视为

OpenJDK

+

GraalVM

的结合体。可以通过

sdk list java

查看相应的

JDK

版本:

fd1d1bb15aa00454682230d0ee10e786.png

spring-boot-native-image-guide-4

这里选择

JDK-17

的版本进行安装:

sdk install java 23.0.1.r17-nik
# 这里最好把此JDK设置为当前系统的默认JDK,否则后面编译镜像时候会提示找不到GraalVM
sdk default java 23.0.1.r17-nik

安装完成后,通过

java -version

验证一下:

83646d6e7114e71d16c4ae04613e6627.png

spring-boot-native-image-guide-5

编写 SpringBoot 应用

基于

Maven

新建一个

SpringBoot

应用,这里已经整理好了一份

POM

文件,实践过程可以直接用,如下:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>cn.vlts</groupId>
    <artifactId>spring-boot-native-image-demo</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>jar</packaging>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.1.2</version>
        <relativePath/>
    </parent>
    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.version>3.11.0</maven.compiler.version>
        <maven.install.version>3.1.1</maven.install.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>org.apache.tomcat.embed</groupId>
                    <artifactId>tomcat-embed-core</artifactId>
                </exclusion>
                <exclusion>
                    <groupId>org.apache.tomcat.embed</groupId>
                    <artifactId>tomcat-embed-websocket</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.apache.tomcat.experimental</groupId>
            <artifactId>tomcat-embed-programmatic</artifactId>
            <version>${tomcat.version}</version>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <artifactId>maven-compiler-plugin</artifactId>
                <groupId>org.apache.maven.plugins</groupId>
                <version>${maven.compiler.version}</version>
                <configuration>
                    <source>${maven.compiler.source}</source>
                    <target>${maven.compiler.target}</target>
                    <encoding>${project.build.sourceEncoding}</encoding>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-install-plugin</artifactId>
                <version>${maven.install.version}</version>
            </plugin>
            <plugin>
                <groupId>org.graalvm.buildtools</groupId>
                <artifactId>native-maven-plugin</artifactId>
            </plugin>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <executions>
                    <execution>
                        <goals>
                            <goal>repackage</goal>
                        </goals>
                    </execution>
                </executions>
                <configuration>
                    <mainClass>cn.vlts.NativeImageApplication</mainClass>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

这里把

Maven

的所有插件都提升到当前()最新版本,原生镜像打包的关键插件是

native-maven-plugin

,此插件是跟随

spring-boot-starter-parent

进行版本管理,这里无须指定插件的版本。另外,

tomcat-embed-programmatic

是一个实验性依赖,可以降低嵌入式

Tomcat

的内存使用,在生产中应用时候可以暂不启用此特性。接着编写启动类

cn.vlts.NativeImageApplication

@SpringBootApplication
@RestController
public class NativeImageApplication {

    public static void main(String[] args) {
        SpringApplication.run(NativeImageApplication.class, args);
    }

    @RequestMapping(path = "/")
    public ResponseEntity<String> index() {
        return ResponseEntity.ok("index");
    }
}

构建、测试与发布

三个操作的

Maven

命令分别是:

  • 构建:

    mvn -Pnative native:compile

  • 测试:

    mvn -PnativeTest test

  • 发布:

    mvn -Pnative spring-boot:build-image

    ,注意此命令会打包镜像并且发布到

    Docker

    的官方仓库中

虽然 native:compile 命令表面意义是编译,但是实际上它就是构建原生镜像的命令

执行构建流程:

mvn -Pnative native:compile -Dmaven.test.skip=true

构建结果如下:

b624e94c317b26c47a2e4b912dffd1d6.png

spring-boot-native-image-guide-6

其中这个不带

.jar

后缀的就是最终的原生镜像,并且

Native Image

是不支持跨平台的,它只能在

ARM

架构的

macOS

中运行(受限于笔者的编译环境)。可以发现它(见上图中的

target/spring-boot-native-image-demo

,它是一个二进制执行文件)的体积比

executable jar

大好几倍。参照

SpringBoot

的官方文档,经过

AOT

编译的

SpringBoot

应用会生成下面的文件:


  • Java

    源代码

  • 字节码(例如动态代理编译后的产物等)


  • GraalVM

    识别的提示文件:

    • 资源提示文件(

      resource-config.json

    • 反射提示文件(

      reflect-config.json

    • 序列化提示文件(

      serialization-config.json


    • Java

      (动态)代理提示文件(

      proxy-config.json


    • JNI

      提示文件(

      jni-config.json

这里的输出非执行包产物基本都在

target/spring-aot

目录下,其他非

Spring

或者项目源代码相关的产物输出到

graalvm-reachability-metadata

目录中。最后可以验证一下产出的

Native Image

79781eacf165d08007ae8ab46ec4f1f5.png

spring-boot-native-image-guide-7

可以看到启动速度达到惊人的毫秒级别,如果应用在生产中应该可以全天候近乎无损发布。当然,理论上

Native Image

性能也会大幅度提升,但是限于篇幅这里暂时不进行性能测试。

小结

鉴于

SpringBoot3.x

的正式版已经推出一段时间,从文档上看,

Native Image

使用的技术已经相对成熟,能够用于生产条件。当然,

Native Image

目前还存在一些局限性会让一些组件完全无法使用或者部分功能受限(参考Spring Boot with GraalVM),希望这些问题或者局限性有一天能够突破让所有

JVM

应用迎来一次性能飞跃。

(本文完 c-2-d e-a-20230820)



版权声明:本文为zjcsuct原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。