背景
之前用kotlin開發過一款根據建表DDL語句生成plantuml ER圖的應用。被問如何使用,答曰"給你一個jar包,然后執行java -jar ddl2plantuml.jar ./ddl.sql ./er.puml 就可以了。是不是so easy?"
結果被吐槽了一番,
- 為什么不能像命令行應用一樣提供相關幫助信息?
- 為什么是Java, 而不是一個原生命令行應用?
這個吐槽帶來了一個思考: 為什么Java很少用于開發原生命令行(CLI)應用呢?我認為主要問題有2個
- Java通過JVM實現跨平臺。也就是說,如果要使用Java應用需要先安裝JRE。
- Java的優勢在于JVM熱點代碼檢測和運行時編譯及優化,所以這是一門程序運行時間越長,速度越快的神奇語言。而付出的代價則是應用啟動速度較慢。這與一次性啟動運行的命令行應用的場景需求正相反。
方案
為了解決上述問題,引入2個名詞
- Picocli
- GraalVM
Picocli
Picocli 致力于提供“最簡便的方式來創建富命令行應用,這種應用可以在 JVM 上和 JVM 之外運行”
使用起來非常簡單
fun main(args: Array) { val cmd = CommandLine(Convert()) when { args.isEmpty() -> { cmd.usage(System.out) } else -> { val exitCode = cmd.execute(*args) exitProcess(exitCode) } }}@CommandLine.Command(name = "ddl2plantuml", version = ["軟件名稱:Ddl2plantuml版本:V1.1.0"], description = ["convert sql ddl to plantuml er"], mixinStandardHelpOptions = true)class Convert : Callable { @CommandLine.Parameters(index = "0", description = ["The sql ddl file that should be convert to plantuml er."]) lateinit var src: Path @CommandLine.Option(names = ["-o", "--output"], description = ["The file where the plantuml file to be saved. default is console "]) private var target: Path? = null override fun call(): Int { require(src.toFile().exists()) { "ddl file must be existed!" } when (target) { null -> { FileReader(src).read() .apply { ConsoleWriter(this).write() } } else -> { FileReader(src).read() .apply { FileWriter(target!!, this).write() } } } return 0 }}
效果

這里介紹用到的幾個注解及概念
- @Parameters 和 @Options 都是用來定義參數,區別在于 @Parameters根據位置區分,而@Options可以指定名稱

- 退出碼。call()方法返回的0表示退出碼,用來描述命令行應用的執行結果。通常用0表示成功,其他數字為自定義異常。退出碼不會影響程序的執行,但是有一個很實用的功能是當你通過連接的方式同時執行多個應用時,一個非零的退出碼會中斷這個組合。如: ./ddl2plantuml_mac ddl.sql |grep "table"
- 版本及幫助信息。可以自定義并指定樣式,version可以通過versionProvider自定義生成。
GraalVM
Go的一個宣傳點是可以將程序編譯為一個靜態可執行文件,而Java也可以通過GraalVM做到這一點
GraalVM: Run Programs Faster Anywhere
這個slogan和Java的"Write Once, Run Anywhere"遙相呼應,同時又展示了極大的野心,準備帶來下一個20年的輝煌。
GraalVM 是一個高性能的通用虛擬機,可以運行使用 JavaScript,Python 3,Ruby,R,基于 JVM 的語言以及基于 LLVM 的語言開發的應用。 GraalVM 消除了編程語言之間的隔離性,并且通過共享運行時增強了他們的互操作性。它可以獨立運行,也可以運行在 OpenJDK,Node.js,Oracle,MySQL 等環境中。
可以看到GraalVM提供了非常強大的功能,這里我們不做展開介紹,只看如何解決我們遇到的問題。主要用到了2個功能特性
- 即時編譯,提升程序啟動速度
- Native Image,將應用編譯為單個靜態可執行文件
使用方式
- 安裝GraalVM
- 安裝 native-image 工具 gu install native-image
- 編譯應用 native-image -jar target/ddl2plantuml-1.1.0.jar ddl2plantuml
編譯后的native image不運行在Java VM上,但是包含了必要的組件,如內存管理和線程調度,這些組件來自另一個Substrate VM。這個過程稱為提前編譯
此時我們已經得到了一個可以直接執行的原生命令行應用
./ddl2plantuml_mac ddl.sql
注意:
native image不支持Java的所有特性,尤其是對reflection的限制。在這次改造過程中,原來通過阿里的druid進行sql解析,但是druid使用了大量的reflection導致native image編譯失敗,所以改用jsqlparser。
其他
- Picocli提供了maven插件native-image-maven-plugin,用于編譯階段進行native image構建。但是建議分離開發和構建,在CICD中執行構建過程,可以節省開發時間,并構建不同平臺的應用,解決開發環境局限
- 除了構建命令行應用,GraalVM也帶來了更多的可能性,比如Java在FAAS中的應用。