Ohhnews

分类导航

$ cd ..
DZone Java原文

通过修复其他工具的未解决问题,我构建了一个Java版本管理器

#jolta#java#jdk#版本管理#开源工具

每个 Java 开发者都熟悉这套仪式:一个配置文件里导出 JAVA_HOME,另一个文件里又不一样;这个终端里 sdk use java 21,另一个终端却还在用 Java 8;构建在你的 shell 里能通过,在 IDE 里却失败,因为 IDE 是从 Dock 启动的,从未加载过你的初始化配置;队友口中的“在我机器上能跑”,其实意思是“在我的 shell 里能跑”。

我受够了这一切,所以构建了 Jolta。它是 Volta,不过是给 Java 用的。这篇文章一部分是在讲它能做什么,但更多是在讲我是如何构建它的,因为这个过程是我会推荐给任何在拥挤工具品类中做开发的人的方法:我挖掘了竞争对手的 bug 跟踪系统,把他们的待办事项变成了我的测试套件。

一句话介绍

有了 Jolta,你再也无需操心 Java 版本(如果你不想的话)。brew installjolta setup,结束。现在,cd 进任何项目,javajavac、Maven、Gradle、IDE 的运行按钮、git hooks 和 CI 脚本都会使用该目录对应的正确 JDK——即使在新机器上、固定版本 JDK 尚未安装也没关系(首次使用时它会自动获取)。不需要 sdk use,不需要 jenv add,也不用记得切回来。固定版本的方式是提交一个纯文本的 .java-version 文件,它在任何进程启动的地方都具有权威性。

技术实现概览

Jolta 是一个静态的 Rust 二进制文件,其 shim 就是解析器。每个 shim(javajavacjar 以及 JDK 工具集中的其他工具)都是指向该二进制的符号链接。每次调用都会从其工作目录向上查找最近的 .java-version 文件,从机器上已有的任意 JDK 中选择一个(Jolta 自己安装的、Homebrew 的、/Library/Java 下的、CI 镜像中设置的 JAVA_HOME_17_X64 风格变量等),导出 JAVA_HOME,然后 exec 真正的工具。开销大约两毫秒。

有两个设计结果比任何功能列表都更重要。第一,没有 shell 钩子,也没有 per-shell 状态,因此不会有过期的问题。解析发生在进程启动本身内部,这就是为什么 IDE、cron 任务和 CI 步骤无需任何设置就能获得正确的 JDK。第二,JAVA_HOME 由 shim 在每次调用时设置,因此直接读取 JAVA_HOME 的 Maven 和 Gradle 守护进程无法逃逸这一设置。

第二点是架构差异,而非质量差异。SDKMAN 本质上是 shell 函数:sdk use 会修改当前交互式 shell 的状态,这种模型无法让子进程跟随目录切换到不同的固定版本。jenv 是每次调用时用 shim,但它的 JAVA_HOME 来自一个在提示符时运行的 shell 插件;让它的 JAVA_HOME 保持正确是其长期未关闭的头号问题(jenv #232)。

有趣的部分:从别人的待办事项构建测试套件

这部分才最值得借鉴。版本管理器是一个成熟的品类。volta、jenv、SDKMAN、mise 和 asdf 累积了十年的 bug 报告,每一条都是用户遇到的边缘情况,而且已经被分类整理好、免费写好了。所以在我写大量代码之前,我分三遍挖掘了它们:

  1. 先看它们的测试套件。volta 在 tests/acceptance 中断言了什么,jenv 在它的 bats 文件中检查了什么,SDKMAN 说明了什么规范:这些都成了符合性测试用例。如果某个竞争对手认为某个行为值得固定下来,那大概率也值得我固定。
  2. 再看它们已关闭的 issue。每个已修复的 bug 都至少向用户交付过一次边缘情况。每一个都在 Jolta 暴露同样问题之前,变成了一个回归测试。
  3. 然后看它们仍开放的 issue。这才是最有趣的部分:那些已被报告、确认,但依然躺在待办事项里的 bug。我在一个它们从未被报告过的工具中主动修复了它们,因为这个工具从未把这些问题发布出去。

下面是一些具体的挖掘成果示例:

上游 issue该 bugJolta 的替代做法
mise #9679一个架构错误/libc 错误的 JDK 能“安装”成功,但每次运行都会在加载器中崩溃并抛出晦涩错误安装时执行 exec 探测:JVM 必须真正运行起来,安装才会升级为成功
mise #1887一个裸 GA 版本(“21”)在解析时遮蔽了更新的补丁版本(21.0.x)使用数字版本键;主版本固定始终解析到满足条件的最新构建
mise #6907一个 early-access 构建静默满足 GA 固定版本(或反过来)EA 和 GA 是分开的:-ea 规范只匹配 EA,GA 规范优先选择 GA
volta #1183shim 路径上的一个多余目录会永远静默阻塞该 shimshims 目录完全由 Jolta 拥有,每次重新生成 shim 时逐条清除
volta #2075下载不校验 checksum使用侧车 SHA-256 校验,包括离线镜像
jenv #294为捆绑运行时(GraalVM 自带 node)创建 shim,劫持了用户的其他版本管理器故意永远不为捆绑的语言运行时创建 shim
jenv #232JAVA_HOME 过期,Maven/Gradle 绕过版本管理器shim 每次调用时设置 JAVA_HOME,外加一个 doctor 命令明确指出是什么遮蔽了什么

最终结果是 300 多个回归测试,其中几十个固定到了其他工具跟踪系统中仍处于开放状态的 issue。当有人问 Jolta 有哪些功能是其他工具从根本上做不到的,这就是一半的答案:它不可能不修复这些 bug,因为它们已经在 CI 里了。

这套方法可以推广。如果你正在一个已有成熟参与者的品类中构建任何东西,那么他们的 issue 跟踪系统就是一份按优先级排列的领域难点规格说明,而且是由你未来的用户写好的。在你写架构之前,先读它。

如果你确实在意自己运行的是哪个 Java

Jolta 功能完备,可以让你随心所欲地定制 Java 版本。Jolta 可以下载并管理 8 种发行版(Temurin、Corretto、GraalVM、Oracle、Zulu、Liberica、SapMachine、GraalVM CE),并识别 12 种发行版用于固定版本。你可以设置首选厂商:一个使用 Corretto 的团队固定 11,会得到 Corretto 11.0.31,即使机器上安装了更高的 Temurin 构建,也会选择 Corretto,因为厂商偏好应该优先于构建号贪婪匹配。精确固定就是精确:21.0.2 绝不会被邻近的构建静默满足,自动安装会获取该精确构建。.sdkmanrc 文件开箱即用,方便团队迁移。还有面向隔离 CI 的离线镜像模式、一流的 Windows 支持(硬链接 shim、PowerShell 钩子、无需开发者模式),以及退出码即裁决的 jolta doctor

它不做什么

Jolta 只管理 JDK,仅此而已。SDKMAN 还管理 Maven、Gradle 和 Kotlin;mise 管理你的整个多语言工具链。如果你想用一个工具同时管理 node、python 和 java,请使用 mise。它很好。Jolta 的赌注更聚焦:Java 版本切换应该从每个入口点都正确,而在其余时间完全无感。

试试看

brew install OneAppPlatform/tap/jolta
jolta setup

curl 一键安装脚本和 Windows 说明都在 README 中。在我能找到的每一个方面,我都努力让这个版本管理器成为同类最佳。试试看,然后告诉我是否达到了这个目标。

本文表达的观点属于 DZone 贡献者个人观点。