mac使用cli3构建项目
重要要点
- 开发人员希望将其命令行应用程序作为单个本机可执行文件进行分发 。
- GraalVM可以将Java应用程序编译为单个本机映像,但是有一些限制。
- Picocli是一个现代库,用于在JVM上编写CLI应用程序,可以帮助解决GraalVM的局限性,包括在Windows上。
- 没有很好地说明如何设置GraalVM工具链以在Windows上创建本机映像。
梦想:Java可执行文件
Go编程语言已成为编写命令行应用程序的流行方法。 这样做的原因可能很多,但是Go令人着迷的一个方面是能够将程序编译为单个本机可执行文件。 这使程序更易于分发。
传统上,Java程序很难分发,因为它们需要在目标计算机上安装Java虚拟机。 可以将最新的JVM与该应用程序捆绑在一起,但这将使程序包大小增加大约200MB。 事情是朝着正确的方向:在Java模块系统(JPMS),在Java 9引入,包括JLINK工具,它允许一个应用程序来创建自定义,最小化,JRE,其可以是小至30-40MB和Java 14将包含jpackage实用程序,该实用程序可以创建一个安装程序,该安装程序在您的应用程序中包括此最低JRE。
但是,对于命令行应用程序,安装程序并不理想。 理想情况下,我们希望将 CLI实用程序作为“真实的”本机可执行文件分发,而无需打包的运行时。 GraalVM允许我们使用Java编写的程序执行此操作。
GraalVM
GraalVM是一种通用虚拟机,可以运行以JavaScript,Python,Ruby,R,基于JVM的语言(例如Java,Scala,Clojure,Kotlin和基于LLVM的语言,例如C和C ++)编写的应用程序。 一个有趣的方面是GraalVM允许您混合编程语言:可以用Java调用部分用JavaScript,R,Python或Ruby编写的程序,并且可以彼此共享数据。 另一个功能是创建本机映像的能力,这就是我们将在本文中探讨的内容。
GraalVM本机映像
GraalVM本机映像允许您提前将Java代码编译为称为本机映像的独立可执行文件。 该可执行文件包括应用程序,库,JDK,并且不能在Java VM上运行,而是包括必要的组件,例如来自不同虚拟机(称为“底物VM”)的内存管理和线程调度。 底物VM是运行时组件(例如反优化器,垃圾收集器,线程调度等)的名称。 与Java VM相比,生成的程序具有更快的启动时间和更低的运行时内存开销。
本机图像限制
为了保持实现的简洁和简洁,并允许进行积极的提前优化,Native Image不支持Java的所有功能。 完整的限制记录在GitHub项目上。
特别要注意两个限制:
基本上,要创建一个自包含的二进制文件,本机图像编译器需要预先了解应用程序的所有类,它们的依赖关系以及它们使用的资源。 反射和资源束通常需要配置。 稍后我们将看到一个示例。
皮科克里
Picocli是用于在JVM上构建命令行应用程序的现代库和框架。 它支持Java,Groovy,Kotlin和Scala。 它不到3年的历史,但已变得非常流行,每月下载量超过500,000。 Groovy语言使用picocli来实现其CliBuilder DSL。
Picocli旨在成为“创建可以在JVM上和在JVM之外运行的丰富命令行应用程序的最简单方法”。 与其他JVM CLI库相比,它提供彩色输出,TAB自动补全,子命令和一些独特功能,例如可协商的选项,重复的复合参数组,重复的子命令和带引号的参数的复杂处理。 它的源代码在单个文件中,因此可以选择将其作为源包括在内,以避免添加依赖项。 Picocli以其广泛而细致的文档记录而感到自豪。
Picocli使用反射,因此很容易受到GraalVM的Java本机映像限制的影响,但它提供了一个注释处理器,该处理器会生成可在编译时解决此限制的配置文件。
具体的用例
让我们举一个命令行实用程序的具体示例,该实用程序将用Java编写并编译为单个本机可执行文件。 在此过程中,我们将研究picocli库的一些功能,这些功能有助于使我们的实用程序易于使用。
我们将构建一个校验和 CLI实用程序,它使用一个命名选项-a或--algorithm和一个位置参数,该参数是要计算其校验和的文件。
我们希望我们的用户能够使用我们的Java 校验和实用程序,就像他们使用以C ++或其他语言编写的应用程序一样。 像这样:
$ echo hi > hi.txt
$ checksum -a md5 hi.txt
764efa883dda1e11db47671c4a3bbd9e
$ checksum -a sha1 hi.txt
55ca6286e3e4f4fba5d0448333fa99fc5a404a73
这是我们期望从命令行应用程序获得的最低要求,但是对于最低公分母应用程序我们将不满意,我们希望创建一个令用户满意的出色的CLI应用程序。 这是什么意思,我们该怎么做?
出色的CLI应用程序很有帮助
我们进行了权衡:通过选择命令行界面(CLI)而不是图形用户界面(GUI),我们的应用程序对于新用户来说不太容易学习。 通过提供良好的在线帮助,我们可以部分弥补这一不足。
当用户使用-h或--help选项请求帮助时,或者指定了无效的用户输入时,我们的应用程序应显示用法帮助消息。 当使用-V或--version时,它还应该显示版本信息。 我们将看到picocli如何使此操作变得容易。
用户体验
通过在支持的平台上使用颜色,我们可以使我们的应用程序更加用户友好。 这不仅看起来不错,而且还减轻了用户的认知负担:对比度使重要的信息(如命令,选项和参数)从周围的文本中脱颖而出。
默认情况下,基于picocli的应用程序生成的使用帮助消息使用颜色。 我们的校验和示例如下所示:
通常,应用程序仅在交互使用时才输出颜色; 在执行脚本时,我们不希望日志文件杂乱无章的ANSI转义代码。 幸运的是,picocli会自动处理此问题。 这将我们带入下一个主题:优秀的CLI应用程序旨在与其他命令结合使用。
出色的CLI应用程序可与他人很好地协作
标准输出vs标准输出
许多CLI实用程序使用标准的I / O流,因此可以将它们与其他实用程序结合使用。 魔鬼往往在细节中。 当用户请求帮助时,应用程序应将使用帮助消息打印到标准输出。 这允许用户将输出通过管道传递到grep或更少的其他工具。
另一方面,在输入无效的情况下,错误消息和使用帮助消息应打印到标准错误流中:如果将我们程序的输出用作另一个程序的输入,我们不希望我们的错误消息中断东西。
退出码
程序结束时,它将返回退出状态代码。 退出代码零通常用于指示成功,而非零退出代码通常指示某种失败。
这样,用户便可以使用&&将多个命令链接在一起,从而知道序列中的任何命令失败,整个序列将停止。
默认情况下,非法用户输入,1 picocli回报2当异常发生在应用程序的业务逻辑,否则为零(如果一切顺利)。 当然,在应用程序中配置其他退出代码很容易,但是对于我们的校验和示例,默认值很好。
请注意,picocli库不会调用System.exit ; 它只是返回一个整数,由应用程序决定是否调用System.exit 。
简码
上面的部分描述了很多功能。 您可能会认为这需要大量代码才能完成,但是大多数“标准CLI行为”由picocli库提供。 在我们的应用程序中,我们需要做的就是定义选项和位置参数,并通过使我们的类Callable或Runnable实现业务逻辑。 我们可以用一行代码在主方法中引导应用程序:
import picocli.CommandLine;
import picocli.CommandLine.Command;
import picocli.CommandLine.Option;
import picocli.CommandLine.Parameters;
import java.io.File;
import java.math.BigInteger;
import java.nio.file.Files;
import java.security.MessageDigest;
import java.util.concurrent.Callable;
@Command(name = "checksum", mixinStandardHelpOptions = true,
version = "checksum 4.0",
description = "Prints the checksum (MD5 by default) of a file to STDOUT.")
class CheckSum implements Callable<Integer> {
@Parameters(index = "0", arity = "1",
description = "The file whose checksum to calculate.")
private File file;
@Option(names = {"-a", "--algorithm"},
description = "MD5, SHA-1, SHA-256, ...")
private String algorithm = "MD5";
// this example implements Callable, so parsing, error handling
// and handling user requests for usage help or version help
// can be done with one line of code.
public static void main(String... args) {
int exitCode = new CommandLine(new CheckSum()).execute(args);
System.exit(exitCode);
}
@Override
public Integer call() throws Exception { // the business logic...
byte[] data = Files.readAllBytes(file.toPath());
byte[] digest = MessageDigest.getInstance(algorithm).digest(data);
String format = "%0" + (digest.length*2) + "x%n";
System.out.printf(format, new BigInteger(1, digest));
return 0;
}
}
我们有一个现实的Java实用程序示例。 接下来,让我们看一下将其转换为本地可执行文件。
本机图像
反射配置
前面我们提到,本机图像编译器有一些限制:支持反射,但需要配置 。
这会影响基于picocli的应用程序:在运行时,picocli使用反射来发现任何@Command注释的子命令以及@Option和@Parameters注释的命令选项和位置参数。
因此,我们需要为GraalVM提供一个配置文件,该文件指定所有带注释的类,方法和字段。 这样的配置文件如下所示:
[
{
"name" : "CheckSum",
"allDeclaredConstructors" : true,
"allPublicConstructors" : true,
"allDeclaredMethods" : true,
"allPublicMethods" : true,
"fields" : [
{ "name" : "algorithm" },
{ "name" : "file" }
]
},
{
"name" : "picocli.CommandLine$AutoHelpMixin",
"allDeclaredConstructors" : true,
"allPublicConstructors" : true,
"allDeclaredMethods" : true,
"allPublicMethods" : true,
"fields" : [
{ "name" : "helpRequested" },
{ "name" : "versionRequested" }
]
}
]
对于具有许多选项的实用程序来说,这很快变得很麻烦,但是幸运的是,我们不需要手动执行此操作。
Picocli注释处理器
picocli-codegen模块包括一个注释处理器,该处理器可以在编译时而不是在运行时根据picocli注释构建模型。
注释处理器在编译过程中在META-INF / native-image / picocli-generated / $ project下生成Graal配置文件,以包含在应用程序jar中。 这包括用于反射 , 资源和动态代理的配置文件。 通过嵌入这些配置文件,您的jar即刻启用了Graal。 在大多数情况下,生成本机映像时不需要进一步的配置。
另外,注释处理器在编译时立即显示无效注释或属性的错误,而不是在运行时进行测试,从而缩短了反馈周期。
因此,我们需要做的就是使用classpath上的picocli-codegen jar编译CheckSum.java源文件:
在Linux上编译CheckSum.java并创建一个checksum.jar 。 更换:路分离器; 这些命令才能在Windows上运行。
mkdir classes
javac -cp .:picocli-4.2.0.jar:picocli-codegen-4.2.0.jar -d classes CheckSum.java
cd classes && jar -cvef CheckSum ../checksum.jar * && cd ..
您可以看到生成的配置文件位于jar内的META-INF / native-image / picocli-generation /目录中:
jar -tf checksum.jar
META-INF/
META-INF/MANIFEST.MF
CheckSum.class
META-INF/native-image/
META-INF/native-image/picocli-generated/
META-INF/native-image/picocli-generated/proxy-config.json
META-INF/native-image/picocli-generated/reflect-config.json
META-INF/native-image/picocli-generated/resource-config.json
我们已经完成了我们的应用程序。 让我们制作一个本地图像作为下一步!
GraalVM本机映像工具链
要创建本机映像,我们需要安装GraalVM,确保已安装本机映像实用程序,并为要构建的OS安装C / C ++编译器工具链。 我在执行此操作时遇到了一些麻烦,因此希望以下步骤可以为其他开发人员阐明一些内容。
安装GraalVM
首先,在撰写本文时,安装最新版本的GraalVM 20.0。 GraalVM 入门页面是获取在各种操作系统和容器中安装GraalVM的最新说明的最佳位置。
安装本机映像实用程序
GraalVM带有本机图像生成器实用程序。 在最新版本的GraalVM中,需要先下载该文件,然后使用Graal Updater工具将其单独安装:
在Linux上安装Java 11的本机图像生成器实用程序
gu install -L /path/to/native-image-installable-svm-java11-linux-amd64-20.0.0.jar
从20.0版开始,Windows版本的GraalVM也需要执行此步骤。
有关更多详细信息,请参见《 GraalVM 参考手册》的“本机映像”部分。
安装编译器工具链
Linux和MacOS编译器工具链
为了进行编译, 本机映像取决于本地工具链,因此在Linux和MacOS上,我们需要glibc-devel , zlib-devel (用于C库和zlib的头文件)和gcc在我们的系统上可用。
要在Linux上完成此操作: sudo dnf install gcc glibc-devel zlib-devel或sudo apt-get install build-essential libz-dev 。
在macOS上,执行xcode-select --install 。
适用于Java 8的Windows编译器工具链
从19.2.0版本开始,GraalVM开始为Windows本机映像提供实验性支持。
Windows支持仍处于试验阶段,有关Windows上本机映像的详细信息,官方文档很少。 从版本19.3开始,GraalVM支持Java 8和Java 11,在Windows上,它们需要不同的工具链。
要使用Java 8版本的GraalVM构建本机映像,您需要用于Windows 7和.NET Framework 4的Microsoft Windows SDK以及KB2519277中的C编译器 。 您可以使用Chocolatey安装这些:
Choco安装Windows-SDK-7.1 kb2519277
然后(在cmd提示符下)激活sdk-7.1环境:
call "C:\Program Files\Microsoft SDKs\Windows\v7.1\Bin\SetEnv.cmd"
这将启动一个新的命令提示符,并启用sdk-7.1环境。 在此“命令提示符”窗口中运行所有后续命令。 这适用于GraalVM从19.2.0到20.0的所有Java 8版本。
适用于Java 11的Windows编译器工具链
要使用Java 11版本的GraalVM(19.3.0及更高版本)生成本机映像,可以安装Visual Studio 2017 IDE(确保包括用于CMake的Visual C ++工具),也可以安装Visual C生成工具工作负载适用于使用Chocolatey的 Visual Studio 2017生成工具:
choco install visualstudio2017-workload-vctools
安装后,使用以下命令从cmd提示符设置环境:
调用“ C:\ Program Files(x86)\ Microsoft Visual Studio \ 2017 \ BuildTools \ VC \ Auxiliary \ Build \ vcvars64.bat”
小费 | 如果安装了Visual Studio 2017 IDE,则根据您的Visual Studio版本,将以上命令中的BuildTools替换为Community或Enterprise 。 |
然后在该命令提示符窗口中运行本机映像 。
创建本机映像
本机映像实用程序可以采用Java应用程序并将其编译为本机映像,该映像可以作为本机可执行文件在其编译的平台上运行。 在Linux上,它可能如下所示:
在Linux上创建本机映像
$ /usr/lib/jvm/graalvm/bin/native-image \
-cp classes:picocli-4.2.0.jar --no-server \
--static -H:Name=checksum CheckSum
native-image实用程序大约需要一分钟在我的笔记本电脑上完成,并产生如下输出:
[checksum:1073] classlist: 3,124.74 ms, 1.14 GB
[checksum:1073] (cap): 2,885.31 ms, 1.14 GB
[checksum:1073] setup: 4,767.19 ms, 1.14 GB
[checksum:1073] (typeflow): 8,733.59 ms, 1.94 GB
[checksum:1073] (objects): 6,073.44 ms, 1.94 GB
[checksum:1073] (features): 313.28 ms, 1.94 GB
[checksum:1073] analysis: 15,384.41 ms, 1.94 GB
[checksum:1073] (clinit): 322.84 ms, 1.94 GB
[checksum:1073] universe: 793.02 ms, 1.94 GB
[checksum:1073] (parse): 2,191.69 ms, 1.94 GB
[checksum:1073] (inline): 2,064.62 ms, 2.13 GB
[checksum:1073] (compile): 14,960.43 ms, 2.73 GB
[checksum:1073] compile: 20,040.78 ms, 2.73 GB
[checksum:1073] image: 1,272.17 ms, 2.73 GB
[checksum:1073] write: 722.20 ms, 2.73 GB
[checksum:1073] [total]: 46,743.28 ms, 2.73 GB
最后,我们有一个本地Linux可执行文件。 有趣的是,使用Java 11版本的GraalVM创建的本机二进制文件比使用Java 8版本的GraalVM创建的本机二进制文件大一点:
-rwxrwxrwx 1 remko remko 14744296 Feb 19 09:51 java11-20.0/checksum*
-rwxrwxrwx 1 remko remko 12393600 Feb 19 09:48 java8-20.0/checksum*
我们可以看到二进制文件的大小为12.4-14.7 MB。 我们可以根据大小比较来考虑大小。 对我来说,这是可以接受的尺寸。
让我们运行该应用程序以验证其是否有效。 在此情况下,我们还可以将在基于JIT的常规JVM上运行应用程序的启动时间与本机映像的启动时间进行比较:
$ time java -cp classes:picocli-4.2.0.jar CheckSum hi.txt
764efa883dda1e11db47671c4a3bbd9e
real 0m0.415s ← startup is 415 millis with normal Java
user 0m0.609s
sys 0m0.313s
$ time ./checksum hi.txt
764efa883dda1e11db47671c4a3bbd9e
real 0m0.004s ← native image starts up in 4 millis
user 0m0.002s
sys 0m0.002s
因此,至少在Linux上,我们现在可以将Java应用程序作为单个本机可执行文件分发。 Windows上的故事是什么?
Windows上的本机映像
Windows上对本机映像的支持有些不足,因此我们将对此进行更详细的介绍。
在Windows上创建本机映像
创建本机映像本身不是问题。 例如:
在Windows上创建本机映像
C:\apps\graalvm-ce-java8-20.0.0\bin\native-image ^
-cp picocli-4.2.0.jar --static -jar checksum.jar
我们从Windows上的native-image.cmd实用程序中获得的输出与在Linux上看到的输出类似,花费了相当多的时间,并且Java 8版本的GraalVM的可执行文件稍小一些,为11.3 MB,而Java的14.2 MB用Java 11版本的GraalVM创建的二进制文件。
二进制文件可以正常工作,但有一个区别:在控制台上看不到ANSI颜色。 让我们来看看解决这个问题。
具有彩色输出的Windows本机映像
要在Windows命令提示符下获取ANSI颜色,我们需要使用Jansi库 。 不幸的是,Jansi(从1.18版开始)存在一些问题 ,这意味着它无法在GraalVM本机映像中生成彩色输出。 为解决此问题,picocli提供了Jansi 随行 库 picocli-jansi-graalvm ,该库允许Jansi库在Windows的GraalVM本机映像中正常工作。
我们更改了主要方法,以告知Jansi在Windows上启用呈现ANSI转义代码,如下所示:
//...
import picocli.jansi.graalvm.AnsiConsole;
//...
public class CheckSum implements Callable<Integer> {
// ...
public static void main(String[] args) {
int exitCode = 0;
// enable colors on Windows
try (AnsiConsole ansi = AnsiConsole.windowsInstall()) {
exitCode = new CommandLine(new CheckSum()).execute(args);
}
System.exit(exitCode);
}
}
set GRAALVM_HOME=C:\apps\graalvm-ce-java11-20.0.0
%GRAALVM_HOME%\bin\native-image ^
-cp "picocli-4.2.0.jar;jansi-1.18.jar;picocli-jansi-graalvm-1.1.0.jar;checksum.jar" ^
picocli.nativecli.demo.CheckSum checksum
我们的DOS控制台应用程序中有颜色:
这需要花费一些额外的精力,但是现在我们的本机Windows CLI应用程序可以使用颜色对比来提供与Linux上相似的用户体验。
通过添加Jansi库,生成的二进制文件的大小没有太大变化:使用Java 11 GraalVM进行构建可以提供14.3 MB的二进制文件,使用Java 8 GraalVM进行构建可以提供11.3 MB的二进制文件。
在Windows上运行本机映像
我们已经快完成了,但是还有另外一个尚不明显的陷阱。
我们刚刚创建的本机二进制文件在我们刚刚构建的本机上可以正常工作,但是当您在其他Windows机器上运行它时,您可能会看到以下错误:
事实证明,我们的本机映像需要 VS C ++ Redistributable 2010中的msvcr100.dll 。此dll可以与exe放在同一目录中,也可以放在C:\ Windows \ System32中 。 正在进行尝试以对此进行改进。
使用适用于Java 11的GraalVM,我们会遇到类似的错误,只是它报告了另一个丢失的DLL VCRUNTIME140.dll :
现在,我们将不得不通过应用程序分发这些DLL,或者告诉我们的用户下载并安装Microsoft Visual C ++ 2015 Redistributable Update 3 RC,以获取基于Java 11的本机映像或Microsoft Visual C ++ 2010的VCRUNTIME140.dll 。 SP1可再发行程序包(x64),以获取基于Java 8的本机映像的msvcr100.dll 。
GraalVM虽然将来可能会不支持交叉编译。 目前,我们需要在Linux上进行编译以获取Linux可执行文件,在MacOS上进行编译以获取MacOS可执行文件,以及在Windows上进行编译以获取Windows可执行文件。
结论
命令行应用程序是GraalVM本机映像的典型用例:现在,我们可以用Java(或另一种JVM语言)进行开发,并将CLI应用程序作为一个相对较小的本机可执行文件进行分发。 (在Windows上,可能需要分发其他运行时DLL除外。)快速启动和减少内存占用是不错的选择。
GraalVM本机映像有一些局限性,应用程序可能需要做一些工作才能将它们转换成本机映像。
通过Picocli ,可以轻松地使用多种基于JVM的语言编写命令行应用程序,并提供了许多附加功能,可以轻松地将CLI应用程序转换为本地映像。
为您的下一个命令行应用程序尝试Picocli和GraalVM!
mac使用cli3构建项目