Java平臺為桌面和Web應(yīng)用程序開發(fā)提供了豐富的資源。是,除非有專有軟件解決方案,否則從平臺外部使用其他資源是不切實際的。沒有行業(yè)標(biāo)準(zhǔn)定義或闡明開發(fā)人員如何使用java類文件執(zhí)行其他編程語言。腳本語言還沒有一種標(biāo)準(zhǔn)的,行業(yè)支持的方法來與Java技術(shù)集成。 Java Specification Request(JSR)223 是一項更改,它通過定義標(biāo)準(zhǔn)框架和應(yīng)用程序編程接口(API)來執(zhí)行以下操作,從而幫助開發(fā)人員集成Java技術(shù)和腳本語言:
- Access and control Java technology-based objects from a scripting environment
- Create web content with scripting languages
- Embed scripting environments within Java technology-based applications
本文重點關(guān)注該規(guī)范的第三個目標(biāo),并將展示如何使用Java平臺應(yīng)用程序中的嵌入式腳本環(huán)境。 一個名為ScriptCalc的演示應(yīng)用程序?qū)⑻峁┮粋€有效的示例,說明如何使用JavaScript語言的自定義腳本擴展你的應(yīng)用程序。
為什么要使用腳本語言
大多數(shù)腳本語言是動態(tài)執(zhí)行的。 通常,你可以在不預(yù)先確定變量類型的情況下創(chuàng)建新變量,并且可以重用變量來存儲不同類型的值。 同樣,腳本語言會自動嘗試執(zhí)行許多類型轉(zhuǎn)換,例如,根據(jù)需要將數(shù)字10轉(zhuǎn)換為文本“ 10”。 盡管有些腳本語言是編譯性的,但大多數(shù)語言都是解釋性的。 腳本環(huán)境通常在同一過程中執(zhí)行腳本的編譯和執(zhí)行。 通常,這些環(huán)境在首次執(zhí)行時也會將腳本解析并編譯為中間代碼。
這些高質(zhì)量的腳本語言可幫助你更快地編寫應(yīng)用程序,重復(fù)執(zhí)行命令以及將來自不同技術(shù)的組件集成在一起。 專用腳本語言比通用語言更容易或更快速地執(zhí)行特定任務(wù)。 例如,許多開發(fā)人員認為Perl腳本語言是處理文本和生成報告的好方法。 其他開發(fā)人員將bash or ksh shell 用于命令和作業(yè)控制。 其他腳本語言有助于方便地定義用戶界面或Web內(nèi)容。 開發(fā)人員可以使用Java編程語言和平臺來完成這些任務(wù)中的任何一項,但是腳本語言有時可以更好地完成這項工作。 這個事實并沒有削弱Java平臺的功能和豐富性,而只是承認腳本語言在開發(fā)人員的工具箱中占有重要地位。
將腳本語言與Java平臺相結(jié)合,為開發(fā)人員提供了利用兩種環(huán)境的能力的機會。 出于任何原因,你可以繼續(xù)使用腳本語言,并且可以使用功能強大的Java類庫來擴展這些語言的功能。 如果您是Java語言程序員,則現(xiàn)在可以發(fā)布用戶動態(tài)定制的應(yīng)用程序。 Java平臺和腳本語言之間的協(xié)同作用產(chǎn)生了一個環(huán)境,開發(fā)人員和最終用戶可以在其中協(xié)作創(chuàng)建更有用的動態(tài)應(yīng)用程序。
例如,假設(shè)一個計算器具有一組核心運算。 盡管基本計算器可能只有四個或五個基本操作,但是你可以提供用戶可以自定義的可編程功能鍵。 客戶可以使用他們喜歡的任何腳本語言向計算器添加抵押貸款計算,溫度轉(zhuǎn)換或其他更復(fù)雜的功能。 另一個使用場景文字處理器,它允許客戶提供用于生成各種文件格式的自定義過濾器。 本文其余部分的示例將展示如何使用腳本為客戶提供可定制的Java應(yīng)用程序。
JSR 223 實現(xiàn)
JDK 6和JRE 6庫中包含了用于JavaScript編程語言的Mozilla Rhino引擎。 Java SE 6平臺實現(xiàn)了java.script API,你可以使用符合JSR 223的腳本引擎。 您可以通過訪問Mozilla Rhino web site 網(wǎng)站來了解有關(guān)嵌入式JavaScript技術(shù)引擎的更多信息。
使用腳本 API 的方法
腳本 API 在 Java SE 6平臺中 javax.script 包中. 如下表所示,The API仍然相對較小,由六個接口和六個類組成 .
Table 1: Interfaces and Classes in the Java SE 6 Platform
| Interface | Class |
|---|---|
Bindings |
AbstractScriptEngine |
Compilable |
CompiledScript |
Invocable |
ScriptEngineManager |
ScriptContext |
SimpleBindings |
ScriptEngine |
SimpleScriptContext |
ScriptEngineFactory |
ScriptException |
你的起點應(yīng)該是 ScriptEngineManager 類. ScriptEngineManager 對象可以告訴你你 Java Runtime Environment (JRE) 可以使用那些腳本引擎. 它還可以提供 ScriptEngine 對象,這些對象可以解釋特定腳本語言編寫的攪拌. 使用此API的最簡單方法是執(zhí)行以下操作:
- 創(chuàng)建一個
ScriptEngineManager對象. - 從manager 中獲取一個
ScriptEngine對象. - 使用
ScriptEngine對象執(zhí)行 script .
這聽起來很容易, 但是代碼是什么樣的呢? Example 1 代碼執(zhí)行了這三個步驟, 并在控制臺打印 Hello, world!.
Example 1: 通過engine 名稱創(chuàng)建一個 ScriptEngine 對象.
ScriptEngineManager mgr = new ScriptEngineManager();
ScriptEngine jsEngine = mgr.getEngineByName("JavaScript");
try {
jsEngine.eval("print('Hello, world!')");
} catch (ScriptException ex) {
ex.printStackTrace();
}
如果要查詢支持的腳本引擎列表,將值傳遞到腳本環(huán)境或編譯腳本以重復(fù)執(zhí)行, API 僅僅智慧稍微復(fù)雜一點點. 其他 APIs 允許你查詢 ScriptEngineManager 以獲取與特定文件擴展名關(guān)聯(lián)的引擎, 從文件執(zhí)行腳本以及在腳本中調(diào)用特定功能. 本文會介紹其他一些功能.
可用的腳本引擎
ScriptEngineManager 對象為腳本框架提供發(fā)現(xiàn)機制. 可以找到ScriptEngineFactory 類, 該類創(chuàng)建 ScriptEngine 對象. 開發(fā)人員可以使用 JAR Service Provider specification 將腳本引擎添加到JRE. 盡管此規(guī)范不是我們要討論的內(nèi)容,更多信息請參考 JAR File Specification.
Example 1 直接從腳本管理器中獲取了腳本引擎. 但是, 只有當(dāng)你知道引擎名稱時 , 這種訪問 ScriptEngine 對象的方式才有效. 如果需要使用更復(fù)雜的條件來獲取 ScriptEngine 對象, 則可能需要首先獲取 ScriptEngineFactory 對象支持的引擎的整個列表. ScriptEngineFactory 可以為特定的腳本語言創(chuàng)建 ScriptEngine 對象.
Example 2 提供可發(fā)現(xiàn)工廠的列表.
Example 2: 你可以獲取安裝到Java平臺的所有引擎的列表.
ScriptEngineManager mgr = new ScriptEngineManager();
List<ScriptEngineFactory> factories = mgr.getEngineFactories();
一旦擁有腳本引擎工廠后, 你可以獲取有關(guān)工廠支持的腳本語言的各種詳細信息:
- 引擎名稱和版本
- 語言名稱和版本
- 腳本引擎的別名
- 腳本語言的
ScriptEngine對象
Example 3 展示如何獲取這些信息.
Example 3: ScriptEngineFactory 對象提供有關(guān)其提供的引擎的詳細信息.
ScriptEngineManager mgr = new ScriptEngineManager();
List<ScriptEngineFactory> factories =
mgr.getEngineFactories();
for (ScriptEngineFactory factory: factories) {
System.out.println("ScriptEngineFactory Info");
String engName = factory.getEngineName();
String engVersion = factory.getEngineVersion();
String langName = factory.getLanguageName();
String langVersion = factory.getLanguageVersion();
System.out.printf("\tScript Engine: %s (%s)\n",
engName, engVersion);
List<String> engNames = factory.getNames();
for(String name: engNames) {
System.out.printf("\tEngine Alias: %s\n", name);
}
System.out.printf("\tLanguage: %s (%s)\n",
langName, langVersion);
}
Example 3 控制臺輸出如下:
ScriptEngineFactory Info
Script Engine: Java ScriptEngine (2.0.0)
Engine Alias: Java
Engine Alias: java
Engine Alias: bern:java-scriptengine
Engine Alias: bern-java
Language: Java (1.8.0_211)
ScriptEngineFactory Info
Script Engine: Oracle Nashorn (1.8.0_211)
Engine Alias: nashorn
Engine Alias: Nashorn
Engine Alias: js
Engine Alias: JS
Engine Alias: JavaScript
Engine Alias: javascript
Engine Alias: ECMAScript
Engine Alias: ecmascript
Language: ECMAScript (ECMA - 262 Edition 5.1)
請注意,不同版本的JDK或者安裝了不同腳本的引擎,輸出都不太一樣,其中 JavaScript 腳本引擎工廠是java內(nèi)置的引擎。 Rhino是核心JDK 6庫中包含的唯一引擎,Nashorn是JDK8庫中的引擎。 你可以通過將基于JAR文件的服務(wù)提供程序安裝到JRE中來添加其他引擎,如前所述。 本文的代碼示例使用Nashorn引擎。 請注意,腳本引擎工廠提供了許多引擎名稱別名,以幫助您獲取腳本語言的引擎。
創(chuàng)建 ScriptEngine 的方法
一旦獲得了有關(guān)工廠及其提供的引擎的所有信息 , 就可以在運行時決定使用哪個引擎工廠. 如果找到合適的 ScriptEngineFactory, 則創(chuàng)建關(guān)聯(lián)的 ScriptEngine 就比較容易了. 從工廠獲取實際的引擎,如示例4所示, 只需要調(diào)用工廠的 getScriptEngine 方法. 此代碼遍歷所有已知的工廠, 搜索符合語言名稱和版本的工廠. 在此示例中, 條件是硬編碼的. 該代碼獲取支持ECMA - 262 Edition 5.1版的工廠.
Example 4: 獲取滿足你應(yīng)用要求的腳本引擎.
ScriptEngineManager mgr = new ScriptEngineManager();
List<ScriptEngineFactory> scriptFactories =
mgr.getEngineFactories();
ScriptEngine engine = null;
for (ScriptEngineFactory factory: scriptFactories) {
String langName = factory.getLanguageName();
String langVersion = factory.getLanguageVersion();
if (langName.equals("ECMAScript") &&
langVersion.equals("ECMA - 262 Edition 5.1")) {
engine = factory.getScriptEngine();
break;
}
}
當(dāng)然, 如果確定引擎可用, 則可以直接通過名稱,文件擴展名甚至MIME類型向 ScriptEngineManager 中獲取對象. 一下代碼將獲取 JavaScript programming language engine ,因為 js 是 JavaScript 語言的通用擴展名.
engine = mgr.getEngineByExtension("js");
如何運行腳本
ScriptEngine 對象運行腳本代碼. 引擎的 eval 方法來翻譯腳本, 腳本是從 String 或 java.io.Reader 對象中獲得得字符序列 . Reader 對象也可以從文件獲取其字符. 即使你已經(jīng)部署了應(yīng)用程序,也可以使用此功能來讀取客戶提供得腳本 .
Example 1 使用 eval 方法來計算 String 字符序列:
try {
jsEngine.eval("print('Hello, world!')");
} catch (ScriptException ex) {
ex.printStackTrace();
}
Nashorn's 的 print 方法實現(xiàn)將其參數(shù)數(shù)據(jù)發(fā)送到控制臺. Hello, world! 消息出現(xiàn)在你的命令控制臺中. 如果在 NetBeans 或 Eclipse 等集中開發(fā)環(huán)境中運行此命令 (IDE) , 則輸出將顯示在 IDE 的 調(diào)試或輸出窗口中.
在應(yīng)用程序中使用腳本的最佳原因之一是允許用戶自定義其功能. 允許這種客制化的最簡單的方式是讀取客戶提供的腳本文件. eval 的重載方法可以使用 Reader 參數(shù), 你可以使用該參數(shù)來讀取來自外部文件的腳本.
在應(yīng)用程序的 JAR 文件之外查找資源可能會有問題 . 但是, 如果將腳本放置在classpath 的相對目錄或用戶明確定義的絕對位置中 , 則應(yīng)用程序可以可靠的找到腳本. 如果確定所有用戶自定義的腳本都存在于應(yīng)該程序的JAR目錄下的scripts 子目錄中 , 則應(yīng)確保 JAR 文件的子目錄位于 classpath . 只要你的應(yīng)用程序目錄位于 classpath, 你的應(yīng)用程序就應(yīng)該在 scripts 子目錄中找到客戶定義的腳本 . 你可以使用JAR 中的清單文件(manifest )中 Class-path 語句將 JAR 文件的相對目錄位置放在 classpath 下. JAR文件位置的相對路徑用 . 字符表示. 本位幣的 ScriptCalc demo 應(yīng)用程序使用 manifest.xml 文件,與Example 5中的文件類似.
Example 5: 在classpath 中間 . 有助于應(yīng)用程序查找具有相對于JAR文件的路徑的腳本.
Manifest-Version: 1.0
Ant-Version: Apache Ant 1.6.5
Created-By: 1.6.0-rc-b89 (Sun Microsystems Inc.)
Main-Class: com.sun.demo.calculator.Calculator
Class-Path: .
Example 6 顯示了如何計算客戶提供的文件. 文件名為 /scripts/F1.js, 位于應(yīng)用程序目錄下.
Example 6: eval 方法可以讀取腳本文件.**
ScriptEngineManager engineMgr = new ScriptEngineManager();
ScriptEngine engine = engineMgr.getEngineByName("ECMAScript");
InputStream is =
this.getClass().getResourceAsStream("/scripts/F1.js");
try {
Reader reader = new InputStreamReader(is);
engine.eval(reader);
} catch (ScriptException ex) {
ex.printStackTrace();
}
如何調(diào)用腳本函數(shù)
運行整個腳本很有用, 但是你可能只想調(diào)用特定的腳本函數(shù). 一些腳本引擎實現(xiàn)了 Invocable 接口. 如果引擎實現(xiàn)此接口, 則你可以調(diào)用,或者調(diào)用引擎提供的其他的特定的方法或函數(shù).
腳本引擎不是必須要實現(xiàn) Invocable 接口. 但是, JDK8 中包含的Nashorn JavaScript 技術(shù)實現(xiàn)了這個接口. 如果你的腳本包含一個名叫 sayHello 的函數(shù), 則可以通過將 ScriptEngine 對象強轉(zhuǎn)為 Invocable 對象并調(diào)用其 invokeFunction 方法來重復(fù)調(diào)用它. 另外, 如果你的腳本定了了對象,你可以使用 invokeMethod方法來調(diào)用對象的方法. Example 7 演示了如何使用此接口.
Example 7: 你可以使用 Invocable 接口來調(diào)用特定方法.
ScriptEngineManager engineMgr = new ScriptEngineManager();
ScriptEngine jsEngine = engineMgr.getEngineByName("js");
jsEngine.eval("function sayHello() {" +
" print('Hello, world!');" +
"}");
Invocable invocableEngine = (Invocable) jsEngine;
invocableEngine.invokeFunction("sayHello");
Example 7 將會在將 Hello, world! 打印到控制臺.
請注意 invokeMethod 和 invokeFunction 方法可能會引發(fā)多個異常, 因此你需要必火 ScriptException, NoSuchMethodException, 甚至是 NullPointerException 異常.
如何從腳本訪問 Java 對象
JSR 223 的實現(xiàn)提供允許訪問Java 類,方法,屬性的編程語言綁定. 對于該特定腳本環(huán)境的原生對象,訪問機制通常將遵循腳本語言的約定 .
如何將Java 對象放入腳本環(huán)境中?你可以使用Invocable 接口將對象作為參數(shù)傳遞到腳本過程中. 另外, 你也可以在其中 "put" 他們 : Java 編程語言代碼可以通過調(diào)用腳本引擎的 put 方法將Java對象放置到腳本環(huán)境中. 此方法將鍵值對放入一個 javax.script.Bindings 對象中, 該對象由腳本引擎維護. Bindings 對象是可以從引擎內(nèi)部訪問的 key-value 對的映射.
假設(shè)你有一個要處理的腳本名稱列表. Example 8 展示了在Java編程語言中需要處理的列表.
Example 8: Java 編程語言添加名稱到列表.
List<String> namesList = new ArrayList<String>();
namesList.add("Jill");
namesList.add("Bob");
namesList.add("Laureen");
namesList.add("Ed");
創(chuàng)建名為jsEngine的 ScriptEngine 對象后, 你可以將 namesList Java 對象傳遞到腳本環(huán)境中. put 方法需要一個String 和 Object 表示的一個 key-value 對. 在 Example 9, 腳本代碼可以使用 namesListKey 引用來訪問 namesList 的Java 對象.
Example 9: 腳本代碼訪問和修改原生的Java 對象.
jsEngine.put("namesListKey", namesList);
System.out.println("Executing in script environment...");
try {
jsEngine.eval("var x;" +
"var names = namesListKey.toArray();" +
"for(x in names) {" +
" print(names[x] );" +
"}" +
"namesListKey.add(\"Dana\");");
} catch (ScriptException ex) {
ex.printStackTrace();
}
System.out.println("Executing in Java environment...");
namesList.stream().forEach(System.out::println);
將 namesListKey 鍵值對綁定到腳本引擎作用域后 , 可以將Java對象用作腳本對象 . 使用 namesListKey 變量, 腳本可以訪問 namesList 對象. 在 Example 9 中 , 腳本打印出列表的名稱并添加新的元素 Dana. 從 eval 方法返回后,通過打印 namesList 內(nèi)容 , 該示例顯示腳本已經(jīng)成功訪問和修改了列表.
示例9打印了兩次列表 . 腳本打印該列表并添加了一個名稱 . 在執(zhí)行完腳本之后 , 代碼再次打印該列表, 表明腳本成功的修改了列表:
Executing in script environment...
Jill
Bob
Laureen
Ed
Executing in Java environment...
Jill
Bob
Laureen
Ed
Dana
你也可以使用Invocable 接口將namesList 對象傳遞給腳本代碼. 腳本代碼可以訪問和修改通過 Invocable 接口提供的過程參數(shù),而不是使用 key-value 對綁定機制. Example 10 展示老人如何通過 Invocable 接口使用 Java 對象 . 該代碼將 namesList 的值作為invokeFunction 方法的參數(shù)船體給腳本環(huán)境.
Example 10: 應(yīng)用程序可以使用Invocable接口將值傳遞給腳本.
Invocable invocableEngine = (Invocable)jsEngine;
try {
jsEngine.eval("function printNames1(namesList) {" +
" var x;" +
" var names = namesList.toArray();" +
" for(x in names) {" +
" print(names[x]);" +
" }" +
"}" +
"function addName(namesList, name) {" +
" namesList.add(name);" +
"}");
System.out.println("Executing in script environment...");
invocableEngine.invokeFunction("printNames1", namesList);
invocableEngine.invokeFunction("addName", namesList, "Dana");
System.out.println("Executing in Java environment...");
} catch (ScriptException ex) {
ex.printStackTrace();
} catch (NoSuchMethodException ex) {
ex.printStackTrace();
}
你還可以在腳本環(huán)境中創(chuàng)建新的 Java 對象. 你的腳本可以使用原生的Java類 . 你可以從腳本中創(chuàng)建一個Swing消息對話框, 替代將消息打印到控制臺 , 如 Example 11 所示.
Example 11: 腳本可以導(dǎo)入Java平臺程序包.
try {
jsEngine.eval("" +
"var optionPane = " +
" javax.swing.JOptionPane.showMessageDialog(null, 'Hello, world!');");
} catch (ScriptException ex) {
ex.printStackTrace();
}

如何訪問腳本對象
eval, invokeMethod, 和 invokeFunction 方法始終返回一個 Object 實例. 對于大多數(shù)腳本引擎, 此對象是你的腳本計算的最終值. 因此,在腳本環(huán)境中訪問對象的最簡單方法是從腳本函數(shù)中返回它們, 或者確保你的腳本計算了你所需的對象.
腳本引擎實現(xiàn)將會某些腳本數(shù)據(jù)類型映射到Java編程語言中對應(yīng)的類型 . 類如 , Nashorn 腳本引擎將數(shù)字 number 和string 類型映射成Java 編程語言的 Double and String 類型. 如果你確定返回類型,你可以強制轉(zhuǎn)化 eval , invokeMethod, 或者 invokeFunction 方法的返回值. 你應(yīng)該查閱腳本引擎文檔以獲取類型映射的詳細信息 . 當(dāng)然 , 你的腳本也可以創(chuàng)建和返回原生Java對象.
總結(jié)
JSR 223規(guī)范定義了Java平臺中的腳本。 Java SE 8平臺實現(xiàn)了此規(guī)范,JDK 6和JRE 6提供了Mozilla Rhino腳本引擎,JDK 8和JRE 8提供了Nashorn腳本引擎 以支持JavaScript技術(shù)。 還可以使用其他腳本引擎,你可以將它們作為常見的JAR擴展添加到您的運行時環(huán)境中。
出于多種原因,你可能需要在應(yīng)用程序中包含腳本支持:
復(fù)雜的配置選項.
用戶自定義功能.
發(fā)布應(yīng)用程序后易于維護.
用戶技術(shù)棧 -- 最終用戶可能熟悉其他腳本語言,而不是Java編程語言.
重用其他編程語言中的代碼模塊.
由于腳本的API相對較小,因此使用Java平臺的腳本很容易. 你可以僅使用 javax.script 包中少數(shù)接口和類將腳本支持快速的添加到應(yīng)用程序中.
更多信息請參考 JSR 223 specification, documentation, and reference implementation