当前位置: 首页 > news >正文

在JVM跑JavaScript脚本 | Oracle GraalJS 简介与实践

这是2024年初的 GraalVM 系列博文,当时写了大纲,知道一年半后的现在才得以完成发布😄

1、概述

实话说,标题的场景为小众需求,日常开发基本用不到,我是最近在做一个低代码轮子玩具 app-meta 需要实现 FaaS(Function as a Service)功能才接触到 JS 引擎。还有如下的场景会用的上:

  • 调用 js 特有的函数(java 体系没有更好的选择)
  • 动态执行代码(代码逻辑随时可修改,这一块脚本语言有天然优势)
  • 需要利用脚本语言扩展 Java 功能

我计划针对在JVM跑JavaScript脚本写系列的文章:

  1. 在JVM跑JavaScript脚本 | Oracle GraalJS 简介与实践
  2. 在JVM跑JavaScript脚本 | FaaS架构简单实现

温馨提示:文章内容较长,可按需定位章节阅读😃

1.1、JVM 下 JS 引擎

内置引擎

引擎所属 JDK 版本基于的 ECMAScript 版本备注
RhinoJDK 6 及之前(Java 1.6)ES3(部分 ES5)由 Mozilla 用 Java 编写,最早的 JVM JS 引擎,速度慢但易集成。
NashornJDK 8 ~ JDK 14ES5.1(少量 ES6 特性)Oracle 开发,性能较 Rhino 高,支持 Java <-> JS 互操作;JDK 15 开始移除。

外部高性能引擎

引擎运行机制特点项目链接
Graal.jsGraalVM 提供的 JS 实现支持 ES2022 及后续,性能高,可与 Java 混合调用,无需 JNI 手写Graal.js 官方
JavetJNI 调用 V8 引擎完整支持现代 JS/Node API,性能接近 Node.jsJavet
Duktape-Java嵌入 Duktape 引擎小巧、易嵌入、启动快,适合轻量脚本执行Duktape-Java
QuickJS-JavaJNI 调用 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 ImageAOT 编译工具,将 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()}`)");}}
}

代码浅析

  • ContextGraalVM 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接口,简单快速!
http://www.lryc.cn/news/619665.html

相关文章:

  • 【AI论文】GLM-4.5:具备智能体特性、推理能力与编码能力的(ARC)基础模型
  • Avalon-MM协议
  • 浅层神经网络
  • SimD小目标样本分配方法
  • 开发避坑指南(24):RocketMQ磁盘空间告急异常处理,CODE 14 “service not available“解决方案
  • 设计原则之【抽象层次一致性(SLAP)】,方法也分三六九等
  • 从零到一:TCP 回声服务器与客户端的完整实现与原理详解
  • Linux LNMP配置全流程
  • 机器学习之词向量转换
  • 第5章 学习的机制
  • 对比学习中核心损失函数的发展脉络
  • AI服务器需求激增,三星内存与SSD供不应求,HBM与DDR5成关键驱动力
  • 2025年高效能工程项目管理软件推荐榜单:AI重构工程进度可视化与资源动态调度体系
  • kernel pwn 入门(四) ret2dir详细
  • 《嵌入式Linux应用编程():Linux Framebuffer图形编程》
  • Win11和Mac设置环境变量
  • 机器学习阶段性总结:对深度学习本质的回顾 20250813
  • Html5-canvas动态渐变背景
  • mac 安卓模拟器 blueStacks
  • MacOS字体看起来比在 Windows 上更好?
  • 367. 有效的完全平方数
  • Spring Boot + MyBatis
  • Python 元类基础:从理解到应用的深度解析
  • [CSCCTF 2019 Qual]FlaskLight
  • [AI React Web] 包与依赖管理 | `axios`库 | `framer-motion`库
  • Spring cloud集成ElastictJob分布式定时任务完整攻略(含snakeyaml报错处理方法)
  • 使用TexLive与VScode排版论文
  • 从0开始配置conda环境并在PyCharm中使用
  • Node.js浏览器引擎+Python大脑的智能爬虫系统
  • 低成本扩展方案:S7-200SMART作为S7-1500分布式IO从站的上位机配置指南