在JVM跑JavaScript脚本 | Oracle GraalJS 简介与实践
这是2024年初的 GraalVM 系列博文,当时写了大纲,知道一年半后的现在才得以完成发布😄
1、概述
实话说,标题的场景为小众需求,日常开发基本用不到,我是最近在做一个低代码轮子玩具 app-meta 需要实现 FaaS(Function as a Service)功能才接触到 JS 引擎。还有如下的场景会用的上:
- 调用 js 特有的函数(java 体系没有更好的选择)
- 动态执行代码(代码逻辑随时可修改,这一块脚本语言有天然优势)
- 需要利用脚本语言扩展 Java 功能
我计划针对在JVM跑JavaScript脚本写系列的文章:
- 在JVM跑JavaScript脚本 | Oracle GraalJS 简介与实践
- 在JVM跑JavaScript脚本 | FaaS架构简单实现
温馨提示:文章内容较长,可按需定位章节阅读😃
1.1、JVM 下 JS 引擎
内置引擎
引擎 | 所属 JDK 版本 | 基于的 ECMAScript 版本 | 备注 |
---|---|---|---|
Rhino | JDK 6 及之前(Java 1.6) | ES3(部分 ES5) | 由 Mozilla 用 Java 编写,最早的 JVM JS 引擎,速度慢但易集成。 |
Nashorn | JDK 8 ~ JDK 14 | ES5.1(少量 ES6 特性) | Oracle 开发,性能较 Rhino 高,支持 Java <-> JS 互操作;JDK 15 开始移除。 |
外部高性能引擎
引擎 | 运行机制 | 特点 | 项目链接 |
---|---|---|---|
Graal.js | GraalVM 提供的 JS 实现 | 支持 ES2022 及后续,性能高,可与 Java 混合调用,无需 JNI 手写 | Graal.js 官方 |
Javet | JNI 调用 V8 引擎 | 完整支持现代 JS/Node API,性能接近 Node.js | Javet |
Duktape-Java | 嵌入 Duktape 引擎 | 小巧、易嵌入、启动快,适合轻量脚本执行 | Duktape-Java |
QuickJS-Java | JNI 调用 QuickJS | 支持最新 JS 特性(ES2020+),内存占用小 | QuickJS-Java |
再后来,GraalVM 横空出世,它是 Oracle Labs 开发的一款 高性能、多语言虚拟机,目标是在 同一个运行时 下高效运行多种编程语言(Java、JavaScript、Python、Ruby、R、LLVM-based 语言、WebAssembly 等),并且实现这些语言之间的无缝互操作。
主要组件
组件 | 作用 |
---|---|
Graal Compiler | 高性能 JIT 编译器,可替代 HotSpot 的 C2 编译器。 |
GraalJS | 在 GraalVM 上运行的 JavaScript/Node.js 实现,支持现代 ECMAScript 规范。 |
Truffle | 一套多语言实现框架,用于开发新语言运行时。 |
Native Image | AOT 编译工具,将 Java 应用打包成本地二进制可执行文件。 |
Polyglot API | 提供跨语言调用的统一 API。 |
今天我们的主角就是 GraalJS。
1.2、 GraalJS 简介
GraalJS: A ECMAScript 2022 compliant JavaScript implementation built on GraalVM. With polyglot language interoperability support. Running Node.js applications!
翻译过来就是,GraalJS 是基于 GraalVM 构建,兼容 ECMAScript 2022 语法的 JavaScript 实现,能够运行 Node.js 应用,同时支持 polyglot (多语言互操作)。
为什么选择它?
最主要原因是它支持较新的 js 语法,有大公司背书,还考虑到 GraalVM 还支持其他脚本语言(如 python),有利于以后的功能扩展。
2、开始使用
📦 依赖引入
此处以 maven 为例
<!-- 增加 GraalJS 依赖,graalvm.version 替换为最新的版本号即可 -->
<properties><graal.version>24.2.1</graal.version>
</properties><dependencies><dependency><groupId>org.graalvm.polyglot</groupId><artifactId>polyglot</artifactId><version>${graal.version}</version></dependency><dependency><groupId>org.graalvm.polyglot</groupId><artifactId>js</artifactId><version>${graal.version}</version><type>pom</type></dependency>
</dependencies>
👋 惯例 Hello World
import org.graalvm.polyglot.Context;public class GraalJSDemo {public static void main(String[] args) {try (Context context = Context.create()) {context.eval("js", "console.log(`来自 GraalJS 的问候!Time=${Date.now()}`)");}}
}
代码浅析
-
Context
是 GraalVM Polyglot API 的核心类。它代表一个“多语言执行上下文”(Execution Context),里面可以执行不同语言的代码,比如"js"
(JavaScript)、"python"
、"ruby"
等。 -
每个
Context
可以看成是一个沙箱(sandbox),里面有独立的全局变量、函数等运行环境。 -
使用 try-with-resources,保证
Context
在使用结束后会自动关闭并释放资源(例如内存、线程等)。 -
Context.create()
会创建一个默认的多语言上下文:- 默认启用 JavaScript、Python 等 GraalVM 已安装的语言(如果你是 GraalVM Standard Edition,可能默认只开启 JavaScript)。
- 你也可以用
Context.create("js")
来只创建 JS 运行环境(更精简)。
-
eval(languageId, sourceCode)
用来在指定语言中执行一段代码。languageId
→"js"
代表执行 JavaScript 代码。sourceCode
→"console.log('Hello from GraalJS!')"
是要运行的 JavaScript 源码。
-
在 GraalVM 里,
console.log
是 Graal.js 提供的一个 JS 全局函数,输出到 Java 的标准输出(System.out
)。
执行后,你会在 Java 控制台看到:
💱 参数传递
我们可以在 JavaScript 里定义函数,然后从 Java 调用它,传递参数。这种方式适合当脚本是函数
而不是全局执行代码
。
/*** 构建一个 JS 函数,支持传递参数并得到结果*/
@Test
public void funWithParams(){try(Context ctx = Context.create(JS)){// 构建函数对象Value addFunc = ctx.eval(JS, "(x, y)=> x+y");// 传递参数调用它int result = addFunc.execute(100, 100).asInt();System.out.println("执行 100+100 函数,结果="+result);}
}
🔌 全局变量
全局变量
就是给 JS 引擎赋予全局可访问的值,类似于 HTML 中的 window
😄。这里就需要用到Bindings
组件。GraalVM 的 Bindings
类似于一个共享的变量表,你可以在 Java 里放值,JS 直接读取。同时参数类型也会自动映射(Java 数字 → JS 数字)👍。
定义 Java 类
public class JavaLogger {// 定义时间格式器(HH表示24小时制,hh表示12小时制)DateTimeFormatter formatter = DateTimeFormatter.ofPattern("HH:mm");private void log(String level, String msg){String time = LocalTime.now().format(formatter);System.out.printf("[JAVA] %s %-5s %s%n", time, level, msg);}public void info(String msg){ log("INFO", msg); }public void debug(String msg){ log("DEBUG", msg); }public void error(String msg){ log("ERROR", msg); }
}
private void printValue(Value value){System.out.printf("%n------------------------ 脚本返回值 ------------------------%n%s", value);
}/*** 设置全局变量*/
@Test
public void bindings(){/*** 自定义 Java 、JS 互通规则,按需开启对应的权限-*/HostAccess hostAccess = HostAccess.newBuilder()//允许不受限制地访问所有公共构造函数、公共类的方法或字段.allowPublicAccess(true)//允许客户端语言实现任何 Java 接口.allowAllImplementations(false)//允许客户端语言实现(扩展)任何 Java 类.allowAllClassImplementations(false)//允许访问数组.allowArrayAccess(false)//允许访问 List.allowListAccess(false)//允许客户应用程序以缓冲区元素的形式访问 ByteBuffers.allowBufferAccess(false)//允许客户应用程序使用迭代器将可迭代对象作为值进行访问.allowIterableAccess(false)//允许客户应用程序将迭代器作为迭代器值进行访问。.allowIteratorAccess(false)//允许客户应用程序以哈希值形式访问 Map 对象.allowMapAccess(true)//允许客户应用程序继承对允许方法的访问权限.allowAccessInheritance(false).build();// 使用自定义 HostAccess 构建 Contexttry(Context ctx=Context.newBuilder(JS).allowHostAccess(hostAccess).build()){Value global = ctx.getBindings(JS);global.putMember("UUID", UUID.randomUUID().toString());// 传递 Map 键值对global.putMember("User",Map.of("name", "集成显卡","url", "https://github.com/0604hx"));// 放置对象示例global.putMember("log", new JavaLogger());String script = """log.info(`开始执行 JS 脚本,UUID=${UUID}`)log.debug(`测试 debug 日志...`)log.error(`测试 error 日志...`)let result = { time: Date.now(), name: User.name, uuid: UUID }result""";printValue(ctx.eval(JS, script));}
}
关于 HostAccess 权限,可以查看官方文档:HostAccess.Builder。
⛑️ 安全管理
allowAllAccess
Context.allowAllAccess
是 GraalVM Polyglot API 里Context.newBuilder()
的一个配置,用来放开 Java 与其他语言之间的所有访问限制。
如果通过context.allowAllAccess(true)
,则表示:“我信任这个脚本,允许它干任何事,包括直接操作 Java 类、方法、字段,甚至文件系统和网络”。对于不明来源不明作用的脚本,这是非常危险的!所以,该项是默认 false
。除非特殊情况,我都强烈建议关闭它。在脚本真要调用什么 Java 代码,可以通过全局对象
来实现。
开启 allowAllAccess(true)
后:
- 解除几乎所有安全限制
- JS / Python / 其他脚本语言可以直接调用 Java API
- 可以访问文件、网络、系统属性等
例子(JS 调用 Java 类):
try (Context context = Context.newBuilder("js").allowAllAccess(true).build()) {context.eval("js", """const File = Java.type('java.io.File');let f = new File('test.txt');console.log("Absolute Path:", f.getAbsolutePath());""");
}
如果没有 allowAllAccess(true)
,上面会抛异常:
java.lang.SecurityException: Access to host classes is not allowed.
allowIO
默认情况下, Context 是不允许执行 I/O 操作(输入输出)的,包括读写文件、访问标准输入输出流、打开网络连接等。必要情况可通过context.allowIO(IOAccess.ALL)
开启。
附录
源代码
本文所有源代码均在:⭐Java实用示例合集-GraalJS ⭐
参考资料
- 全栈虚拟机GraalVM初体验
- clever-graaljs:基于 graaljs 的高性能js脚本引擎,适合各种需要及时修改代码且立即生效的场景,如:ETL工具、动态定时任务、接口平台、工作流执行逻辑。 fast-api 就是基于clever-graaljs开发的接口平台,可以直接写js脚本开发Http接口,简单快速!