Kotlin 使用 GraalVM+Picocli 开发原生命令行应用

本贴最后更新于 1557 天前,其中的信息可能已经斗转星移

代码已开源: https://github.com/wangyuheng/ddl2plantuml

背景

之前用 kotlin 开发过一款根据建表 DDL 语句生成 plantuml ER 图的应用。被问如何使用,答曰"给你一个 jar 包,然后执行 java -jar ddl2plantuml.jar ./ddl.sql ./er.puml 就可以了。是不是 so easy?"

结果被吐槽了一番,

  1. 为什么不能像命令行应用一样提供相关帮助信息?
  2. 为什么是 Java, 而不是一个原生命令行应用?

这个吐槽带来了一个思考: 为什么 Java 很少用于开发原生命令行(CLI)应用呢?我认为主要问题有 2 个

  1. Java 通过 JVM 实现跨平台。也就是说,如果要使用 Java 应用需要先安装 JRE。
  2. Java 的优势在于 JVM 热点代码检测和运行时编译及优化,所以这是一门程序运行时间越长,速度越快的神奇语言。而付出的代价则是应用启动速度较慢。这与一次性启动运行的命令行应用的场景需求正相反。

方案

为了解决上述问题,引入 2 个名词

  1. Picocli
  2. GraalVM

Picocli

Picocli 致力于提供“最简便的方式来创建富命令行应用,这种应用可以在 JVM 上和 JVM 之外运行”

使用起来非常简单

fun main(args: Array<String>) {

    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\n版本:V1.1.0"],
        description = ["convert sql ddl to plantuml er"],
        mixinStandardHelpOptions = true
)
class Convert : Callable<Int> {

    @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
    }

}

效果

image.png

这里介绍用到的几个注解及概念

  • @Parameters@Options 都是用来定义参数,区别在于 @Parameters 根据位置区分,而 @Options 可以指定名称

    image.png

  • 退出码。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 个功能特性

  1. 即时编译,提升程序启动速度
  2. Native Image,将应用编译为单个静态可执行文件

使用方式

  1. 安装 GraalVM
  2. 安装 native-image 工具 gu install native-image
  3. 编译应用 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

其他

  1. Picocli 提供了 maven 插件 native-image-maven-plugin,用于编译阶段进行 native image 构建。但是建议分离开发和构建,在 CICD 中执行构建过程,可以节省开发时间,并构建不同平台的应用,解决开发环境局限
  2. 除了构建命令行应用,GraalVM 也带来了更多的可能性,比如 Java 在 FAAS 中的应用。
  • Kotlin

    Kotlin 是一种在 Java 虚拟机上运行的静态类型编程语言,由 JetBrains 设计开发并开源。Kotlin 可以编译成 Java 字节码,也可以编译成 JavaScript,方便在没有 JVM 的设备上运行。在 Google I/O 2017 中,Google 宣布 Kotlin 成为 Android 官方开发语言。

    19 引用 • 33 回帖 • 63 关注
  • GraalVM
    3 引用
  • Picocli
    1 引用
2 操作
crick77 在 2020-08-31 10:55:11 置顶了该帖
crick77 在 2020-08-30 23:08:28 置顶了该帖

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...