Java SE 6 引入的 javax.script 包(JSR 223)為 JVM 生態帶來了統一的腳本語言集成標準。無論是 Groovy、JavaScript (Nashorn/Rhino)、JRuby、Jython,還是 Kotlin、Scala 等現代 JVM 語言,均可通過標準 API 在運行時動態加載、執行、雙向互調。本文將從核心架構、關鍵 API、上下文隔離、Java 5 兼容、實戰場景五個維度,系統講解 Java 腳本 API 的強大能力。
Application (Java Code)
│
▼
javax.script API (ScriptEngineManager, ScriptEngine, Invocable, Bindings, ScriptContext)
│
▼
Script Engine Implementation (GroovyScriptEngine, NashornScriptEngine, JRubyEngine...)
│
▼
Language Interpreter / Compiler (Groovy, JavaScript, Ruby, Python...)
META-INF/services/javax.script.ScriptEngineFactory。eval()、get/put()、getContext() 等方法。ScriptEngineManager manager = new ScriptEngineManager();
ScriptEngine js = manager.getEngineByName("JavaScript"); // 或 "graal.js", "nashorn"
if (js == null) throw new IllegalStateException("JS 引擎不可用");
js.eval("print('Hello from JavaScript')"); // 直接執行語句
Object result = js.eval("1 + 2 * 3"); // 表達式求值,返回 Java Object (Integer 7)
// 1. 在引擎中定義函數
js.eval("function greet(name) { return 'Hello, ' + name + '!'; }");
// 2. Java 調用腳本函數 (需 Invocable)
Invocable inv = (Invocable) js;
String msg = (String) inv.invokeFunction("greet", "World"); // 返回 "Hello, World!"
// 3. 腳本調用 Java 方法
js.put("javaObj", new MyService()); // 暴露 Java 對象
js.eval("var result = javaObj.doBusiness('param'); print(result);");
public interface Calculator { int add(int a, int b); }
// Groovy 腳本實現接口
String groovyScript = "class GroovyCalc implements Calculator { int add(int a, int b) { a + b } }";
js.eval(groovyScript); // 先編譯類
// 獲取代理實例
Calculator calc = inv.getInterface(Calculator.class);
System.out.println(calc.add(3, 5)); // 輸出 8,實際執行 Groovy 代碼
// 引擎級綁定(默認)
js.put("key", "engineScope");
// 創建獨立腳本上下文
ScriptContext ctx = new SimpleScriptContext();
Bindings engineBindings = js.createBindings();
engineBindings.put("key", "isolatedEngine");
ctx.setBindings(engineBindings, ScriptContext.ENGINE_SCOPE);
// 全局綁定(跨引擎共享)
Bindings global = js.getBindings(ScriptContext.GLOBAL_SCOPE);
global.put("sharedKey", "globalValue");
// 執行時指定上下文
js.eval("print(key);", ctx); // 輸出 "isolatedEngine"
StringWriter output = new StringWriter();
js.getContext().setWriter(new PrintWriter(output));
js.eval("print('captured output')");
System.out.println(output.toString().trim()); // 輸出 "captured output"
| 語言 | 推薦引擎 | Maven 坐標 | 特點 |
|---|---|---|---|
| JavaScript | GraalJS | org.graalvm.js:js | 高性能、ES2023+、支持 Polyglot |
| Groovy | Groovy Script Engine | org.codehaus.groovy:groovy-jsr223 | 原生 Java 集成、DSL 首選 |
| Ruby | JRuby | org.jruby:jruby | 完整 Ruby 3.x 兼容 |
| Python | Jython | org.python:jython | 僅支持 Python 2.7(維護緩慢) |
| Kotlin | Kotlin Script Engine | org.jetbrains.kotlin:kotlin-scripting-jsr223 | 類型安全、協程支持 |
注意:JDK 15 移除了 Nashorn,JDK 8/11 仍內置。生產環境強烈建議顯式引入 GraalJS 等獨立引擎,避免 JDK 版本差異導致行為不一致。
若項目受限於舊版 JDK,可引入 JSR 223 參考實現:
<dependency>
<groupId>javax.script</groupId>
<artifactId>script-api</artifactId>
<version>1.0</version>
</dependency>
<dependency>
<groupId>org.mozilla.javascript</groupId>
<artifactId>rhino</artifactId>
<version>1.7R5</version>
</dependency>
並手動註冊 ScriptEngineFactory 或使用 new ScriptEngineManager(ClassLoader) 指定擴展類加載器。
某電商促銷系統,營運團隊需頻繁調整折扣規則,無法等待代碼發布。方案:
ScriptEngine。Bindings 注入上下文(用戶等級、訂單金額、活動元數據)。Invocable.invokeFunction("calculateDiscount", context) 獲取結果。// 規則腳本示例 (discount.groovy)
import com.example.model.OrderContext
int calculateDiscount(OrderContext ctx) {
if (ctx.userLevel == 'VIP' && ctx.amount > 1000) return 20
if (ctx.amount > 500) return 10
return 0
}
ScriptEngine 線程不安全,但創建開銷大。採用「引擎池」或 ThreadLocal 復用,或使用無狀態 CompiledScript (GraalJS 支持)。Compilable.compile() 獲取 CompiledScript 直接 eval(),跳過解析階段。HostAccess.EXPLICIT,禁止腳本訪問未授權 Java 類/方法/字段。ThreadMXBean 監控腳本執行時間/CPU,超時強制中斷(需引擎支持中斷機制)。Java 腳本 API 將「代碼即數據」的動態能力引入強類型 JVM 世界,配合現代高性能引擎,已成為實現插件架構、動態規則、用戶自定義邏輯、多語言協作的基礎設施標配。掌握其作用域模型、雙向互調機制與引擎選型策略,將為你的系統賦予極強的業務靈活性與擴展性。