不得不说一句,按我目前的经验,官方出品的东西大部分都是坑,踩进去了根本出不来,还是只能自己研究。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 开发时做不到的,也算是有了一点小优势吧。