2006 年底,Sun 公司發(fā)布了 Java Standard Edition 6(Java SE 6)的最終正式版,代號 Mustang(野馬)。跟 Tiger(Java SE 5)相比,Mustang 在性能方面有了不錯的提升。與 Tiger 在 API 庫方面的大幅度加強(qiáng)相比,雖然 Mustang 在 API 庫方面的新特性顯得不太多,但是也提供了許多實(shí)用和方便的功能:在腳本,WebService,XML,編譯器 API,數(shù)據(jù)庫,JMX,網(wǎng)絡(luò) 和 Instrumentation 方面都有不錯的新特性和功能加強(qiáng)。
本系列 文章主要介紹 Java SE 6 在 API 庫方面的部分新特性,通過一些例子和講解,幫助開發(fā)者在編程實(shí)踐當(dāng)中更好的運(yùn)用 Java SE 6,提高開發(fā)效率。本文是系列文章的第 6 篇,介紹了 Java SE 6 在腳本編程方面的新特性。
![]() |
|
Java SE 6 引入了對 Java Specification Request(JSR)233 的支持,JSR 233 旨在定義一個統(tǒng)一的規(guī)范,使得 Java 應(yīng)用程序可以通過一套固定的接口與各種腳本引擎交互,從而達(dá)到在 Java 平臺上調(diào)用各種腳本語言的目的。javax.script
包定義了這些接口,即 Java 腳本編程 API。Java 腳本 API 的目標(biāo)與 Apache 項(xiàng)目 Bean Script Framework(BSF)類似,通過它 Java 應(yīng)用程序就能通過虛擬機(jī)調(diào)用各種腳本,同時,腳本語言也能訪問應(yīng)用程序中的 Java 對象和方法。Java 腳本 API 是連通 Java 平臺和腳本語言的橋梁。首先,通過它為數(shù)眾多的現(xiàn)有 Java 庫就能被各種腳本語言所利用,節(jié)省了開發(fā)成本縮短了開發(fā)周期;其次,可以把一些復(fù)雜異變的業(yè)務(wù)邏輯交給腳本語言處理,這又大大提高了開發(fā)效率。
在 javax.script
包中定義的實(shí)現(xiàn)類并不多,主要是一些接口和對應(yīng)的抽象類,圖 1 顯示了其中包含的各個接口和類。
圖 1. javax.script 包概況

這個包的具體實(shí)現(xiàn)類少的根本原因是這個包只是定義了一個編程接口的框架規(guī)范,至于對如何解析運(yùn)行具體的腳本語言,還需要由第三方提供實(shí)現(xiàn)。雖然這些腳本引擎的實(shí)現(xiàn)各不相同,但是對于 Java 腳本 API 的使用者來說,這些具體的實(shí)現(xiàn)被很好的隔離隱藏了。Java 腳本 API 為開發(fā)者提供了如下功能:
- 獲取腳本程序輸入,通過腳本引擎運(yùn)行腳本并返回運(yùn)行結(jié)果,這是最核心的接口。
- 發(fā)現(xiàn)腳本引擎,查詢腳本引擎信息。
- 通過腳本引擎的運(yùn)行上下文在腳本和 Java 平臺間交換數(shù)據(jù)。
- 通過 Java 應(yīng)用程序調(diào)用腳本函數(shù)。
在詳細(xì)介紹這四個功能之前,我們先通過一個簡單的例子來展示如何通過 Java 語言來運(yùn)行腳本程序,這里仍然以經(jīng)典的“Hello World”開始。
清單 1. Hello World
import javax.script.*; public class HelloWorld { public static void main(String[] args) throws ScriptException { ScriptEngineManager manager = new ScriptEngineManager(); ScriptEngine engine = manager.getEngineByName("JavaScript"); engine.eval("print ('Hello World')"); } } |
這個例子非常直觀,只要通過 ScriptEngineManager
和 ScriptEngine
這兩個類就可以完成最簡單的調(diào)用。首先,ScriptEngineManager
實(shí)例創(chuàng)建一個 ScriptEngine
實(shí)例,然后返回的 ScriptEngine
實(shí)例解析 JavaScript 腳本,輸出運(yùn)行結(jié)果。運(yùn)行這段程序,終端上會輸出“Hello World“。在執(zhí)行 eval
函數(shù)的過程中可能會有 ScriptEngine
異常拋出,引發(fā)這個異常被拋出的原因一般是由腳本輸入語法有誤造成的。在對整個 API 有了大致的概念之后,我們就可以開始介紹各個具體的功能了。
![]() ![]() |
![]()
|
Java 腳本 API 通過腳本引擎來運(yùn)行腳本,整個包的目的就在于統(tǒng)一 Java 平臺與各種腳本引擎的交互方式,制定一個標(biāo)準(zhǔn),Java 應(yīng)用程序依照這種標(biāo)準(zhǔn)就能自由的調(diào)用各種腳本引擎,而腳本引擎按照這種標(biāo)準(zhǔn)實(shí)現(xiàn),就能被 Java 平臺支持。每一個腳本引擎就是一個腳本解釋器,負(fù)責(zé)運(yùn)行腳本,獲取運(yùn)行結(jié)果。ScriptEngine
接口是腳本引擎在 Java 平臺上的抽象,Java 應(yīng)用程序通過這個接口調(diào)用腳本引擎運(yùn)行腳本程序,并將運(yùn)行結(jié)果返回給虛擬機(jī)。
ScriptEngine
接口提供了許多 eval
函數(shù)的變體用來運(yùn)行腳本,這個函數(shù)的功能就是獲取腳本輸入,運(yùn)行腳本,最后返回輸出。清單 1 的例子中直接通過字符串作為 eval
函數(shù)的參數(shù)讀入腳本程序。除此之外,ScriptEngine
還提供了以一個 java.io.Reader
作為輸入?yún)?shù)的 eval
函數(shù)。腳本程序?qū)嵸|(zhì)上是一些可以用腳本引擎執(zhí)行的字節(jié)流,通過一個 Reader
對象,eval
函數(shù)就能從不同的數(shù)據(jù)源中讀取字節(jié)流來運(yùn)行,這個數(shù)據(jù)源可以來自內(nèi)存、文件,甚至直接來自網(wǎng)絡(luò)。這樣 Java 應(yīng)用程序就能直接利用項(xiàng)目原有的腳本資源,無需以 Java 語言對其進(jìn)行重寫,達(dá)到腳本程序與 Java 平臺無縫集成的目的。清單 2 即展示了如何從一個文件中讀取腳本程序并運(yùn)行,其中如何通過 ScriptEngineManager
獲取 ScriptEngine
實(shí)例的細(xì)節(jié)會在后面詳細(xì)介紹。
清單 2. Run Script
public class RunScript { public static void main(String[] args) throws Exception { String script = args[0]; String file = args[1]; FileReader scriptReader = new FileReader(new File(file)); ScriptEngineManager manager = new ScriptEngineManager(); ScriptEngine engine = manager.getEngineByName(script); engine.eval(scriptReader); } } |
清單 2 代碼,從命令行分別獲取腳本名稱和腳本文件名,程序通過腳本名稱創(chuàng)建對應(yīng)的腳本引擎實(shí)例,通過腳本名稱指定的腳本文件名讀入腳本程序運(yùn)行。運(yùn)行下面這個命令,就能在 Java 平臺上運(yùn)行所有的 JavaScript 腳本。
java RunScript javascript run.js |
通過這種方式,Java 應(yīng)用程序可以把一些復(fù)雜易變的邏輯過程,用更加靈活的弱類型的腳本語言來實(shí)現(xiàn),然后通過 javax.Script
包提供的 API 獲取運(yùn)行結(jié)果,當(dāng)腳本改變時,只需替換對應(yīng)的腳本文件,而無需重新編譯構(gòu)建項(xiàng)目,好處是顯而易見的,即節(jié)省了開發(fā)時間又提高了開發(fā)效率。
EngineScript
接口分別針對 String
輸入和 Reader
輸入提供了三個不同形態(tài)的 eval
函數(shù),用于運(yùn)行腳本:
表 1. ScriptEngine 的 eval 函數(shù)
函數(shù) | 描述 |
---|---|
Object eval(Reader reader) |
從一個 Reader 讀取腳本程序并運(yùn)行 |
Object eval(Reader reader, Bindings n) |
以 n 作為腳本級別的綁定,從一個 Reader 讀取腳本程序并運(yùn)行 |
Object eval(Reader reader, ScriptContext context) |
在 context 指定的上下文環(huán)境下,從一個 Reader 讀取腳本程序并運(yùn)行 |
Object eval(String script) |
運(yùn)行字符串表示的腳本 |
Object eval(String script, Bindings n) |
以 n 作為腳本級別的綁定,運(yùn)行字符串表示的腳本 |
Object eval(String script, ScriptContext context) |
在 context 指定的上下文環(huán)境下,運(yùn)行字符串表示的腳本 |
Java 腳本 API 還為 ScriptEngine
接口提供了一個抽象類 —— AbstractScriptEngine
,這個類提供了其中四個 eval
函數(shù)的默認(rèn)實(shí)現(xiàn),它們分別通過調(diào)用 eval(Reader,ScriptContext)
或 eval(String, ScriptContext)
來實(shí)現(xiàn)。這樣腳本引擎提供者,只需繼承這個抽象類并提供這兩個函數(shù)實(shí)現(xiàn)即可。AbstractScriptEngine
有一個保護(hù)域 context
用于保存默認(rèn)上下文的引用,SimpleScriptContext
類被作為 AbstractScriptEngine
的默認(rèn)上下文。關(guān)于上下文環(huán)境,將在后面進(jìn)行詳細(xì)介紹。
![]() ![]() |
![]()
|
在前面的兩個例子中,ScriptEngine
實(shí)例都是通過調(diào)用 ScriptEngineManager
實(shí)例的方法返回的,而不是常見的直接通過 new
操作新建一個實(shí)例。JSR 233 中引入 ScriptEngineManager
類的意義就在于,將 ScriptEngine
的尋找和創(chuàng)建任務(wù)委托給 ScriptEngineManager
實(shí)例處理,達(dá)到對 API 使用者隱藏這個過程的目的,使 Java 應(yīng)用程序在無需重新編譯的情況下,支持腳本引擎的動態(tài)替換。通過 ScriptEngineManager
類和 ScriptEngineFactory
接口即可完成腳本引擎的發(fā)現(xiàn)和創(chuàng)建:
ScriptEngineManager
類:自動尋找ScriptEngineFactory
接口的實(shí)現(xiàn)類ScriptEngineFactory
接口:創(chuàng)建合適的腳本引擎實(shí)例
![]() |
|
ScriptEngineManager
類本身并不知道如何創(chuàng)建一個具體的腳本引擎實(shí)例,它會依照 Jar 規(guī)約中定義的服務(wù)發(fā)現(xiàn)機(jī)制,查找并創(chuàng)建一個合適的 ScriptEngineFactory
實(shí)例,并通過這個工廠類來創(chuàng)建返回實(shí)際的腳本引擎。首先,ScriptEngineManager
實(shí)例會在當(dāng)前 classpath 中搜索所有可見的 Jar 包;然后,它會查看每個 Jar 包中的 META -INF/services/ 目錄下的是否包含 javax.script.ScriptEngineFactory
文件,腳本引擎的開發(fā)者會提供在 Jar 包中包含一個 ScriptEngineFactory
接口的實(shí)現(xiàn)類,這個文件內(nèi)容即是這個實(shí)現(xiàn)類的完整名字;ScriptEngineManager
會根據(jù)這個類名,創(chuàng)建一個 ScriptEngineFactory
接口的實(shí)例;最后,通過這個工廠類來實(shí)例化需要的腳本引擎,返回給用戶。舉例來說,第三方的引擎提供者可能升級更新了新版的腳本引擎實(shí)現(xiàn),通過 ScriptEngineManager
來管理腳本引擎,無需修改一行 Java 代碼就能替換更新腳本引擎。用戶只需在 classpath 中加入新的腳本引擎實(shí)現(xiàn)(Jar 包的形式),ScriptEngineManager
就能通過 Service Provider 機(jī)制來自動查找到新版本實(shí)現(xiàn),創(chuàng)建并返回對應(yīng)的腳本引擎實(shí)例供調(diào)用。圖 2 所示時序圖描述了其中的步驟:
圖 2. 腳本引擎發(fā)現(xiàn)機(jī)制時序圖

ScriptEngineFactory
接口的實(shí)現(xiàn)類被用來描述和實(shí)例化 ScriptEngine
接口,每一個實(shí)現(xiàn) ScriptEngine
接口的類會有一個對應(yīng)的工廠類來描述其元數(shù)據(jù)(meta data),ScriptEngineFactory
接口定義了許多函數(shù)供 ScriptEngineManager
查詢這些元數(shù)據(jù),ScriptEngineManager
會根據(jù)這些元數(shù)據(jù)查找需要的腳本引擎,表 2 列出了可供使用的函數(shù):
表 2. ScriptEngineFactory 提供的查詢函數(shù)
函數(shù) | 描述 |
---|---|
String getEngineName() |
返回腳本引擎的全稱 |
String getEngineVersion() |
返回腳本引擎的版本信息 |
String getLanguageName() |
返回腳本引擎所支持的腳本語言的名稱 |
String getLanguageVersion() |
返回腳本引擎所支持的腳本語言的版本信息 |
List<String> getExtensions() |
返回一個腳本文件擴(kuò)展名組成的 List,當(dāng)前腳本引擎支持解析這些擴(kuò)展名對應(yīng)的腳本文件 |
List<String> getMimeTypes() |
返回一個與當(dāng)前引擎關(guān)聯(lián)的所有 mimetype 組成的 List |
List<String> getNames() |
返回一個當(dāng)前引擎所有名稱的 List,ScriptEngineManager 可以根據(jù)這些名字確定對應(yīng)的腳本引擎 |
通過 getEngineFactories()
函數(shù),ScriptEngineManager
會返回一個包含當(dāng)前環(huán)境中被發(fā)現(xiàn)的所有實(shí)現(xiàn) ScriptEngineFactory
接口的具體類,通過這些工廠類中保存的腳本引擎信息檢索需要的腳本引擎。第三方提供的腳本引擎實(shí)現(xiàn)的 Jar 包中除了包含 ScriptEngine
接口的實(shí)現(xiàn)類之外,還需要提供 ScriptEngineFactory
接口的實(shí)現(xiàn)類,以及一個 javax.script.ScriptEngineFactory
文件用于指明這個工廠類。這樣,Java 平臺就能通過 ScriptEngineManager
尋找到這個工廠類,并通過這個工廠類為用戶提供一個腳本引擎實(shí)例。Java SE 6 默認(rèn)提供了 JavaScirpt 腳本引擎的實(shí)現(xiàn),如果需要支持其他腳本引擎,需要將它們對應(yīng)的 Jar 包包含在 classpath 中,比如對于前面 清單 2 中的代碼,只需在運(yùn)行程序前將 Groovy 的腳本引擎添加到 classpath 中,然后運(yùn)行:
java RunScript groovy run.groovy |
無需修改一行 Java 代碼就能以 Groovy 腳本引擎來運(yùn)行 Groovy 腳本。在 這里 為 Java SE 6 提供了許多著名腳本語言的腳本引擎對 JSR 233 的支持,這些 Jar 必須和腳本引擎配合使用,使得這些腳本語言能被 Java 平臺支持。到目前為止,它提供了至少 25 種腳本語言的支持,其中包括了 Groovy、Ruby、Python 等當(dāng)前非常流行的腳本語言。這里需要再次強(qiáng)調(diào)的是,負(fù)責(zé)創(chuàng)建 ScriptEngine
實(shí)例的 ScriptEngineFactory
實(shí)現(xiàn)類對于用戶來說是不可見的,ScriptEngingeManager
實(shí)現(xiàn)負(fù)責(zé)與其交互,通過它創(chuàng)建腳本引擎。
![]() ![]() |
![]()
|
如果僅僅是通過腳本引擎運(yùn)行腳本的話,還無法體現(xiàn)出 Java 腳本 API 的優(yōu)點(diǎn),在 JSR 233 中,還為所有的腳本引擎定義了一個簡潔的執(zhí)行環(huán)境。我們都知道,在 Linux 操作系統(tǒng)中可以維護(hù)許多環(huán)境變量比如 classpath、path 等,不同的 shell 在運(yùn)行時可以直接使用這些環(huán)境變量,它們構(gòu)成了 shell 腳本的執(zhí)行環(huán)境。在 javax.script
支持的每個腳本引擎也有各自對應(yīng)的執(zhí)行的環(huán)境,腳本引擎可以共享同樣的環(huán)境,也可以有各自不同的上下文。通過腳本運(yùn)行時的上下文,腳本程序就能自由的和 Java 平臺交互,并充分利用已有的眾多 Java API,真正的站在“巨人”的肩膀上。javax.script.ScriptContext
接口和 javax.script.Bindings
接口定義了腳本引擎的上下文。
- Bindings 接口:
繼承自 Map,定義了對這些“鍵-值”對的查詢、添加、刪除等 Map 典型操作。
Bingdings
接口實(shí)際上是一個存放數(shù)據(jù)的容器,它的實(shí)現(xiàn)類會維護(hù)許多“鍵-值”對,它們都通過字符串表示。Java 應(yīng)用程序和腳本程序通過這些“鍵-值”對交換數(shù)據(jù)。只要腳本引擎支持,用戶還能直接在Bindings
中放置 Java 對象,腳本引擎通過Bindings
不僅可以存取對象的屬性,還能調(diào)用 Java 對象的方法,這種雙向自由的溝通使得二者真正的結(jié)合在了一起。 - ScriptContext 接口:
將
Bindings
和ScriptEngine
聯(lián)系在了一起,每一個ScriptEngine
都有一個對應(yīng)的ScriptContext
,前面提到過通過ScriptEnginFactory
創(chuàng)建腳本引擎除了達(dá)到隱藏實(shí)現(xiàn)的目的外,還負(fù)責(zé)為腳本引擎設(shè)置合適的上下文。ScriptEngine
通過ScriptContext
實(shí)例就能從其內(nèi)部的Bindings
中獲得需要的屬性值。ScriptContext
接口默認(rèn)包含了兩個級別的Bindings
實(shí)例的引用,分別是全局級別和引擎級別,可以通過GLOBAL_SCOPE
和ENGINE_SCOPE
這兩個類常量來界定區(qū)分這兩個Bindings
實(shí)例,其中GLOBAL_SCOPE
從創(chuàng)建它的ScriptEngineManager
獲得。顧名思義,全局級別指的是Bindings
里的屬性都是“全局變量”,只要是同一個ScriptEngineMananger
返回的腳本引擎都可以共享這些屬性;對應(yīng)的,引擎級別的Bindings
里的屬性則是“局部變量”,它們只對同一個引擎實(shí)例可見,從而能為不同的引擎設(shè)置獨(dú)特的環(huán)境,通過同一個腳本引擎運(yùn)行的腳本運(yùn)行時能共享這些屬性。
ScriptContext
接口定義了下面這些函數(shù)來存取數(shù)據(jù):
表 3. ScriptContext 存取屬性函數(shù)
函數(shù) | 描述 |
---|---|
Object removeAttribute(String name, int scope) |
從指定的范圍里刪除一個屬性 |
void setAttribute(String name, Object value, int scope) |
在指定的范圍里設(shè)置一個屬性的值 |
Object getAttribute(String name) |
從上下文的所有范圍內(nèi)獲取優(yōu)先級最高的屬性的值 |
Object getAttribute(String name, int scope) |
從指定的范圍里獲取屬性值 |
ScriptEngineManager
擁有一個全局性的 Bindings
實(shí)例,在通過 ScriptEngineFactory
實(shí)例創(chuàng)建 ScriptEngine
后,它把自己的這個 Bindings
傳遞給所有它創(chuàng)建的 ScriptEngine
實(shí)例,作為 GLOBAL_SCOPE
。同時,每一個 ScriptEngine
實(shí)例都對應(yīng)一個 ScriptContext
實(shí)例,這個 ScriptContext
除了從 ScriptEngineManager
那獲得的 GLOBAL_SCOPE
,自己也維護(hù)一個 ENGINE_SCOPE
的 Bindings
實(shí)例,所有通過這個腳本引擎運(yùn)行的腳本,都能存取其中的屬性。除了 ScriptContext
可以設(shè)置屬性,改變內(nèi)部的 Bindings
,Java 腳本 API 為 ScriptEngineManager
和 ScriptEngine
也提供了類似的設(shè)置屬性和 Bindings
的 API。
圖 3. Bindings 在 Java 腳本 API 中的分布

從 圖 3 中可以看到,共有三個級別的地方可以存取屬性,分別是 ScriptEngineManager
中的 Bindings
,ScriptEngine
實(shí)例對應(yīng)的 ScriptContext
中含有的 Bindings
,以及調(diào)用 eval
函數(shù)時傳入的 Bingdings
。離函數(shù)調(diào)用越近,其作用域越小,優(yōu)先級越高,相當(dāng)于編程語言中的變量的可見域,即 Object getAttribute(String name)
中提到的優(yōu)先級。從 清單 3 這個例子中可以看出各個屬性的存取優(yōu)先級:
清單 3. 上下文屬性的作用域
import javax.script.*; public class ScopeTest { public static void main(String[] args) throws Exception { String script=" println(greeting) "; ScriptEngineManager manager = new ScriptEngineManager(); ScriptEngine engine = manager.getEngineByName("javascript"); //Attribute from ScriptEngineManager manager.put("greeting", "Hello from ScriptEngineManager"); engine.eval(script); //Attribute from ScriptEngine engine.put("greeting", "Hello from ScriptEngine"); engine.eval(script); //Attribute from eval method ScriptContext context = new SimpleScriptContext(); context.setAttribute("greeting", "Hello from eval method", ScriptContext.ENGINE_SCOPE); engine.eval(script,context); } } |
JavaScript 腳本 println(greeting)
在這個程序中被重復(fù)調(diào)用了三次,由于三次調(diào)用的環(huán)境不一樣,導(dǎo)致輸出也不一樣,greeting
變量每一次都被優(yōu)先級更高的也就是距離函數(shù)調(diào)用越近的值覆蓋。從這個例子同時也演示了如何使用 ScriptContext
和 Bindings
這兩個接口,在例子腳本中并沒有定義 greeting
這個變量,但是腳本通過 Java 腳本 API 能方便的存取 Java 應(yīng)用程序中的對象,輸出 greeting
相應(yīng)的值。運(yùn)行這個程序后,能看到輸出為:
圖 4. 程序 ScopeTest 的輸出

除了能在 Java 平臺與腳本程序之間的提供共享屬性之外,ScriptContext
還允許用戶重定向引擎執(zhí)行時的輸入輸出流:
表 4. ScriptContext 輸入輸出重定向
函數(shù) | 描述 |
---|---|
void setErrorWriter(Writer writer) |
重定向錯誤輸出,默認(rèn)是標(biāo)準(zhǔn)錯誤輸出 |
void setReader(Reader reader) |
重定向輸入,默認(rèn)是標(biāo)準(zhǔn)輸入 |
void setWriter(Writer writer) |
重定向輸出,默認(rèn)是標(biāo)準(zhǔn)輸出 |
Writer getErrorWriter() |
獲取當(dāng)前錯誤輸出字節(jié)流 |
Reader getReader() |
獲取當(dāng)前輸入流 |
Writer getWriter() |
獲取當(dāng)前輸出流 |
清單 4 展示了如何通過 ScriptContext
將其對應(yīng)的 ScriptEngine
標(biāo)準(zhǔn)輸出重定向到一個 PrintWriter
中,用戶可以通過與這個 PrintWriter
連通的 PrintReader
讀取實(shí)際的輸出,使 Java 應(yīng)用程序能獲取腳本運(yùn)行輸出,滿足更加多樣的應(yīng)用需求。
清單 4. 重定向腳本輸出
import java.io.*; import javax.script.*; public class Redirectory { public static void main(String[] args) throws Exception { ScriptEngineManager manager = new ScriptEngineManager(); ScriptEngine engine = manager.getEngineByName("javascript"); PipedReader pr = new PipedReader(); PipedWriter pw = new PipedWriter(pr); PrintWriter writer = new PrintWriter(pw); engine.getContext().setWriter(writer); String script = "println('Hello from JavaScript')"; engine.eval(script); BufferedReader br =new BufferedReader(pr); System.out.println(br.readLine()); } } |
Java 腳本 API 分別為這兩個接口提供了一個簡單的實(shí)現(xiàn)供用戶使用。SimpleBindings
通過組合模式實(shí)現(xiàn) Map
接口,它提供了兩個構(gòu)造函數(shù)。無參構(gòu)造函數(shù)在內(nèi)部構(gòu)造一個 HashMap
實(shí)例來實(shí)現(xiàn) Map
接口要求的功能;同時,SimpleBindings
也提供了一個以 Map
接口作為參數(shù)的構(gòu)造函數(shù),允許任何實(shí)現(xiàn) Map
接口的類作為其組合的實(shí)例,以滿足不同的要求。SimpleScriptContext
提供了 ScriptContext
簡單實(shí)現(xiàn)。默認(rèn)情況下,它使用了標(biāo)準(zhǔn)輸入、標(biāo)準(zhǔn)輸出和標(biāo)準(zhǔn)錯誤輸出,同時維護(hù)一個 SimpleBindings
作為其引擎級別的 Bindings,它的默認(rèn)全局級別 Bindings 為空。
![]() ![]() |
![]()
|
在 Java 腳本 API 中還有兩個腳本引擎可以選擇是否實(shí)現(xiàn)的接口,這個兩個接口不是強(qiáng)制要求實(shí)現(xiàn)的,即并非所有的腳本引擎都能支持這兩個函數(shù),不過 Java SE 6 自帶的 JavaScript 引擎支持這兩個接口。無論如何,這兩個接口提供了非常實(shí)用的功能,它們分別是:
- Invocable 接口:允許 Java 平臺調(diào)用腳本程序中的函數(shù)或方法。
- Compilable 接口:允許 Java 平臺編譯腳本程序,供多次調(diào)用。
有時候,用戶可能并不需要運(yùn)行已有的整個腳本程序,而僅僅需要調(diào)用其中的一個過程,或者其中某個對象的方法,這個時候 Invocable
接口就能發(fā)揮作用。它提供了兩個函數(shù) invokeFunction
和 invokeMethod
,分別允許 Java 應(yīng)用程序直接調(diào)用腳本中的一個全局性的過程以及對象中的方法,調(diào)用后者時,除了指定函數(shù)名字和參數(shù)外,還需要傳入要調(diào)用的對象引用,當(dāng)然這需要腳本引擎的支持。不僅如此,Invocable
接口還允許 Java 應(yīng)用程序從這些函數(shù)中直接返回一個接口,通過這個接口實(shí)例來調(diào)用腳本中的函數(shù)或方法,從而我們可以從腳本中動態(tài)的生成 Java 應(yīng)用中需要的接口對象。清單 5 演示了如何使用一個 Invocable
接口:
清單 5. 調(diào)用腳本中的函數(shù)
import javax.script.*; public class CompilableTest { public static void main(String[] args) throws ScriptException, NoSuchMethodException { String script = " function greeting(message){println (message);}"; ScriptEngineManager manager = new ScriptEngineManager(); ScriptEngine engine = manager.getEngineByName("javascript"); engine.eval(script); if (engine instanceof Invocable) { Invocable invocable = (Invocable) engine; invocable.invokeFunction("greeting", "hi"); // It may through NoSuchMethodException try { invocable.invokeFunction("nogreeing"); } catch (NoSuchMethodException e) { // expected } } } } |
在調(diào)用函數(shù)前,可以先通過 instanceof
操作判斷腳本引擎是否支持編譯操作,防止類型轉(zhuǎn)換時拋出運(yùn)行時異常,需要特別注意的時,如果調(diào)用了腳本程序中不存在的函數(shù)時,運(yùn)行時會拋出一個 NoSuchMethodException
的異常,實(shí)際開發(fā)中應(yīng)該注意處理這種特殊情況。
一般來說,腳本語言都是解釋型的,這也是腳本語言區(qū)別與編譯語言的一個特點(diǎn),解釋性意味著腳本隨時可以被運(yùn)行,開發(fā)者可以邊開發(fā)邊查看接口,從而省去了編譯這個環(huán)節(jié),提供了開發(fā)效率。但是這也是一把雙刃劍,當(dāng)腳本規(guī)模變大,重復(fù)解釋一段穩(wěn)定的代碼又會帶來運(yùn)行時的開銷。有些腳本引擎支持將腳本運(yùn)行編譯成某種中間形式,這取決與腳本語言的性質(zhì)以及腳本引擎的實(shí)現(xiàn),可以是一些操作碼,甚至是 Java 字節(jié)碼文件。實(shí)現(xiàn)了這個接口的腳本引擎能把輸入的腳本預(yù)編譯并緩存,從而提高多次運(yùn)行相同腳本的效率。
Java 腳本 API 還為這個中間形式提供了一個專門的類,每次調(diào)用 Compilable
接口的編譯函數(shù)都會返回一個 CompiledScript
實(shí)例。CompiledScript
類被用來保存編譯的結(jié)果,從而能重復(fù)調(diào)用腳本而沒有重復(fù)解釋的開銷,實(shí)際效率提高的多少取決于中間形式的徹底程度,其中間形式越接近低級語言,提高的效率就越高。每一個 CompiledScript
實(shí)例對應(yīng)于一個腳本引擎實(shí)例,一個腳本引擎實(shí)例可以含有多個 CompiledScript
(這很容易理解),調(diào)用 CompiledScript
的 eval
函數(shù)會傳遞給這個關(guān)聯(lián)的 ScriptEngine 的 eval
函數(shù)。關(guān)于 CompiledScript
類需要注意的是,它運(yùn)行時對與之對應(yīng)的 ScriptEngine 狀態(tài)的改變可能會傳遞給下一次調(diào)用,造成運(yùn)行結(jié)果的不一致。清單 6 演示了如何使用 Compiable
接口來調(diào)用腳本:
清單 6. 編譯腳本
import javax.script.*; public class CompilableTest { public static void main(String[] args) throws ScriptException { String script = " println (greeting); greeting= 'Good Afternoon!' "; ScriptEngineManager manager = new ScriptEngineManager(); ScriptEngine engine = manager.getEngineByName("javascript"); engine.put("greeting", "Good Morning!"); if (engine instanceof Compilable) { Compilable compilable = (Compilable) engine; CompiledScript compiledScript = compilable.compile(script); compiledScript.eval(); compiledScript.eval(); } } } |
與 InovcableTest
類似,也應(yīng)該先通過 instanceof
操作判斷腳本引擎是否支持編譯操作,防止預(yù)料外的異常拋出。并且我們可以發(fā)現(xiàn)同一段編譯過的腳本,在第二次運(yùn)行時 greeting
變量的內(nèi)容被上一次的運(yùn)行改變了,導(dǎo)致輸出不一致:
圖 5. 程序 CompilableTest 的輸出

![]() ![]() |
![]()
|
Java SE 6 還為運(yùn)行腳本添加了一個專門的工具 —— jrunscript。jrunscript 支持兩種運(yùn)行方式:一種是交互式,即邊讀取邊解析運(yùn)行,這種方式使得用戶可以方便調(diào)試腳本程序,馬上獲取預(yù)期結(jié)果;還有一種就是批處理式,即讀取并運(yùn)行整個腳本文件。用戶可以把它想象成一個萬能腳本解釋器,即它可以運(yùn)行任意腳本程序,而且它還是跨平臺的,當(dāng)然所有這一切都有一個前提,那就是必須告訴它相應(yīng)的腳本引擎的位置。默認(rèn)即支持的腳本是 JavaScript,這意味著用戶可以無需任何設(shè)置,通過 jrunscript 在任何支持 Java 的平臺上運(yùn)行任何 JavaScript 腳本;如果想運(yùn)行其他腳本,可以通過 -l
指定以何種腳本引擎運(yùn)行腳本。不過這個工具仍是實(shí)驗(yàn)性質(zhì)的,不一定會包含在 Java 的后續(xù)版本中,無論如何,它仍是一個非常有用的工具。
![]() ![]() |
![]()
|
在 Java 平臺上使用腳本語言編程非常方便,因?yàn)?Java 腳本 API 相對其他包要小很多。通過 javax.script
包提供的接口和類我們可以很方便為我們的 Java 應(yīng)用程序添加對腳本語言的支持。開發(fā)者只要遵照 Java 腳本 API 開發(fā)應(yīng)用程序,開發(fā)中就無需關(guān)注具體的腳本語言細(xì)節(jié),應(yīng)用程序就可以動態(tài)支持任何符合 JSR 233 標(biāo)準(zhǔn)的腳本語言,不僅如此,只要按照 JSR 233 標(biāo)準(zhǔn)開發(fā),用戶甚至還能為 Java 平臺提供一個自定義腳本語言的解釋器。在 Java 平臺上運(yùn)行自己的腳本語言,這對于眾多開發(fā)者來說都是非常有誘惑力的。
- 閱讀 Java SE 6 新特性系列 文章的完整列表,了解 Java SE 6 其它重要的增強(qiáng)。
- developerWorks 文章“動態(tài)調(diào)用動態(tài)語言,第 1 部分: 引入 Java 腳本 API”:介紹了 Java 腳本 API 的各種特性,并使用一個簡單的 Hello World 應(yīng)用程序展示 Java 代碼如何執(zhí)行腳本代碼以及腳本如何反過來執(zhí)行 Java 代碼。
- developerWorks 文章“動態(tài)調(diào)用動態(tài)語言,第 2 部分: 在運(yùn)行時尋找、執(zhí)行和修改腳本”:進(jìn)一步講解了 Java 腳本 API 的功能,演示如何在無需停止并重新啟動應(yīng)用程序的情況下,在運(yùn)行時執(zhí)行外部 Ruby、Groovy 和 JavaScript 腳本以修改業(yè)務(wù)邏輯。
- developerWorks 文章“給 Java SE 注入腳本語言的活力”:這篇文章在 Java SE 6 正式發(fā)布之前預(yù)覽了 Java 腳本 API 的功能。
- Java SE 6 文檔:Java SE 6 的規(guī)范文檔,可以找到絕大部分新特性的官方說明。
- JSR 233:詳細(xì)描述了腳本語言和 Java 平臺交互的規(guī)范。
- Java SE 6 的規(guī)范文檔上關(guān)于 Java 腳本編程的 教程。
- 這個網(wǎng)站 為許多著名腳本語言的腳本引擎提供了對 JSR 233 的支持。
- Sun 的 Java SE 6 實(shí)現(xiàn)包含了 Rhino 版本 1.6R2 的腳本引擎,Rhino 是一個完全用 Java 開發(fā)的開源的 JavaScript 實(shí)現(xiàn)。
- JAR 規(guī)范中對 Service Provider 的介紹。
![]() |
||
|
![]() |
邱小俠,目前就職于 IBM 中國開發(fā)中心 Harmony 開發(fā)團(tuán)隊(duì),負(fù)責(zé)類庫開發(fā)工作。 對開源軟件,Java 編程,項(xiàng)目構(gòu)建均抱有濃厚的興趣。您可以通過 qiuxiaox@cn.ibm.com 聯(lián)系到他。 |