知名网站建设定制Java在云原生的破局利器——AOT(JIT与AOT)

导读

JIT(Just-in-Time,实时编译)一直是Java知名网站建设定制语言的灵魂特性之一,知名网站建设定制与之相对的AOT(Ahead-of-Time,预编译)方式,知名网站建设定制似乎长久以来和Java知名网站建设定制语言都没有什么太大的关系。知名网站建设定制但是近年来随着、知名网站建设定制云原生等概念和技术的火爆,Java JVM和JIT知名网站建设定制的性能问题越来越多地被诟病,在Golang、Rust、NodeJS知名网站建设定制等新一代语言的包夹下,知名网站建设定制业界也不断出现“知名网站建设定制云原生时代,Java已死”的言论。那么,Java知名网站建设定制是否可以使用AOT知名网站建设定制方式进行编译,知名网站建设定制摆脱性能的桎梏,知名网站建设定制又是否能够在云原生时知名网站建设定制代焕发新的荣光?知名网站建设定制本文会带着这样的疑问,去探索Java AOT知名网站建设定制技术的历史和现状。

知名网站建设定制上上篇有讲过,HotSpot 知名网站建设定制中集成了两种JIT编译器,Client Compiler和Server Compiler,知名网站建设定制它们的作用也不同。Client Compiler知名网站建设定制注重启动速度和局部的优化,Server Compiler知名网站建设定制则更加关注全局的优化,知名网站建设定制性能会更好,知名网站建设定制但由于会进行更多的全局分析,知名网站建设定制所以启动速度会变慢。知名网站建设定制两种编译器有着不同的应用场景,知名网站建设定制在虚拟机中同时发挥作用。知名网站建设定制而随着时间的发展,不论是Client Compiler还是Server Compiler知名网站建设定制都发展出了各具特色的实现,如 C1、C2、Graal Compiler等,你可以在JVM启动参数中选择自己所需的JIT编译器实现。

从JDK 10起,HotSpot虚拟机同时拥有三种不同的即时编译器。此前我们已经介绍了经典的客
户端编译器和服务编译,还有全新的即时编译器:Graal编译器。

JIT与AOT的区别

提前编译是相对于即时编译的概念,提前编译能带来的最大好处是Java虚拟机加载这些已经预编译成二进制库之后就能够直接调用,而无须再等待即时编译器在运行时将其编译成二进制机器码。理论上,提前编译可以滅少即时编译带来的预热时间,减少Java应用长期给人带来的“第一次运行慢"的不良体验,可以放心地进行很多全程序的分析行为,可以使用时间压力更大的优化措施。但是提前编译的坏处也很明显,它破坏了Java"—次编写,到处运行"的承诺,必须为每个不同的硬件、操作系统去编译对应的发行包;也显著降低了Java链接过程的动态性,必须要求加载的代码在编译期就是全部已知的,而不能在运行期才确定,否则就只能舍弃掉己经提前编译好的版本,退回到原来的即时编译执行状态。

AOT的优点

  • 在程序运行前编译,可以避免在运行时的编译性能消耗和内存消耗
  • 可以在程序运行初期就达到最高性能,程序启动速度快
  • 运行产物只有机器码,打包体积小

AOT的缺点

  • 由于是静态提前编译,不能根据硬件情况或程序运行情况择优选择机器指令序列,理论峰值性能不如JIT
  • 没有动态能力
  • 同一份产物不能跨平台运行

Java AOT的历史演进

JIT是Java的一大灵魂特性,得益于即时编译,Java语言同时拥有了不输编译型语言的运行速度和“一次编译、到处运行”的跨平台能力、甚至还拥有和解释型语言类似的动态性能力,可以说JIT是Java语言能够快速风靡全球并得到广泛应用的重要原因之一。因此在Java诞生至今的几十年里,AOT编译方式和Java可以说是“一毛钱关系都没有”,那么为什么今天我们又要提起以AOT的方式运行Java程序呢,是JIT它不香么?

其实Java本身一直存在着一些“问题”:JVM本身是很重的,因此对服务器的性能消耗(某种意义上可以说是性能浪费)是很高的,同时Java应用的启动速度也往往被人所诟病。但是这些问题在Java所带来的跨平台运行能力和动态特性面前,都是“值得的牺牲” —— 使用Java你可以更方便的进行代码的打包和交付,可以轻松写出性能不差的程序并部署在任何主流的OS上。这些对于企业用户而言,一直是技术选型非常重要的考量因素,直到Docker和Serverless的诞生,逐渐改写了这一切:

Docker的诞生,让底层运行环境变得可以随意定制,你可以在生产环境的任何一台服务器上轻松混部Windows和Linux的各种发行版,这让JVM的跨平台能力显得不那么重要了。

Serverless概念的火爆,让弹性伸缩能力成为服务端程序的一大重要目标,这时候JVM的“臃肿”和JIT导致的启动延迟就让Java程序显得很不“Serverless”(拉包时间长、启动速度慢),如今我们提到云原生总是先想到Go、NodeJS而不是Java,似乎Java和云原生已经不是一个时代的产物了。

所以如果想让Java在云原生时代焕发“第二春”,支持AOT是非常重要的一步,而在这一步上,Java语言却经历了一波三折:

2016年,OpenJDK的 提案首次在Java中引入了AOT支持,在这一草案中,JDK团队提供了 jaotc 工具,使用此工具可以将指定class文件中的方法逐个编译到native代码片段,通过Java虚拟机在加载某个类后替换方法的的入口到AOT代码来实现启动加速的效果。

jaotc的类似于给JVM打了一个“补丁”,让用户有权利将部分代码编译成机器码的时期提前,并预装载到JVM中,供运行时代码调用。不过这个补丁存在很多问题:

首先是在设计上没有考虑到Java的多Classloader场景,当多个Classloader加载的同名类都使用了AOT后,他们的static field是共享的,而根据java语言的设计,这部分数据应该是隔开的。由于这个问题无法快速修复,jaotc最终给出的方案只是暴力地禁止用户自定义classloader使用AOT。

此外,由于社区人手不足,缺乏调优和维护,jaotc的实际运行效果不尽人意,有时甚至会对应用的启动和运行速度带来反向优化,实装没多久之后就退化为实验特性,最终在JDK 16中被删除,结束了短暂的一生。

后来阿里AJDK团队自研的AppCDS(Class-Data-Share)技术继承了jatoc的思路,进行了大幅的优化和完善,目前也不失为一种Java AoT的选择,其本质思路和jaotc基本一致 ,这里就不再赘述了。

而目前业界除了这种在JVM中进行AOT的方案,还有另外一种实现Java AOT的思路,那就是直接摒弃JVM,和C/C++一样通过编译器直接将代码编译成机器代码,然后运行。这无疑是一种直接颠覆Java语言设计的思路,不过还是被各路大佬们实现了,那就是GraalVM Native Image。它通过C语言实现了一个超微缩的运行时组件 —— Substrate VM,基本实现了JVM的各种特性,但足够轻量、可以被轻松内嵌,这就让Java语言和工程摆脱JVM的限制,能够真正意义上实现和C/C++一样的AOT编译。这一方案在经过长时间的优化和积累后,已经拥有非常不错的效果,基本上成为Oracle官方首推的Java AOT解决方案,接下来我们会重点分析一下这项技术的原理和实际应用。

新的破局点GraalVM

先说一下GraalVM,这是Oracle在2019年推出的新一代UVM(通用虚拟机),它在HotSpotVM的基础上进行了大量的优化和改进,主要提供了两大特性:

  • Polyglot:多语言支持,你可以在GraalVM中无缝运行多种语言,包括Java、JS、Ruby、Python甚至是Rust。更重要的是可以通过GraalVM的API来实现语言混编 —— 比如在一段Java代码中无缝引用并调用一个Python实现的模块。
  • HighPerformance:高性能,首先它提供了一个高性能的JIT引擎,让Java语言在GraalVM上执行的时候效率更高速度更快 ;其次就是提供了SubstrateVM,通过Graal Compiler你可以将各种支持的语言(包括Java)编译成本地机器代码,获得更好的性能表现。

值得一提的是,Substrate VM虽然名为VM,但并不是一个虚拟机,而是一个包含了 垃圾回收、线程管理 等功能的运行时组件(Runtime Library),就好比C++当中的stdlib一样。当Java程序被编译为Native Image运行的时候,并不是运行在Substrate VM里,而是将SubstrateVM当作库来使用其提供的各种基础能力,以保障程序的正常运行。

不难看出,GraalVM这个项目的野心是非常大的,可以说这个项目是Oracle抢占云原生市场的一个重要布局,随着官方的不断投入和社区的壮大,目前GraalVM已经日渐成熟,在高性能和跨语言支持方面都交出了令人满意的答卷。GraalVM本身是一个非常庞大的项目,有很多的细节点可以深挖,不过接下来我们还是重点研究一下它的AOT能力 —— Native Image。

Native Image:原理与限制

一个Java程序究竟是如何被编译成静态可执行文件的?我们先来看一下NativeImage的原理。

Native Image的输入是整个应用的所有组件,包括应用本身的代码、各种依赖的库、JDK库、以及SVM;首先会进行整个应用的初始化,也就是代码的静态分析,这个分析过程有点类似GC中的“可达性分析”,会讲程序运行过程中将所有可达的代码、变量、对象生成一个快照,最终打包成一个可执行的Native Image。

一个完整的Native Image包含两个部分,一部分称为 Text Section,即用户代码编译成的机器代码;另一部分称为 Data Section,存储了应用启动后堆区内存中各种对象的快照。

可以预见的是,这个静态分析的过程(官方称之为 Pionts-to Analysis)是非常复杂且耗时的,整个分析过程会以递归的方式进行,最终得到两个树形结构Call Tree(包含所有可达的方法)以及Object Tree(包含所有可达的对象),Call Tree中所包含的方法会被AOT编译为机器码,成为Native Image的Text Section,而Object Tree中所包含的对象及变量则会被保存下来,写入Native Image的Data Setion。

整个静态分析的算法非常复杂,目前网上相关的资料也较少,如果有对具体算法感兴趣的同学,官方团队在Youtube上有一个相对比较详细的算法说明视频,可以自行查看:

作为一个Java程序员,你一定会好奇JVM的动态特性,例如反射、代理,要如何进行静态分析呢?很显然,这两者之间是存在冲突的,因此Native Image设置了一个名为“Closed World”的假设作为静态分析的基本前提。

这个基本前提包含三个要求,对应的也就是目前Native Image存在的三个限制:

  1. Points-to分析的时候,需要接受完整的字节码作为输入(即项目中所有用到的class的字节码都需要获取的到)。

=> 在运行期动态生成或者是动态获取字节码的程序,无法构建成 Native Image。

  1. Java的动态特性,包括反射、JNI、代理,都需要通过配置文件在构建前实现声明好。

=> 无法提前声明动态特性使用范围的程序,无法构建成Native Image (例如,根据用户输入的一个参数反射去调用某个方法)。

  1. 在整个运行过程中,程序不会再加载任何新的class。

=> 在运行期间执行动态编译,或者是自定义Classloader动态装载类的程序,无法构建成Native Image。

Native Image:环境安装

Native Image:实践

介绍了Native Image的基本原理和限制后,让我们来实际实践看看这项技术到底能够带给我们什么。

这里我们先给出一个非常基础的DEMO代码:

public class HelloWorld {    private static final String CONST = "this-is-a constant var";    private String name;    public HelloWorld(String name) {        this.name = name;    }    public void sayHello() {        System.out.println("hello, " + name);    }    public static void main(String[] args) {        System.out.println(CONST);        HelloWorld h1 = new HelloWorld("lumin");        HelloWorld h2 = new HelloWorld(args[0]);        h1.sayHello();        h2.sayHello();    }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

如何将这段代码构建成Native Image呢?首先安装好GraalVM,然后先使用javac将代码编译成字节码:

$ javac HelloWorld.java
  • 1

接下来执行Native Image Build,指定类名

$ native-image HelloWorld
  • 1

整个构建过程会执行比较长的一段时间,主要是执行Points-Analysis过程较长(大约三分多钟),最终的产物就是一个二进制文件:

可以看到这个HelloWorld最终打包产出的二进制文件大小为8.2M,这是包含了SVM和JDK各种库后的大小,虽然相比C/C++的二进制文件来说体积偏大,但是对比完整JVM来说,可以说是已经是非常小了。

再对比下运行速度:

可以看到,相比于使用JVM运行,Native Image的速度要快上不少,cpu占用也更低一些,从官方提供的各类实验数据也可以看出Native Image对于启动速度和内存占用带来的提升是非常显著的:

接下来我们加上 -H:+PrintImageObjectTree -H:+ExhaustiveHeapScan -H:+PrintAnalysisCallTree的参数再进行一次build,这样可以将整个Points-to Analysis的详细过程(Object Tree和Call Tree)打印出来以供分析:

call_tree_xxx文件中会包含完整的方法调用树,可以看到是一个递归的树形结构

通过Call Tree就可以得到整个程序运行过程中所有可能用到的方法,这些方法的代码都会被编译为机器码。

object_tree_xxx文件中,则包含了代码中所有使用到的对象和变量:

这里存储的主要是各种静态对象和变量,它们最终都被被打包至Image Heap中。

最后我们再来看一个使用反射的例子:

public class HelloReflection {    public static void foo() {        System.out.println("Running foo");    }    public static void bar() {        System.out.println("Running bar");    }    public static void main(String[] args) {        for (String arg : args) {            try {                HelloReflection.class.getMethod(arg).invoke(null);            }            catch (ReflectiveOperationException ex) {                System.out.println("Exception running" + arg + ": "+ ex.getClass ().getSimpleName());            }        }    }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

这段代码接收用户的输入作为入参,然后通过反射调用用户指定的方法,我们通过普通方式来编译执行这段代码,是可以正常Work的:

$ java HelloReflection foo barRunning fooRunning bar
  • 1
  • 2
  • 3
  • 4
d:\test>native-image HelloReflection========================================================================================================================GraalVM Native Image: Generating 'helloreflection' (executable)...========================================================================================================================[1/7] Initializing...                                                                                    (5.9s @ 0.08GB) Version info: 'GraalVM 22.2.0 Java 11 CE' Java version info: '11.0.16+8-jvmci-22.2-b06' C compiler: cl.exe (microsoft, x64, 19.32.31332) Garbage collector: Serial GC[2/7] Performing analysis...  [*****]                                                                    (6.9s @ 1.05GB)   2,695 (73.98%) of  3,643 classes reachable   3,437 (53.28%) of  6,451 fields reachable  12,173 (45.34%) of 26,851 methods reachable      26 classes,     0 fields, and   267 methods registered for reflection      62 classes,    53 fields, and    52 methods registered for JNI access       1 native library: version[3/7] Building universe...                                                                               (1.0s @ 0.58GB)Warning: Reflection method java.lang.Class.getMethod invoked at HelloReflection.main(HelloReflection.java:14)Warning: Aborting stand-alone image build due to reflection use without configuration.Warning: Use -H:+ReportExceptionStackTraces to print stacktrace of underlying exception------------------------------------------------------------------------------------------------------------------------                        0.7s (5.0% of total time) in 14 GCs | Peak RSS: 1.94GB | CPU load: 6.07========================================================================================================================Failed generating 'helloreflection' after 14.1s.========================================================================================================================GraalVM Native Image: Generating 'helloreflection' (executable)...========================================================================================================================[1/7] Initializing...                                                                                    (5.7s @ 0.08GB) Version info: 'GraalVM 22.2.0 Java 11 CE' Java version info: '11.0.16+8-jvmci-22.2-b06' C compiler: cl.exe (microsoft, x64, 19.32.31332) Garbage collector: Serial GC[2/7] Performing analysis...  [*****]                                                                    (7.5s @ 0.32GB)   2,807 (74.79%) of  3,753 classes reachable   3,564 (53.47%) of  6,666 fields reachable  12,667 (45.85%) of 27,625 methods reachable      26 classes,     0 fields, and   272 methods registered for reflection      62 classes,    53 fields, and    52 methods registered for JNI access       1 native library: version[3/7] Building universe...                                                                               (1.4s @ 0.80GB)[4/7] Parsing methods...      [*]                                                                        (1.0s @ 1.53GB)[5/7] Inlining methods...     [***]                                                                      (0.8s @ 0.46GB)[6/7] Compiling methods...    [***]                                                                      (5.5s @ 1.06GB)[7/7] Creating image...                                                                                  (1.7s @ 1.43GB)   4.45MB (38.22%) for code area:     7,449 compilation units   6.95MB (59.66%) for image heap:   90,863 objects and 5 resources 252.45KB ( 2.12%) for other data  11.64MB in total------------------------------------------------------------------------------------------------------------------------Top 10 packages in code area:                               Top 10 object types in image heap: 664.30KB java.util                                          928.95KB byte[] for code metadata 360.01KB java.lang                                          853.94KB java.lang.String 353.50KB com.oracle.svm.jni                                 840.00KB byte[] for general heap data 225.12KB java.util.regex                                    637.97KB java.lang.Class 222.22KB java.text                                          526.25KB byte[] for java.lang.String 207.03KB java.util.concurrent                               389.16KB java.util.HashMap$Node 131.93KB com.oracle.svm.core.code                           352.09KB char[] 117.02KB java.math                                          241.23KB com.oracle.svm.core.hub.DynamicHubCompanion 110.77KB com.oracle.svm.core.genscavenge                    191.59KB java.util.HashMap$Node[]  99.46KB sun.text.normalizer                                163.05KB java.lang.String[]   1.96MB for 109 more packages                                1.41MB for 777 more object types------------------------------------------------------------------------------------------------------------------------                        0.9s (3.4% of total time) in 17 GCs | Peak RSS: 3.19GB | CPU load: 7.38------------------------------------------------------------------------------------------------------------------------Produced artifacts: D:\test\helloreflection.build_artifacts.txt (txt) D:\test\helloreflection.exe (executable)========================================================================================================================Finished generating 'helloreflection' in 25.0s.Warning: Image 'helloreflection' is a fallback image that requires a JDK for execution (use --no-fallback to suppress fallback image generation and to print more detailed information why a fallback image was necessary).d:\test>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73

但是如果我们通过native-image运行,则会出现问题,这里我们需要加上–no-fallback参数来构建,否则graalvm检测到这个程序使用了未配置的反射时,会把产物自动降级成jvm运行:

$ ./helloreflection fooException runningfoo: NoSuchMethodException
  • 1
  • 2

可以看到,运行foo方法提示 NoSuchMethodException,这就是因为在编译时我们无法知道用户真正调用的会是哪个方法,因此静态编译的时候就不会把foo、bar这两个方法认为是“可达的”,最终的native image中也就不会包括这两个方法的机器码 。要解决这个问题,我们就需要进行 配置化的 提前声明

在编译的目录下新建一个reflect-config.json,格式内容如下:

[  {    "name": "HelloReflection",    "methods": [{"name":"foo", "parameterTypes": []}]  }]
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

这样就相当于显示地声明了HelloReflection的foo方法会被反射调用,native build的时候就会将这个方法编译为机器码并写入image当中。可以再看下运行效果:

编译日志

[7/7] Creating image...                                                                                  (1.6s @ 0.96GB)   4.28MB (37.44%) for code area:     7,124 compilation units   6.91MB (60.45%) for image heap:   89,515 objects and 5 resources 246.55KB ( 2.11%) for other data  11.44MB in total------------------------------------------------------------------------------------------------------------------------Top 10 packages in code area:                               Top 10 object types in image heap: 635.07KB java.util                                          892.45KB byte[] for code metadata 353.50KB com.oracle.svm.jni                                 840.44KB java.lang.String 324.45KB java.lang                                          831.75KB byte[] for general heap data 225.12KB java.util.regex                                    588.85KB java.lang.Class 222.22KB java.text                                          516.40KB byte[] for java.lang.String 166.94KB java.util.concurrent                               389.16KB java.util.HashMap$Node 131.93KB com.oracle.svm.core.code                           352.09KB char[] 117.02KB java.math                                          231.60KB com.oracle.svm.core.hub.DynamicHubCompanion 110.77KB com.oracle.svm.core.genscavenge                    191.59KB java.util.HashMap$Node[]  99.46KB sun.text.normalizer                                160.39KB java.lang.String[]   1.90MB for 110 more packages                                1.39MB for 750 more object types------------------------------------------------------------------------------------------------------------------------                        0.8s (3.4% of total time) in 17 GCs | Peak RSS: 3.24GB | CPU load: 7.12------------------------------------------------------------------------------------------------------------------------Produced artifacts: d:\test\helloreflection.build_artifacts.txt (txt) d:\test\helloreflection.exe (executable)========================================================================================================================Finished generating 'helloreflection' in 23.2s.
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26

运行效果

# linux$native-image -H:ReflectionConfigurationFiles=./reflect-config.json HelloReflection$./helloreflection fooRunning foo$./helloreflection barException runningbar: NoSuchMethodException# win10d:\test>helloreflection.exed:\test>helloreflection.exe fooRunning food:\test>helloreflection.exe foo barRunning fooException runningbar: NoSuchMethodExceptiond:\test>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

可以看到,显示声明了的foo方法可以正常被调用,但是没有声明过的bar方法,依然会抛出NoSuchMethodException。

Spring Native

上面我们的实践都是比较简单的针对某一个Java Class而言,而我们实际线上使用的工程往往都要复杂许多,尽管Native Image也提供了编译一个完整jar包的能力,但是对于我们通常使用的spring、maven工程来说,由于反射和代理的存在,根本不可能直接通过Native Image编译成功,因此我们还需要工程框架层面的支持,否则Native Image永远无法成为一种生产工具,而更像一个玩具。

作为Java工程界的龙头大佬,Spring自然观察到了这一点,于是就有了Spring Native。

首先需要说明一下,Spring Native目前还属于实验特性,最新Beta版本为0.12.1,还没有推出稳定的1.0版本(按照官方预期是2022年内会推出),需要Spring Boot最低版本是2.6.6,后续Spring Boot 3.0中也会默认支持Native Image。

可以看到活跃度还是不错的,现在处于适配和扩展的阶段。

那么,Spring Native给我们带来了什么呢?

首先是Spring框架的Native化支持,包括IOC、AOP等各种Spring组件及能力的Native支持;其次是Configuration支持,允许通过@NativeHint注解来动态生成Native Image Configuration(reflect-config.json, proxy-config.json等);最后就是Maven Plugin,可以通过Maven构建获得Native Image,而不需要再手动去执行native-image命令。

可以参考这篇

手动支持

思路就是先打成jar包,然后native-image -cp spring-native-example-0.0.1-SNAPSHOT.jar
生成二进制文件

工程支持

接下来我们通过一个DEMO来简单入门Spring Native

首先确保Spring Boot的版本在2.6.6以上,然后在一个基础Spring Boot项目的基础上,引入以下依赖:

<dependency>    <groupId>org.springframework.experimental</groupId>    <artifactId>spring-native</artifactId>    <version>0.11.4</version></dependency>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

接着引入plugin

<plugin>    <groupId>org.springframework.experimental</groupId>    <artifactId>spring-aot-maven-plugin</artifactId>    <version>0.11.4</version>    <executions>        <execution>            <id>generate</id>            <goals>                <goal>generate</goal>            </goals>        </execution>        <execution>            <id>test-generate</id>            <goals>                <goal>test-generate</goal>            </goals>        </execution>    </executions></plugin>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

最后指定native build的profile

<profiles>    <profile>        <id>native</id>        <dependencies>            <!-- Required with Maven Surefire 2.x -->            <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.11</version>                    <extensions>true</extensions>                    <executions>                        <execution>                            <id>build-native</id>                            <goals>                                <goal>build</goal>                            </goals>                            <phase>package</phase>                        </execution>                        <execution>                            <id>test-native</id>                            <goals>                                <goal>test</goal>                            </goals>                            <phase>test</phase>                        </execution>                    </executions>                    <configuration>                        <!-- ... -->                    </configuration>                </plugin>                <!-- Avoid a clash between Spring Boot repackaging and native-maven-plugin -->                <plugin>                    <groupId>org.springframework.boot</groupId>                    <artifactId>spring-boot-maven-plugin</artifactId>                    <configuration>                        <classifier>exec</classifier>                    </configuration>                </plugin>            </plugins>        </build>    </profile></profiles>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50

引入之后,运行mvn -Pnative -DskipTests clean package命令,就可以进入native build过程,编译完成后产物在 target/{your-app-name}。

可以看下启动运行的效果:

相比于普通JVM方式运行,启动速度大约提升了5倍(1.2s -> 0.2s)。

对于大部分简单的Spring Boot应用,只需要经过上述这些简单的配置就可以完整运行了,看起来似乎很美好,是不是?但这仅仅是对于Spring组件而言,Native Image目前需要面对的最大问题,还是来自于Java世界数以万计的各种库:Netty、fastjson、logback、junit … 尽管很多的开源库都开始改造以支持Native Build,但对于生产环境的企业级应用来说,依然还有很长的路要走(当然,这可能也不是Native Image最适用的场景)。

小结

最后对Java的AOT方案做一个总结。Java AOT在经过一波三折的发展后,目前最为成熟可行的方案就是 GraalVM Native Image,它所带来的优势是显著的:更快的启动速度、更小的内存消耗、脱离JVM独立运行 。但对应的,它也存在着非常多的限制,尤其是在充满了反射等动态特性的Java工程生态圈,很难得到大规模的广泛应用。

总的来说,Java AOT目前是有着明确使用场景的一种技术,主要可以应用于:

  1. 编写命令行CLI程序,希望程序可以完整独立运行而不是需要额外安装JVM。
  2. 运行环境资源严重受限的场景,例如IoT设备、边缘计算等场景。
  3. 希望追求极致的启动速度,并且应用逻辑相对足够轻量,如FaaS。

当然未来Java AOT仍然会进一步发展,我们可以拭目以待。说不定能和go扳手腕就看这个

附录

网站建设定制开发 软件系统开发定制 定制软件开发 软件开发定制 定制app开发 app开发定制 app开发定制公司 电商商城定制开发 定制小程序开发 定制开发小程序 客户管理系统开发定制 定制网站 定制开发 crm开发定制 开发公司 小程序开发定制 定制软件 收款定制开发 企业网站定制开发 定制化开发 android系统定制开发 定制小程序开发费用 定制设计 专注app软件定制开发 软件开发定制定制 知名网站建设定制 软件定制开发供应商 应用系统定制开发 软件系统定制开发 企业管理系统定制开发 系统定制开发