Spring Native实战

话说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实战_第1张图片

Spring Native项目搭建

网上有很多Spring Native Hello World的示例,这里就不演示了,咱们本次的目标在于,构建一个能满足练手项目最基本要求的Demo:

  • 数据库持久化-spring data jpa(spring native集成mybatis还有些问题);
  • 有版本的初始化SQL;
  • 集成模版引擎-thymeleaf;
  • 应用监控-actuator;

访问spring initializr 网站:

Spring Native实战_第2张图片

这里使用的是SpringBoot 2.6.4,Spring Native 0.11.3,Maven,JDK 11,然后添加依赖

Spring Native实战_第3张图片

下面重点介绍几个依赖包:

spring-native

将 Spring 应用程序转化为原生程序运行所需的其他必需依赖

<dependency>
  <groupId>org.springframework.experimentalgroupId>
  <artifactId>spring-nativeartifactId>
  <version>0.11.3version>
dependency>

spring-aot

在代码标记代理类、资源文件时,需要引用此依赖


<dependency>
	<groupId>org.springframework.experimentalgroupId>
	<artifactId>spring-aotartifactId>
	<scope>providedscope>
	<version>${spring-native.version}version>
dependency>

spring-aot-maven-plugin

Spring AOT 插件执行代码的提前转换,用以修复 native image 的兼容性,就是GraalVM分析不到,但是SpringBoot生态中使用的类、资源等,需要使用aot插件生成proxy-config.jsonreflect-config.jsonresource-config.json,GraalVM 支持通过静态文件进行配置。

<build>
  <plugins>
    
    <plugin>
      <groupId>org.springframework.experimentalgroupId>
      <artifactId>spring-aot-maven-pluginartifactId>
      <version>${spring-native.version}version>
      <executions>
        <execution>
          <id>test-generateid>
          <goals>
            <goal>test-generategoal>
          goals>
        execution>
        <execution>
          <id>generateid>
          <goals>
            <goal>generategoal>
          goals>
        execution>
      executions>
    plugin>
  plugins>
build>

hibernate-enhance-maven-plugin

使用spring data jpa时,需要依赖hibernate-enhance-maven-plugin插件

<build>
  <plugins>
    
    <plugin>
      <groupId>org.hibernate.orm.toolinggroupId>
      <artifactId>hibernate-enhance-maven-pluginartifactId>
      <version>${hibernate.version}version>
      <executions>
        <execution>
          <id>enhanceid>
          <goals>
            <goal>enhancegoal>
          goals>
          <configuration>
            <failOnError>truefailOnError>
            <enableLazyInitialization>trueenableLazyInitialization>
            <enableDirtyTracking>trueenableDirtyTracking>
            <enableAssociationManagement>trueenableAssociationManagement>
            <enableExtendedEnhancement>falseenableExtendedEnhancement>
          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.bootgroupId>
  <artifactId>spring-boot-maven-pluginartifactId>
  <configuration>
    <excludes>
      <exclude>
        <groupId>org.projectlombokgroupId>
        <artifactId>lombokartifactId>
      exclude>
    excludes>
    <classifier>${repackage.classifier}classifier>
    <image>
      
      <name>dudiao/${project.artifactId}:${project.version}name>
      
      <publish>truepublish>
      <builder>paketobuildpacks/builder:tinybuilder>
      <env>
        <BP_NATIVE_IMAGE>trueBP_NATIVE_IMAGE>
      env>
    image>
    <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>nativeid>
    <properties>
      <repackage.classifier>execrepackage.classifier>
      <native-buildtools.version>0.9.10native-buildtools.version>
    properties>
    <dependencies>
      <dependency>
        <groupId>org.junit.platformgroupId>
        <artifactId>junit-platform-launcherartifactId>
        <scope>testscope>
      dependency>
    dependencies>
    <build>
      <plugins>
        <plugin>
          <groupId>org.graalvm.buildtoolsgroupId>
          <artifactId>native-maven-pluginartifactId>
          <version>${native-buildtools.version}version>
          <extensions>trueextensions>
          <executions>
            <execution>
              <id>test-nativeid>
              <phase>testphase>
              <goals>
                <goal>testgoal>
              goals>
            execution>
            <execution>
              <id>build-nativeid>
              <phase>packagephase>
              <goals>
                <goal>buildgoal>
              goals>
            execution>
          executions>
          <configuration>
            
            <removeYamlSupport>trueremoveYamlSupport>
            
            <removeJmxSupport>trueremoveJmxSupport>
            
            <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

Spring Native实战_第4张图片

占用内存:394.9 M

Spring Native实战_第5张图片

总结

可以看出,无论是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静态编译:原理与应用》

你可能感兴趣的:(spring,boot,spring-native,graalvm,spring,boot,cloud,native,java,graalvm)