Spring-Native/GraalVM 带你上路

Posted by rarnu on 05-24,2022

不得不说一句,按我目前的经验,官方出品的东西大部分都是坑,踩进去了根本出不来,还是只能自己研究。Springboot 官方的项目向导也是如此,好不容易加入一个 Spring-Native,结果建立的项目不能通过编译!是的,你没看错,官方向导建立的项目不能编译!这是多大的问题啊,写代码的人应该拉出去枪毙十分钟。

好了,那下面我带大家真正进入 Spring-Native,来看看要把 Springboot 的项目弄成原生的,要如何操作。由于精力有限,我只在自己的 mac 上完成了环境搭建,理论上 Unix 的搭建方式均是一致的,Windows 环境还请各位看官自行研究。

那首先我们去下 GraalVM 的 JDK,对于要使用 GraalVM 来编译为原生应用的 JVM 代码来说,不能用标准的 JDK 进行编译,而是必须使用 GraalVM 改造过的版本。

下载地址在此(点击进入),我们下载 17 版本的,这里特别说一下,JDK 17 与 JDK 8 的相容性很好,我经常会遇到在 IDEA 内打开一个项目时,默认 JDK 被设为 11 于是找不到类的问题,但是将 JDK 8 的程序直接切换到 JDK 17,完全没有问题。

我是 mac 平台,所以下载了 darwin 的包,解压后其目录结构如下:

graalvm-java17.sdk/Contents/Home

这里的 Home 即是 JAVA_HOME,需要用它来替换系统内默认的 JAVA_HOME。如果是其他平台,解包后看到 Home 目录,也应当知道它就是 JAVA_HOME。

然后进入 Home 下的 bin 目录,执行以下代码:

$ ./gu install native-image

这个步骤将为你下载 native-image 命令,有了这个命令,就可以把 Java 代码编译成原生应用了。下载的时候如果一直无法下载成功,请爬墙。

下面我们来建立一个 springboot 的项目,按着官方的向导就可以了,结构如下:

├── demo2.iml
├── HELP.md
├── mvnw
├── mvnw.cmd
├── pom.xml
└── src
   ├── main
   │  ├── kotlin
   │  │  └── com
   │  │     └── demo
   │  │        └── demo2
   │  │           ├── controller
   │  │           │  └── MyController.kt
   │  │           ├── data
   │  │           │  └── Greeing.kt
   │  │           └── Demo2Application.kt
   │  └── resources
   │     ├── application.yml
   │     ├── static
   │     └── templates
   └── test
      └── kotlin
         └── com
            └── demo
               └── demo2
                  └── Demo2ApplicationTests.kt

不要问为啥是 demo2,问就是之前的 demo 被我做坏了。

忽略掉测试代码,此处没有用到,其余三个 kt 文件的源码如下 :

package com.demo.demo2

import org.springframework.boot.SpringApplication
import org.springframework.boot.autoconfigure.SpringBootApplication

@SpringBootApplication
class Demo2Application {
    companion object {
        @JvmStatic
        fun main(args: Array<String>) {
            SpringApplication.run(Demo2Application::class.java, *args)
        }
    }
}

(注意,在 Kotlin 的情况下,Application 类必须如此写,官方向导会将 main 函数生成在外面,它将导致编译失败)

package com.demo.demo2.controller

import com.demo.demo2.data.Greeing
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.bind.annotation.RestController
import java.util.concurrent.atomic.AtomicLong

@RestController
class MyController {

    companion object {
        private const val template = "Hello, %s!"
    }
    private val counter = AtomicLong()

    @GetMapping("/greeting")
    fun greeting(
        @RequestParam(value = "name", defaultValue = "World") name: String
    ): Greeing =
        Greeing(counter.incrementAndGet(), template.format(name))

}
package com.demo.demo2.data

data class Greeing(val id: Long, val content: String)

配置文件代码如下:

server:
  port: 9000

spring:
  profiles:
    active: default

logging:
  level:
    root: info

非常简单的代码,下面是非常麻烦的 maven 配置,只有 maven 配置正确了,才能够正常编译,否则永远都是报出各种奇怪错误

<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.6.2</version>
        <relativePath/>
    </parent>

    <groupId>com.demo</groupId>
    <artifactId>demo2</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>demo2</name>
    <description>springboot native demo</description>

    <properties>
        <java.version>1.8</java.version>
        <kotlin.version>1.6.10</kotlin.version>
        <spring-native.version>0.11.1</spring-native.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.jetbrains.kotlin</groupId>
            <artifactId>kotlin-reflect</artifactId>
        </dependency>

        <dependency>
            <groupId>org.jetbrains.kotlin</groupId>
            <artifactId>kotlin-stdlib-jdk8</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.experimental</groupId>
            <artifactId>spring-native</artifactId>
            <version>${spring-native.version}</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

    </dependencies>

    <build>
        <sourceDirectory>${project.basedir}/src/main/kotlin</sourceDirectory>
        <testSourceDirectory>${project.basedir}/src/test/kotlin</testSourceDirectory>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
            <plugin>
                <groupId>org.jetbrains.kotlin</groupId>
                <artifactId>kotlin-maven-plugin</artifactId>
                <configuration>
                    <args>
                        <arg>-Xjsr305=strict</arg>
                    </args>
                    <compilerPlugins>
                        <plugin>spring</plugin>
                    </compilerPlugins>
                </configuration>
                <dependencies>
                    <dependency>
                        <groupId>org.jetbrains.kotlin</groupId>
                        <artifactId>kotlin-maven-allopen</artifactId>
                        <version>${kotlin.version}</version>
                    </dependency>
                </dependencies>
            </plugin>
            <plugin>
                <groupId>org.springframework.experimental</groupId>
                <artifactId>spring-aot-maven-plugin</artifactId>
                <version>${spring-native.version}</version>
                <executions>
                    <execution>
                        <id>generate</id>
                        <goals>
                            <goal>generate</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>


    <repositories>
        <repository>
            <id>spring-releases</id>
            <name>Spring Releases</name>
            <url>https://repo.spring.io/release</url>
        </repository>
    </repositories>
    <pluginRepositories>
        <pluginRepository>
            <id>spring-releases</id>
            <name>Spring Releases</name>
            <url>https://repo.spring.io/release</url>
        </pluginRepository>
    </pluginRepositories>

    <profiles>
        <profile>
            <id>native</id>
            <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>0.9.9</version>
                        <extensions>true</extensions>
                        <executions>
                            <execution>
                                <id>build-native</id>
                                <phase>package</phase>
                                <goals>
                                    <goal>build</goal>
                                </goals>
                            </execution>
                        </executions>
                        <configuration>

                        </configuration>
                    </plugin>
                    <plugin>
                        <groupId>org.springframework.boot</groupId>
                        <artifactId>spring-boot-maven-plugin</artifactId>
                        <configuration>
                            <classifier>exec</classifier>
                        </configuration>
                    </plugin>
                </plugins>
            </build>
        </profile>
    </profiles>

</project>

这里需要注意一点,即在 pom.xml 内定义了 java.version 属性,即表示使用 JDK8 的兼容特性来进行编译,这句必须写,并不是你在项目设置中选了 Java8 就完事了的。请记得我们真正用来编译的 JDK 版本是 17。另外也需要注意,这里的版本号都是相辅相成的,springboot 必须为 2.6.2

为了编写这个 pom.xml,需要查询相当多的资料,springboot 官方向导所生成的代码会卡在 test-generate,并且在去掉 test-generate 相关代码,顺利编译通过后,执行二进制程序时报出 BeanFactory 的异常,不能生成 @Bean 注解所代表的类,另外,官方文档所述的在 Application 的注解加入 proxyBeanMethods = false 也是错误的,并不需要有这一项改动

在这里,为了防止 GraalVM 对正常的开发造成影响,我们需要在自己的 shell 里面增加以下配置:

JAVA8_HOME=$HOME/Develop/graalvm-java8.jdk/Contents/Home
JAVA17_HOME=$HOME/Develop/graalvm-java17.jdk/Contents/Home
JAVA_HOME=$JAVA17_HOME
export JAVA_HOME=$JAVA_HOME

GRAALVM8_HOME=$JAVA8_HOME/bin
GRAALVM17_HOME=$JAVA17_HOME/bin
GRAALVM_HOME=$GRAALVM17_HOME
export GRAALVM_HOME=$GRAALVM_HOME

alias java8='export JAVA_HOME=`/usr/libexec/java_home -v 1.8`;java -version'
alias java17='export JAVA_HOME=`/usr/libexec/java_home -v 17`;java -version'
alias java8g='export JAVA_HOME=$JAVA8_HOME;export GRAALVM_HOME=$GRAALVM_8_HOME;java -version'
alias java17g='export JAVA_HOME=$JAVA17_HOME;export GRAALVM_HOME=$GRAALVM_17_HOME;java -version'

这样我们就可以通过简单的命令来切换环境了,对于 GraalVM 来说,需要 java17g 的环境,这样切换一下就好:

$ java17g

然后我们就可以去编译了,需要明确的是,在项目中,我们用的 JDK 版本是 8 ,而编译为原生程序时,用的 JDK 版本是 17,目前已经没有供 JDK 8 用的 GraalVM 了。

提示:目前如果要使用 JDK8 的 GraalVM,只能使用 Oracle 出品的企业版,
该版本遵从修改过的 GPL 协议(用了必须开源),并且不允许以商用目的发布由
该企业版编译产生的二进制程序,因此 GraalVM 企业版基本上就是个废物

编译过程是非常漫长的,执行以下命令以编译:

$ mvn -Pnative -DskipTests package

注意,编译需要非常强大的 CPU 和内存,不能小于 8 核 8G,否则编译工具会报错,实测编译时我的 58 核 CPU 占用 70%,内存占用 7.9G,耗时 2:50 编译完。

最后是执行和对比了,springboot 2.6.2 空载运行需要 490M 内存,编译为二进制后,占用 95M 内存。
总的来说,是不满意的,GraalVM 编译后即是原生应用,没理由空载占那么多内存,另外,与其官方宣传的空载 20M 内存占用,差距很大,疑似过度宣传(也有可能是 GraalVM 企业版也有优化)。

总而言之,在内存吃紧的情况下,采用这个方案可以减小一部分内存占用,但是优化的幅度只能说中规中矩,完全达不到所谓的惊艳,另外,在实测中,还发现一个现象,即在服务进行HTTP承压引起内存占用上升后,停止承压,过了一阵子内存占用会下降到原先水平,这是先前使用 Java 开发时做不到的,也算是有了一点小优势吧。