這是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接口,簡單快速!