早几年我还在折腾桌面端工具的时候,最舒坦的一段日子就是用 JDK 8 自带的 jjs 命令行工具,配合 Nashorn 脚本引擎去写 JavaFX 界面。你不用打开 IDE,不用写一堆 public class,不用等编译,一个记事本加一条 jjs 命令,界面和交互就出来了。改完直接重跑,比传统 Java 桌面开发那套“改代码—编译—运行”的循环不知道快到哪里去了。
这篇文章我打算把“jjs + Nashorn + JavaFX”这套组合拳彻底讲透:jjs 和 Nashorn 各自承担什么角色、Java 与 JavaScript 是怎么互相调用的、JavaFX 的控件和事件在脚本里怎么写,然后带你把一个计算器从零跑到能点、能算、能清零。最后会把我这些年踩过的坑和排查技巧整理成一份速查表,方便你以后直接用。不管你是想快速验证一个界面想法的 Java 开发者,还是想碰一碰 Java 生态的 JS 玩家,这篇文章都能让你少走不少弯路。
1. 这把组合拳到底在解决什么问题
1.1 三个角色各自是干什么的
先理清概念。jjs 是 JDK 8 里自带的一个命令行工具,它的作用类似一个 JS 解释器外壳:你在终端里敲jjs,它就会启动一个 JavaScript 运行环境,支持交互式 REPL,也支持直接执行.js脚本文件。它不需要额外安装,只要你的 JDK 是 8 或 9,bin 目录下就有这个命令。
Nashorn 才是真正干活的 JavaScript 引擎。jjs 只是外壳,Nashorn 是发动机。它是 Oracle 在 JDK 8 里用 Java 重写的 ECMAScript 引擎,除了能跑标准 JavaScript 之外,最核心的能力就是和 Java 互操作:你可以在 JavaScript 里直接引用 Java 类、创建 Java 对象、调用 Java 方法,甚至可以传一个 JS 函数进去,Nashorn 会自动帮你把它适配成 Java 接口的实现。这就是我们能用脚本写 JavaFX 应用的基石。
JavaFX 则是 Java 平台上的桌面 GUI 框架,它提供了一套比 Swing 现代化得多的控件和布局体系,有 CSS 样式、属性绑定、动画这类的“现代 GUI”特性。正常情况下你用 Java 代码写 JavaFX,要写一堆样板代码,而通过 Nashorn 的互操作能力,这些类都能在 JS 脚本里被直接使用。
你可以这样理解这三者的关系:jjs 是驾驶舱,Nashorn 是发动机,JavaFX 是底盘和车身。你要做的事情,就是在驾驶舱里指挥发动机,让整台车跑起来。
1.2 为什么会有这种“脚本写桌面程序”的玩法
可能有人会问:明明有 Java 原生 API,为什么还要绕一层 JavaScript?这背后其实是三类真实需求。
第一类是快速原型验证。有时候你脑子里冒出一个界面交互的点子,但完整写一个 Java 工程需要建目录、写 main 方法、处理编译配置,等跑起来已经过去了十分钟。用脚本的话,写几行代码直接跑,界面出来了,不满意就改,整个过程用秒计。我在做内部工具的时候,经常先用脚本把界面交互调顺,再回头用 Java 正式实现,效率极高。
第二类是给现有 Java 桌面应用补脚本能力。比如你做了一个 JavaFX 工具,想让它支持加载用户脚本来自动化流程,Nashorn 就是一个现成的内置脚本引擎,你可以在应用里用ScriptEngineManager加载 JS 脚本,让脚本直接操作应用内的 Java 对象。这时候你先用 jjs 命令行把脚本调通了,再嵌进应用里,成本非常低。
第三类是降低 GUI 开发门槛。团队里不是所有人都熟悉 Java 编译链路,但很多测试和运维同学会写 JavaScript。通过 jjs 加 JavaFX,他们不用学 Java 编译打包那套东西,就能折腾出一个小工具窗口来。这个玩法在当时的社区里非常流行,也是很多人第一次感受到“JavaFX 竟然能被脚本驱使”的乐趣。
2. 先把 jjs 和 Nashorn 的地基打牢
2.1 jjs 命令行工具的常用姿势
jjs 的用法很简单,但有几个参数值得好好记。最基本的是直接进交互模式:
jjs进去之后你会看到jjs>提示符,可以直接敲 JS 代码。比如敲一句print('hello jjs'),回车,结果立刻输出。在交互模式下你可以随意试验 Java 类的调用,方便得很,我平时验证一个Java.type写法是不是正确,就会直接打开 jjs REPL 试一下。
运行脚本文件是另一个高频姿势:
jjs app.js脚本文件里可以直接写 JavaScript,然后由 Nashorn 解释执行。下面这几个参数是实战里最常用的,列成表格给你:
| 参数 | 作用 | 实战场景 |
|---|---|---|
-cp | 指定 Java classpath | 脚本里需要加载你编译好的 Java 类时,必须加上,否则报ClassNotFoundException |
-scripting | 打开脚本增强模式 | 支持$ENV取环境变量、$EXEC执行系统命令,适合写自动化脚本 |
--language=es6 | 启用 ES6 语法 | 脚本里想用let、const、箭头函数时打开 |
-Dkey=value | 设置 Java 系统属性 | 脚本里通过java.lang.System.getProperty读取 |
-J-Xmx512m | 给 JVM 传递参数 | 控制堆大小,处理大数据时有用 |
--debug | 打印更详细的调试信息 | 排查脚本加载或类型转换问题时很有用 |
注意-cp这个参数,它是 jjs 工具的选项,不是 JVM 的选项。也就是说你得写在 jjs 后面、脚本文件前面:
jjs -cp . app.js这里的.代表当前目录,如果你编译了一个Launcher.class在当前目录,脚本里用Java.type('Launcher')就能找到它。
2.2 Java 和 JavaScript 互操作的核心 API
Nashorn 的互操作能力是这套玩法的心脏,核心 API 其实就几个,你搞清楚它们,后面的 JavaFX 脚本基本无障碍。
第一个是Java.type,它用来引用一个 Java 类:
var System = Java.type('java.lang.System'); System.out.println('hello from JS');这段代码在 JS 里拿到了java.lang.System类,然后调用它的静态字段out的println方法。注意System.out是一个静态字段,Nashorn 支持直接访问。
创建 Java 对象的方法则是new关键字配合类型:
var ArrayList = Java.type('java.util.ArrayList'); var list = new ArrayList(); list.add('a'); list.add('b'); print(list.size());在 JS 里 new 一个 Java 类,得到的对象表现得很像 JS 对象,你可以直接调它的 Java 方法。list.add('a')里传的 JS 字符串会被自动转成 Java 的String,这就是 Nashorn 帮你做了类型转换。
第二个是Java.importer,它相当于 Java 的 import 语句,可以极大简化代码。比如你要用一堆 JavaFX 控件,如果每个都Java.type一次会写到手酸:
var JFX = Java.importer('javafx.scene.control', 'javafx.scene.layout', 'javafx.scene'); var Button = JFX.Button; var VBox = JFX.VBox; var Scene = JFX.Scene;当然,Java.importer返回的对象本身也支持属性访问,所以你也可以直接JFX.Button。在写比较大的脚本界面时,这个 API 能省不少事。
第三个是Java.extend,它用来创建 Java 类或接口的子类/匿名实现类。典型的场景是你要定义一个带多个方法的 Java 接口实现,Nashorn 的“函数自动适配 SAM 接口”只适用于只有一个抽象方法的接口,多方法接口就得用Java.extend或者new InterfaceType({...})的方式。
第四个是数组转换:Java.from和Java.to。JavaFX 里很多 API 返回的是 Java 数组或集合,你在 JS 里想用forEach、map这类 JS 方法来处理,就需要先用Java.from把它转成 JS 数组:
var javaArray = Java.type('java.lang.String[]').class; var jsArray = Java.from(javaArray);反过来,如果 Java API 需要接收一个 Java 数组,你就用Java.to把 JS 数组转过去。
2.3 重载方法与类型转换的坑
Nashorn 的互操作看起来方便,但实际写起来有几个坑,我当年都踩过,先拿出来给你打预防针。
第一个坑是重载方法匹配。Java 有很多重载方法,比如setText(String)、setText(int)。Nashorn 会根据你传入参数的类型和数量去自动匹配最合适的重载版本。大部分情况它挑得很准,但有争议的时候就翻车:比如你传一个null进去,Java 那边有四五个重载版本都能接受null,Nashorn 不知道选哪个,直接就抛异常了。我的建议是不到万不得已不要给 Java 方法传null;非要传的话,可以绕一下,用明确类型的 Java 对象包装:
var StringClass = Java.type('java.lang.String'); var typedNull = StringClass.class.cast(null);不过这个写法读起来绕,能避免就避免。
第二个坑是 JS 数字和 Java 数值类型的差异。JS 里所有数字都是 double,Nashorn 在调用 Java 的int、long参数时,如果传的是 JS 数字并且没有小数部分,它一般会自动帮你转成 int 或 long。但是如果数值过大超出精度范围,就会出现精度丢失。比如你要给 JavaFX 的setPrefWidth(100)传一个整数,没问题;但如果你处理的是很大的 ID 数值,就要考虑精度问题。
第三个坑是字符串类型。JS 字符串在大多数情况下会自动转成 JavaString作为方法参数,没问题。但反过来,如果一个 Java 方法返回了String,你在 JS 里拿到的对象是 Java String 的实例,它在 JS 里表现得像一个字符串,但调用 JS 的字符串方法(比如includes、startsWith)时,不一定全都可用。稳妥的做法是先用String(返回值)包一层,转成纯 JS 字符串再操作。
3. JavaFX 在 Nashorn 里的正确打开方式
3.1 从 Java 代码到脚本代码的映射规则
把 JavaFX 代码翻译成 Nashorn 脚本,本质上就是一个“把 Java 语法翻译成 JS 语法”的过程,规则非常固定,我总结成三步。
第一步,引入类。Java 里写import javafx.scene.Scene;,脚本里就写:
var Scene = Java.type('javafx.scene.Scene');第二步,创建对象。Java 里写new Button("点击"),脚本里写:
var button = new Button('点击');第三步,调用方法。Java 里写button.setText("你好"),脚本里写:
button.setText('你好');你看,基本就是一一对应。唯一需要留意的是静态类常量,比如javafx.geometry.Pos.CENTER,在 JS 里也能访问:
var Pos = Java.type('javafx.geometry.Pos'); root.setAlignment(Pos.CENTER);这个映射规则一旦掌握,你熟悉的 JavaFX API 在脚本里基本都能用。我实测下来,布局容器、控件、样式、动画、图表,这些 JavaFX 核心 API 在 Nashorn 里都能正常调通,没遇到什么突破不了的障碍。
3.2 事件处理:拿 JS 函数直接顶替监听器
这是全套玩法中最爽的地方。JavaFX 的按钮事件在 Java 里要写:
button.setOnAction(e -> { System.out.println("clicked"); });在 Nashorn 里,你只需要把一个 JSfunction传进去:
button.setOnAction(function(event) { print('clicked'); });Nashorn 会检测到setOnAction方法需要的参数类型是EventHandler<ActionEvent>,这是一个只有一个抽象方法的接口(SAM 接口),而你传入的是一个 JS 函数,它就自动帮你把这个函数包装成一个EventHandler实例。这个机制对 JavaFX 里几乎所有事件接口都适用:setOnAction、setOnMouseClicked、setOnKeyPressed、Platform.runLater(Runnable),全部可以直接传函数。
这里有一个值得注意的细节:回调函数里的this。Nashorn 把 JS 函数适配成 Java 接口后,回调执行的时候this指向什么,不同版本的 Nashorn 表现可能不同,我建议在事件回调里不要依赖this,需要访问控件本身时,直接用外层闭包里的变量:
var textField = new TextField(); var label = new Label('默认'); textField.setOnAction(function(e) { label.setText(textField.getText()); // 直接用闭包变量,不要用 this });这样写一方面避开了this指向的坑,另一方面也更容易阅读。
如果遇到的是多方法接口,比如你要添加一个监听集合变化的ListChangeListener,它里面有多个方法,直接传 JS 函数就不行了,这时候要用Java.extend配合对象字面量:
var ListChangeListener = Java.type('javafx.collections.ListChangeListener'); var listener = new (Java.extend(ListChangeListener))({ onChanged: function(change) { print('list changed'); } });3.3 属性绑定与动态界面
JavaFX 的一大特色就是属性绑定。在 Java 里你已经用惯了label.textProperty().bind(textField.textProperty()),不用担心,这套东西在 Nashorn 里完全可用:
var label = new Label(); var textField = new TextField(); label.textProperty().bind(textField.textProperty());这样用户往文本框里输入什么,标签就实时显示什么。脚本里写绑定和 Java 里一模一样,没有任何额外负担。更妙的是,Nashorn 的 JS 函数也能直接适配成ChangeListener,你可以在属性变化时做更多自定义逻辑:
textField.textProperty().addListener(function(obs, oldVal, newVal) { print('从 ' + oldVal + ' 变成了 ' + newVal); });如果你的界面需要根据输入动态计算显示内容,可以用javafx.beans.binding.Bindings的静态方法来做更复杂的绑定。比如你想让标签实时显示“你输入的是:xxx”:
var Bindings = Java.type('javafx.beans.binding.Bindings'); label.textProperty().bind(Bindings.concat('你输入的是:', textField.textProperty()));这一段是给你展示用脚本写 JavaFX 的灵活性,你可以把 Java 里能做的那些绑定技巧,原样搬到脚本里,界面逻辑依然清晰。
4. 完整实操:用 jjs 写一个带界面的计算器
理论说了这么多,下面来个能直接跑起来的完整项目。我会带你做一个简单的四则运算计算器,有数字按钮、运算符按钮、显示区和清零操作,界面用GridPane布局。先说明一下:这个例子我在 JDK 8(任意发行版)上实测可以跑,环境是命令行,不依赖任何 IDE。
4.1 环境准备与 Launcher 桥接类
在动手之前,有一个关键问题要解决:JavaFX 的Application.launch()方法必须在 Java 代码里被调用,而且一个 JVM 中只能调用一次。纯 jjs 脚本里没法直接启动 JavaFX 应用,因为Application的启动入口是 Java 类。所以我们需要写一个很小的 Java 桥接类,负责在后台启动 JavaFX 运行时,并暴露一个Stage对象给脚本使用。
新建一个Launcher.java,代码如下:
import javafx.application.Application; import javafx.application.Platform; import javafx.stage.Stage; import java.util.concurrent.CountDownLatch; public class Launcher { private static CountDownLatch latch = new CountDownLatch(1); private static Stage stage; public static void startup() { if (stage != null) { return; } try { new Thread(() -> Application.launch(FXApp.class)).start(); latch.await(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } public static Stage getStage() { return stage; } public static class FXApp extends Application { @Override public void start(Stage primaryStage) { stage = primaryStage; latch.countDown(); } } }这个类的逻辑很简单:startup()方法在一个新线程里启动 JavaFX 的Application.launch(),当前线程会通过CountDownLatch等一会儿,直到start()回调被触发、stage被赋值后才返回。之后脚本就能通过Launcher.getStage()拿到主窗口对象。
编译它:
javac Launcher.java如果你用的是精简过的 JDK,编译时提示找不到javafx包,就手动把 JavaFX 的 jar 指定进 classpath。JDK 8 里 JavaFX 的 jar 一般在$JAVA_HOME/jre/lib/ext/jfxrt.jar,加进去即可:
javac -cp "$JAVA_HOME/jre/lib/ext/jfxrt.jar" Launcher.java编译完成后会在当前目录生成Launcher.class。
4.2 计算器页面与交互逻辑逐段拆解
有了桥接类,脚本这边就顺畅了。新建一个calculator.js,内容如下:
var Launcher = Java.type('Launcher'); Launcher.startup(); var Platform = Java.type('javafx.application.Platform'); var Scene = Java.type('javafx.scene.Scene'); var GridPane = Java.type('javafx.scene.layout.GridPane'); var Button = Java.type('javafx.scene.control.Button'); var TextField = Java.type('javafx.scene.control.TextField'); var Insets = Java.type('javafx.geometry.Insets'); Platform.runLater(function() { var root = new GridPane(); root.setHgap(10); root.setVgap(10); root.setPadding(new Insets(20)); var display = new TextField(); display.setEditable(false); display.setPrefColumnCount(12); display.setPrefHeight(50); root.add(display, 0, 0, 4, 1); var current = ''; var operator = null; var left = null; function calc() { var right = parseFloat(current); switch (operator) { case '+': return left + right; case '-': return left - right; case '*': return left * right; case '/': return right === 0 ? NaN : left / right; } return right; } function makeButton(text) { var btn = new Button(text); btn.setPrefSize(70, 60); btn.setOnAction(function() { if ((text >= '0' && text <= '9') || text === '.') { current += text; display.setText(current); } else if (text === 'C') { current = ''; operator = null; left = null; display.setText(''); } else if (text === '=') { if (operator !== null && current !== '') { var result = calc(); display.setText(String(result)); current = String(result); operator = null; left = null; } } else { if (current !== '') { left = left === null ? parseFloat(current) : calc(); current = ''; operator = text; } } }); return btn; } var keys = ['7','8','9','/','4','5','6','*','1','2','3','-','0','C','=','+']; for (var i = 0; i < keys.length; i++) { root.add(makeButton(keys[i]), i % 4, Math.floor(i / 4) + 1); } var scene = new Scene(root, 320, 380); var stage = Launcher.getStage(); stage.setTitle('Nashorn Calculator'); stage.setScene(scene); stage.show(); });这段代码我看着不长,但信息密度不小,我一段一段给你拆开讲。
开头那段是初始化。Java.type('Launcher')拿到我们刚编译的桥接类,调用startup()启动 JavaFX 运行时。然后用Java.type把需要用的 JavaFX 类都引进来。注意Platform.runLater(function() {...}),我特意把整个 UI 构建都放进了这里,这是为了严格遵守“JavaFX 节点必须在 FX 线程上创建”的规则。虽然你这个脚本的主线程在 JavaFX 8 里创建控件有时候也不报错,但既然有规范做法,就别赌运气。
中间是布局。GridPane是一个网格布局,setHgap和setVgap控制格子之间的水平垂直间距,setPadding设置内边距,需要传一个Insets实例。display是一个不可编辑的TextField,用来当计算器的显示屏,root.add(display, 0, 0, 4, 1)这行表示它占据从第 0 列第 0 行开始、横向跨 4 列、纵向跨 1 行。
再往下是计算逻辑。current存当前输入的数字字符串,operator存运算符,left存运算符左边的值。calc()函数根据operator做四则运算,这里有个防御:除数为 0 时返回NaN,避免脚本抛出异常。makeButton(text)是一个工厂函数,每次调用都会创建一个按钮,并给它绑定点击事件。事件回调里的分支逻辑很直白:数字和点号就追加到当前输入并刷新显示;C就清空一切;=就执行计算;其他运算符就先保存当前值并记录运算符。
有一点需要提醒:我在makeButton里把text作为参数传入,而不是在回调里直接引用循环变量。这是因为 JS 闭包捕获的是变量的引用,如果你在循环里直接写btn.setOnAction(function() { ... keys[i] ... }),等点击发生时,i早就变成循环结束后的值了,所有按钮都会拿到同一个字符。用makeButton(text)把每次的text作为参数传进去,闭包捕获的是参数变量,就不会串。这是一个非常经典且隐蔽的 JS 闭包坑,我在自己项目里也栽过不止一次。
最后是装配窗口。keys数组定义了按钮的顺序,循环里用Math.floor(i / 4)算出行号,i % 4算出列号,一个个放进网格里。最后创建Scene,设置标题和内容,调用stage.show()把窗口显示出来。
4.3 运行效果与结果验证
运行命令很简单:
jjs -cp . calculator.js如果你前面编译时手动指定过 jfxrt.jar,运行也相应带上:
jjs -cp .:"$JAVA_HOME/jre/lib/ext/jfxrt.jar" calculator.js窗口弹出来之后,你会看到一个 4 行 5 列的计算器界面。我用测试用例跑了一轮,结果如下:
- 输入
12 + 34 =,显示46,正常。 - 输入
5 * 6 =,显示30,正常。 - 连续计算
10 + 5 + 3 =,因为left在第二次运算符时用calc()累计了前一次结果,所以显示18,正常。 - 按
C清零后,显示区清空,能重新输入,正常。 - 输入
8 / 0 =,显示NaN,符合预期,没有崩溃。
这个计算器虽然简单,但它把“创建控件—布局—事件响应—状态管理—结果展示”这一整套 GUI 开发链路完整地跑通了。你以后要用脚本写 JavaFX 工具,这个骨架可以直接当模板改。
5. 常见问题与排查技巧实录
5.1 高频报错现场与解决办法
我在用 jjs 写 JavaFX 的过程中,遇到最多的几个报错,基本可以归成以下几类,整理成速查表给你,方便你直接按图索骥:
| 报错信息 | 出错原因 | 解决办法 |
|---|---|---|
java.lang.ClassNotFoundException: Launcher | 脚本用Java.type引用的类不在 classpath 里 | 编译出.class后,用jjs -cp . app.js运行,把对应目录加进去 |
TypeError: not a function | 类引入了,但调用的方法名不存在或拼写错误 | 先用jjsREPL 里Java.type('xxx')拿到类,打印它有哪些方法核对 |
java.lang.IllegalStateException: Toolkit not initialized | 在 JavaFX 运行时启动之前就创建了控件 | 确保先调用Launcher.startup()或Application.launch,再把 UI 构建放进Platform.runLater |
java.lang.IllegalStateException: Not on FX application thread | 在非 FX 线程里更新了界面 | 界面刷新逻辑包进Platform.runLater(function() {...}) |
TypeError: Cannot convert ... to ... | JS 类型无法自动匹配 Java 方法参数 | 检查是不是传了null、JS 数组是不是需要Java.to转换 |
java.lang.NoClassDefFoundError: javafx/... | JavaFX 运行库没在运行时的 classpath 里 | JDK 8 环境下明确指定jfxrt.jar,或换完整版 JDK |
这表格里的每一条,都是我实际运行脚本时真真切切撞到过的。其中Toolkit not initialized和Not on FX application thread是最容易反复撞的,尤其是你写脚本写到一定程度,开始加定时器、线程、动画的时候,线程问题几乎是必现的。
5.2 classpath 与 JVM 参数的坑
classpath 是玩这套组合拳时最容易翻车的地方,而且翻车方式五花八门。
最常见的情况是“编译能找到,运行找不到”。你用javac编译Launcher.java时,可能 IDE 或者某种环境变量帮你把 jfxrt.jar 带进去了,编译很顺利。但到了 jjs 运行,它不会自动加载 IDE 的那套 classpath,你只写了-cp .,结果脚本里Java.type('Launcher')找得到,但Launcher内部引用的javafx.application.Application找不到了,就报NoClassDefFoundError。这种情况你需要在 jjs 的-cp里也把 jfxrt.jar 带上。
还有一个隐蔽的坑:-cp参数的路径分隔符。Linux/macOS 上用冒号:,Windows 上是分号;。如果你在 Windows 上写jjs -cp .:jfxrt.jar app.js,那会直接报错。我在 Windows 上踩过这个坑,改成分号就好了。
JVM 参数方面,jjs 通过-J前缀透传给 JVM。比如你想加大堆内存,就写jjs -J-Xmx1g app.js。这个用到的场景确实不多,但万一脚本里处理的数据量大,或者加载了很多图片资源,堆溢出报错时你就知道该往哪调了。
5.3 调试脚本的实用小技巧
脚本调试跟 Java 调试思路不太一样,我说几个我自己的土办法,好在大多数情况下都够用。
第一招,善用print()。别嫌它原始,在 GUI 程序里,事件回调执行的时机往往靠想象是不准的,你在回调里塞一个print(),就能立刻确认这个回调有没有被触发、触发时参数是什么。这个思路在 JavaFX 脚本里一样适用:
btn.setOnAction(function(e) { print('clicked, text=' + btn.getText()); });第二招,挂 JS 原生异常监听。Nashorn 支持jjs命令行下的--debug参数,你可以在脚本顶部设置异常处理器:
try { buildUI(); } catch (e) { e.printStackTrace(); }Nashorn 的异常对象实现了 Java 的接口,可以直接调用printStackTrace(),这样能看到完整的 Java 调用栈,定位到具体是哪一行脚本调用 Java API 时报的错。
第三招,小步验证。新手最容易犯的错是写一大坨脚本,然后一次运行,报错后找半天。正确姿势是:先写一个只创建空白窗口的三四行脚本,跑通;然后加一个Button,跑通;再加布局,跑通;最后补事件逻辑。每一步都能跑,出问题时定位范围就非常小。我在折腾这个计算器的时候,也是先验证窗口、再验证按钮布局、最后才写计算逻辑,整个过程非常顺。
6. 时代变了,但思路没过时
说句实话,Nashorn 和 jjs 的命运并不长:JDK 8 里风风光光地出现,到 JDK 11 就被标记为废弃,JDK 15 就直接被移除了。现在你用新版本的 JDK 去敲jjs,得到的只会是“命令未找到”。对于眼前还想玩一玩这套技术栈的人来说,最直接的方案就是装一个 JDK 8,或者用一些兼容的实现继续跑旧脚本。如果要拥抱新一点的工具链,GraalVM 里带的那套 JS 引擎在互操作能力上比 Nashorn 只强不弱,走 Polyglot 方式也能在脚本里调用 Java 类,核心思路是相通的。
不过那套新东西已经不是本文要展开的范畴了。我个人的看法是,就算 Nashorn 已经被官方退役,它留下的“以脚本语言驱动 Java 生态”的思路依然非常有价值。我第一次在一个 JavaFX 窗口上通过 jjs 敲出界面并实时交互的时候,那种冲击感至今还记得,它让我意识到:GUI 开发的效率瓶颈很多时候根本不是控件和布局,而是“改代码—编译—等待”这个循环。脚本化直接把循环压缩掉了,让想法和界面之间的距离变得极短。
所以,就算你现在不太可能在生产环境里大规模用 jjs 去写 JavaFX 了,我仍然建议你把它当作一个玩具和工具去玩一玩。装个 JDK 8,把计算器敲进去跑起来,或者在 REPL 里随手创建几个控件试试,那种“万物皆可脚本驱动”的感觉,会让你对 Java 互操作和 GUI 开发框架的理解都上一个台阶。这也是我写了这么多,最希望你能带走的东西。