| 2007 年 9 月 28 日 2006 年底,Sun 公司發(fā)布了 Java Standard Edition 6(Java SE 6)的最終正式版,代號 Mustang(野馬)。跟 Tiger(Java SE 5)相比,Mustang 在性能方面有了不錯的提升。與 Tiger 在 API 庫方面的大幅度加強相比,雖然 Mustang 在 API 庫方面的新特性顯得不太多,但是也提供了許多實(shí)用和方便的功能:在腳本,WebService,XML,編譯器 API,數據庫,JMX,網(wǎng)絡(luò ) 和 Instrumentation 方面都有不錯的新特性和功能加強。 本系列 文章主要介紹 Java SE 6 在 API 庫方面的部分新特性,通過(guò)一些例子和講解,幫助開(kāi)發(fā)者在編程實(shí)踐當中更好的運用 Java SE 6,提高開(kāi)發(fā)效率。本文是系列文章的第 6 篇,介紹了 Java SE 6 在腳本編程方面的新特性。 Java 腳本 API 概述 | 腳本引擎 腳本引擎就是指腳本的運行環(huán)境,它能能夠把運行其上的解釋性語(yǔ)言轉換為更底層的匯編語(yǔ)言,沒(méi)有腳本引擎,腳本就無(wú)法被運行。 | | Java SE 6 引入了對 Java Specification Request(JSR)233 的支持,JSR 233 旨在定義一個(gè)統一的規范,使得 Java 應用程序可以通過(guò)一套固定的接口與各種腳本引擎交互,從而達到在 Java 平臺上調用各種腳本語(yǔ)言的目的。javax.script 包定義了這些接口,即 Java 腳本編程 API。Java 腳本 API 的目標與 Apache 項目 Bean Script Framework(BSF)類(lèi)似,通過(guò)它 Java 應用程序就能通過(guò)虛擬機調用各種腳本,同時(shí),腳本語(yǔ)言也能訪(fǎng)問(wèn)應用程序中的 Java 對象和方法。Java 腳本 API 是連通 Java 平臺和腳本語(yǔ)言的橋梁。首先,通過(guò)它為數眾多的現有 Java 庫就能被各種腳本語(yǔ)言所利用,節省了開(kāi)發(fā)成本縮短了開(kāi)發(fā)周期;其次,可以把一些復雜異變的業(yè)務(wù)邏輯交給腳本語(yǔ)言處理,這又大大提高了開(kāi)發(fā)效率。 在 javax.script 包中定義的實(shí)現類(lèi)并不多,主要是一些接口和對應的抽象類(lèi),圖 1 顯示了其中包含的各個(gè)接口和類(lèi)。 圖 1. javax.script 包概況 這個(gè)包的具體實(shí)現類(lèi)少的根本原因是這個(gè)包只是定義了一個(gè)編程接口的框架規范,至于對如何解析運行具體的腳本語(yǔ)言,還需要由第三方提供實(shí)現。雖然這些腳本引擎的實(shí)現各不相同,但是對于 Java 腳本 API 的使用者來(lái)說(shuō),這些具體的實(shí)現被很好的隔離隱藏了。Java 腳本 API 為開(kāi)發(fā)者提供了如下功能: - 獲取腳本程序輸入,通過(guò)腳本引擎運行腳本并返回運行結果,這是最核心的接口。
- 發(fā)現腳本引擎,查詢(xún)腳本引擎信息。
- 通過(guò)腳本引擎的運行上下文在腳本和 Java 平臺間交換數據。
- 通過(guò) Java 應用程序調用腳本函數。
在詳細介紹這四個(gè)功能之前,我們先通過(guò)一個(gè)簡(jiǎn)單的例子來(lái)展示如何通過(guò) Java 語(yǔ)言來(lái)運行腳本程序,這里仍然以經(jīng)典的“Hello World”開(kāi)始。 清單 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')"); } } | 這個(gè)例子非常直觀(guān),只要通過(guò) ScriptEngineManager 和 ScriptEngine 這兩個(gè)類(lèi)就可以完成最簡(jiǎn)單的調用。首先,ScriptEngineManager 實(shí)例創(chuàng )建一個(gè) ScriptEngine 實(shí)例,然后返回的 ScriptEngine 實(shí)例解析 JavaScript 腳本,輸出運行結果。運行這段程序,終端上會(huì )輸出“Hello World“。在執行 eval 函數的過(guò)程中可能會(huì )有 ScriptEngine 異常拋出,引發(fā)這個(gè)異常被拋出的原因一般是由腳本輸入語(yǔ)法有誤造成的。在對整個(gè) API 有了大致的概念之后,我們就可以開(kāi)始介紹各個(gè)具體的功能了。
使用腳本引擎運行腳本 Java 腳本 API 通過(guò)腳本引擎來(lái)運行腳本,整個(gè)包的目的就在于統一 Java 平臺與各種腳本引擎的交互方式,制定一個(gè)標準,Java 應用程序依照這種標準就能自由的調用各種腳本引擎,而腳本引擎按照這種標準實(shí)現,就能被 Java 平臺支持。每一個(gè)腳本引擎就是一個(gè)腳本解釋器,負責運行腳本,獲取運行結果。ScriptEngine 接口是腳本引擎在 Java 平臺上的抽象,Java 應用程序通過(guò)這個(gè)接口調用腳本引擎運行腳本程序,并將運行結果返回給虛擬機。 ScriptEngine 接口提供了許多 eval 函數的變體用來(lái)運行腳本,這個(gè)函數的功能就是獲取腳本輸入,運行腳本,最后返回輸出。清單 1 的例子中直接通過(guò)字符串作為 eval 函數的參數讀入腳本程序。除此之外,ScriptEngine 還提供了以一個(gè) java.io.Reader 作為輸入參數的 eval 函數。腳本程序實(shí)質(zhì)上是一些可以用腳本引擎執行的字節流,通過(guò)一個(gè) Reader 對象,eval 函數就能從不同的數據源中讀取字節流來(lái)運行,這個(gè)數據源可以來(lái)自?xún)却?、文件,甚至直接?lái)自網(wǎng)絡(luò )。這樣 Java 應用程序就能直接利用項目原有的腳本資源,無(wú)需以 Java 語(yǔ)言對其進(jìn)行重寫(xiě),達到腳本程序與 Java 平臺無(wú)縫集成的目的。清單 2 即展示了如何從一個(gè)文件中讀取腳本程序并運行,其中如何通過(guò) ScriptEngineManager 獲取 ScriptEngine 實(shí)例的細節會(huì )在后面詳細介紹。 清單 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 代碼,從命令行分別獲取腳本名稱(chēng)和腳本文件名,程序通過(guò)腳本名稱(chēng)創(chuàng )建對應的腳本引擎實(shí)例,通過(guò)腳本名稱(chēng)指定的腳本文件名讀入腳本程序運行。運行下面這個(gè)命令,就能在 Java 平臺上運行所有的 JavaScript 腳本。 java RunScript javascript run.js | 通過(guò)這種方式,Java 應用程序可以把一些復雜易變的邏輯過(guò)程,用更加靈活的弱類(lèi)型的腳本語(yǔ)言來(lái)實(shí)現,然后通過(guò) javax.Script 包提供的 API 獲取運行結果,當腳本改變時(shí),只需替換對應的腳本文件,而無(wú)需重新編譯構建項目,好處是顯而易見(jiàn)的,即節省了開(kāi)發(fā)時(shí)間又提高了開(kāi)發(fā)效率。 EngineScript 接口分別針對 String 輸入和 Reader 輸入提供了三個(gè)不同形態(tài)的 eval 函數,用于運行腳本: 表 1. ScriptEngine 的 eval 函數 | 函數 | 描述 | Object eval(Reader reader) | 從一個(gè) Reader 讀取腳本程序并運行 | Object eval(Reader reader, Bindings n) | 以 n 作為腳本級別的綁定,從一個(gè) Reader 讀取腳本程序并運行 | Object eval(Reader reader, ScriptContext context) | 在 context 指定的上下文環(huán)境下,從一個(gè) Reader 讀取腳本程序并運行 | Object eval(String script) | 運行字符串表示的腳本 | Object eval(String script, Bindings n) | 以 n 作為腳本級別的綁定,運行字符串表示的腳本 | Object eval(String script, ScriptContext context) | 在 context 指定的上下文環(huán)境下,運行字符串表示的腳本 | Java 腳本 API 還為 ScriptEngine 接口提供了一個(gè)抽象類(lèi) —— AbstractScriptEngine,這個(gè)類(lèi)提供了其中四個(gè) eval 函數的默認實(shí)現,它們分別通過(guò)調用 eval(Reader,ScriptContext) 或 eval(String, ScriptContext) 來(lái)實(shí)現。這樣腳本引擎提供者,只需繼承這個(gè)抽象類(lèi)并提供這兩個(gè)函數實(shí)現即可。AbstractScriptEngine 有一個(gè)保護域 context 用于保存默認上下文的引用,SimpleScriptContext 類(lèi)被作為 AbstractScriptEngine 的默認上下文。關(guān)于上下文環(huán)境,將在后面進(jìn)行詳細介紹。
發(fā)現和創(chuàng )建腳本引擎 在前面的兩個(gè)例子中,ScriptEngine 實(shí)例都是通過(guò)調用 ScriptEngineManager 實(shí)例的方法返回的,而不是常見(jiàn)的直接通過(guò) new 操作新建一個(gè)實(shí)例。JSR 233 中引入 ScriptEngineManager 類(lèi)的意義就在于,將 ScriptEngine 的尋找和創(chuàng )建任務(wù)委托給 ScriptEngineManager 實(shí)例處理,達到對 API 使用者隱藏這個(gè)過(guò)程的目的,使 Java 應用程序在無(wú)需重新編譯的情況下,支持腳本引擎的動(dòng)態(tài)替換。通過(guò) ScriptEngineManager 類(lèi)和 ScriptEngineFactory 接口即可完成腳本引擎的發(fā)現和創(chuàng )建: ScriptEngineManager 類(lèi):自動(dòng)尋找 ScriptEngineFactory 接口的實(shí)現類(lèi) ScriptEngineFactory 接口:創(chuàng )建合適的腳本引擎實(shí)例 | Service Provider 服務(wù)(service)是指那些成為事實(shí)上標準的接口,服務(wù)提供者(service provider)則提供了這個(gè)接口的具體實(shí)現。不同的提供者會(huì )遵循同樣的接口提供實(shí)現,客戶(hù)可以自由選擇不同的實(shí)現??梢詮?Sun 提供的文檔 Jar 文件規約 中獲取有關(guān) Service Provider 更詳細的信息。 | | ScriptEngineManager 類(lèi)本身并不知道如何創(chuàng )建一個(gè)具體的腳本引擎實(shí)例,它會(huì )依照 Jar 規約中定義的服務(wù)發(fā)現機制,查找并創(chuàng )建一個(gè)合適的 ScriptEngineFactory 實(shí)例,并通過(guò)這個(gè)工廠(chǎng)類(lèi)來(lái)創(chuàng )建返回實(shí)際的腳本引擎。首先,ScriptEngineManager 實(shí)例會(huì )在當前 classpath 中搜索所有可見(jiàn)的 Jar 包;然后,它會(huì )查看每個(gè) Jar 包中的 META -INF/services/ 目錄下的是否包含 javax.script.ScriptEngineFactory 文件,腳本引擎的開(kāi)發(fā)者會(huì )提供在 Jar 包中包含一個(gè) ScriptEngineFactory 接口的實(shí)現類(lèi),這個(gè)文件內容即是這個(gè)實(shí)現類(lèi)的完整名字;ScriptEngineManager 會(huì )根據這個(gè)類(lèi)名,創(chuàng )建一個(gè) ScriptEngineFactory 接口的實(shí)例;最后,通過(guò)這個(gè)工廠(chǎng)類(lèi)來(lái)實(shí)例化需要的腳本引擎,返回給用戶(hù)。舉例來(lái)說(shuō),第三方的引擎提供者可能升級更新了新版的腳本引擎實(shí)現,通過(guò) ScriptEngineManager 來(lái)管理腳本引擎,無(wú)需修改一行 Java 代碼就能替換更新腳本引擎。用戶(hù)只需在 classpath 中加入新的腳本引擎實(shí)現(Jar 包的形式),ScriptEngineManager 就能通過(guò) Service Provider 機制來(lái)自動(dòng)查找到新版本實(shí)現,創(chuàng )建并返回對應的腳本引擎實(shí)例供調用。圖 2 所示時(shí)序圖描述了其中的步驟: 圖 2. 腳本引擎發(fā)現機制時(shí)序圖 ScriptEngineFactory 接口的實(shí)現類(lèi)被用來(lái)描述和實(shí)例化 ScriptEngine 接口,每一個(gè)實(shí)現 ScriptEngine 接口的類(lèi)會(huì )有一個(gè)對應的工廠(chǎng)類(lèi)來(lái)描述其元數據(meta data),ScriptEngineFactory 接口定義了許多函數供 ScriptEngineManager 查詢(xún)這些元數據,ScriptEngineManager 會(huì )根據這些元數據查找需要的腳本引擎,表 2 列出了可供使用的函數: 表 2. ScriptEngineFactory 提供的查詢(xún)函數 | 函數 | 描述 | String getEngineName() | 返回腳本引擎的全稱(chēng) | String getEngineVersion() | 返回腳本引擎的版本信息 | String getLanguageName() | 返回腳本引擎所支持的腳本語(yǔ)言的名稱(chēng) | String getLanguageVersion() | 返回腳本引擎所支持的腳本語(yǔ)言的版本信息 | List<String> getExtensions() | 返回一個(gè)腳本文件擴展名組成的 List,當前腳本引擎支持解析這些擴展名對應的腳本文件 | List<String> getMimeTypes() | 返回一個(gè)與當前引擎關(guān)聯(lián)的所有 mimetype 組成的 List | List<String> getNames() | 返回一個(gè)當前引擎所有名稱(chēng)的 List,ScriptEngineManager 可以根據這些名字確定對應的腳本引擎 | 通過(guò) getEngineFactories() 函數,ScriptEngineManager 會(huì )返回一個(gè)包含當前環(huán)境中被發(fā)現的所有實(shí)現 ScriptEngineFactory 接口的具體類(lèi),通過(guò)這些工廠(chǎng)類(lèi)中保存的腳本引擎信息檢索需要的腳本引擎。第三方提供的腳本引擎實(shí)現的 Jar 包中除了包含 ScriptEngine 接口的實(shí)現類(lèi)之外,還需要提供 ScriptEngineFactory 接口的實(shí)現類(lèi),以及一個(gè) javax.script.ScriptEngineFactory 文件用于指明這個(gè)工廠(chǎng)類(lèi)。這樣,Java 平臺就能通過(guò) ScriptEngineManager 尋找到這個(gè)工廠(chǎng)類(lèi),并通過(guò)這個(gè)工廠(chǎng)類(lèi)為用戶(hù)提供一個(gè)腳本引擎實(shí)例。Java SE 6 默認提供了 JavaScirpt 腳本引擎的實(shí)現,如果需要支持其他腳本引擎,需要將它們對應的 Jar 包包含在 classpath 中,比如對于前面 清單 2 中的代碼,只需在運行程序前將 Groovy 的腳本引擎添加到 classpath 中,然后運行: java RunScript groovy run.groovy | 無(wú)需修改一行 Java 代碼就能以 Groovy 腳本引擎來(lái)運行 Groovy 腳本。在 這里 為 Java SE 6 提供了許多著(zhù)名腳本語(yǔ)言的腳本引擎對 JSR 233 的支持,這些 Jar 必須和腳本引擎配合使用,使得這些腳本語(yǔ)言能被 Java 平臺支持。到目前為止,它提供了至少 25 種腳本語(yǔ)言的支持,其中包括了 Groovy、Ruby、Python 等當前非常流行的腳本語(yǔ)言。這里需要再次強調的是,負責創(chuàng )建 ScriptEngine 實(shí)例的 ScriptEngineFactory 實(shí)現類(lèi)對于用戶(hù)來(lái)說(shuō)是不可見(jiàn)的,ScriptEngingeManager 實(shí)現負責與其交互,通過(guò)它創(chuàng )建腳本引擎。
腳本引擎的運行上下文 如果僅僅是通過(guò)腳本引擎運行腳本的話(huà),還無(wú)法體現出 Java 腳本 API 的優(yōu)點(diǎn),在 JSR 233 中,還為所有的腳本引擎定義了一個(gè)簡(jiǎn)潔的執行環(huán)境。我們都知道,在 Linux 操作系統中可以維護許多環(huán)境變量比如 classpath、path 等,不同的 shell 在運行時(shí)可以直接使用這些環(huán)境變量,它們構成了 shell 腳本的執行環(huán)境。在 javax.script 支持的每個(gè)腳本引擎也有各自對應的執行的環(huán)境,腳本引擎可以共享同樣的環(huán)境,也可以有各自不同的上下文。通過(guò)腳本運行時(shí)的上下文,腳本程序就能自由的和 Java 平臺交互,并充分利用已有的眾多 Java API,真正的站在“巨人”的肩膀上。javax.script.ScriptContext 接口和 javax.script.Bindings 接口定義了腳本引擎的上下文。 - Bindings 接口:
繼承自 Map,定義了對這些“鍵-值”對的查詢(xún)、添加、刪除等 Map 典型操作。Bingdings 接口實(shí)際上是一個(gè)存放數據的容器,它的實(shí)現類(lèi)會(huì )維護許多“鍵-值”對,它們都通過(guò)字符串表示。Java 應用程序和腳本程序通過(guò)這些“鍵-值”對交換數據。只要腳本引擎支持,用戶(hù)還能直接在 Bindings 中放置 Java 對象,腳本引擎通過(guò) Bindings 不僅可以存取對象的屬性,還能調用 Java 對象的方法,這種雙向自由的溝通使得二者真正的結合在了一起。 - ScriptContext 接口:
將 Bindings 和 ScriptEngine 聯(lián)系在了一起,每一個(gè) ScriptEngine 都有一個(gè)對應的 ScriptContext,前面提到過(guò)通過(guò) ScriptEnginFactory 創(chuàng )建腳本引擎除了達到隱藏實(shí)現的目的外,還負責為腳本引擎設置合適的上下文。ScriptEngine 通過(guò) ScriptContext 實(shí)例就能從其內部的 Bindings 中獲得需要的屬性值。ScriptContext 接口默認包含了兩個(gè)級別的 Bindings 實(shí)例的引用,分別是全局級別和引擎級別,可以通過(guò) GLOBAL_SCOPE 和 ENGINE_SCOPE 這兩個(gè)類(lèi)常量來(lái)界定區分這兩個(gè) Bindings 實(shí)例,其中 GLOBAL_SCOPE 從創(chuàng )建它的 ScriptEngineManager 獲得。顧名思義,全局級別指的是 Bindings 里的屬性都是“全局變量”,只要是同一個(gè) ScriptEngineMananger 返回的腳本引擎都可以共享這些屬性;對應的,引擎級別的 Bindings 里的屬性則是“局部變量”,它們只對同一個(gè)引擎實(shí)例可見(jiàn),從而能為不同的引擎設置獨特的環(huán)境,通過(guò)同一個(gè)腳本引擎運行的腳本運行時(shí)能共享這些屬性。 ScriptContext 接口定義了下面這些函數來(lái)存取數據: 表 3. ScriptContext 存取屬性函數 | 函數 | 描述 | Object removeAttribute(String name, int scope) | 從指定的范圍里刪除一個(gè)屬性 | void setAttribute(String name, Object value, int scope) | 在指定的范圍里設置一個(gè)屬性的值 | Object getAttribute(String name) | 從上下文的所有范圍內獲取優(yōu)先級最高的屬性的值 | Object getAttribute(String name, int scope) | 從指定的范圍里獲取屬性值 | ScriptEngineManager 擁有一個(gè)全局性的 Bindings 實(shí)例,在通過(guò) ScriptEngineFactory 實(shí)例創(chuàng )建 ScriptEngine 后,它把自己的這個(gè) Bindings 傳遞給所有它創(chuàng )建的 ScriptEngine 實(shí)例,作為 GLOBAL_SCOPE。同時(shí),每一個(gè) ScriptEngine 實(shí)例都對應一個(gè) ScriptContext 實(shí)例,這個(gè) ScriptContext 除了從 ScriptEngineManager 那獲得的 GLOBAL_SCOPE,自己也維護一個(gè) ENGINE_SCOPE 的 Bindings 實(shí)例,所有通過(guò)這個(gè)腳本引擎運行的腳本,都能存取其中的屬性。除了 ScriptContext 可以設置屬性,改變內部的 Bindings,Java 腳本 API 為 ScriptEngineManager 和 ScriptEngine 也提供了類(lèi)似的設置屬性和 Bindings 的 API。 圖 3. Bindings 在 Java 腳本 API 中的分布 從 圖 3 中可以看到,共有三個(gè)級別的地方可以存取屬性,分別是 ScriptEngineManager 中的 Bindings,ScriptEngine 實(shí)例對應的 ScriptContext 中含有的 Bindings,以及調用 eval 函數時(shí)傳入的 Bingdings。離函數調用越近,其作用域越小,優(yōu)先級越高,相當于編程語(yǔ)言中的變量的可見(jiàn)域,即 Object getAttribute(String name) 中提到的優(yōu)先級。從 清單 3 這個(gè)例子中可以看出各個(gè)屬性的存取優(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) 在這個(gè)程序中被重復調用了三次,由于三次調用的環(huán)境不一樣,導致輸出也不一樣,greeting 變量每一次都被優(yōu)先級更高的也就是距離函數調用越近的值覆蓋。從這個(gè)例子同時(shí)也演示了如何使用 ScriptContext 和 Bindings 這兩個(gè)接口,在例子腳本中并沒(méi)有定義 greeting 這個(gè)變量,但是腳本通過(guò) Java 腳本 API 能方便的存取 Java 應用程序中的對象,輸出 greeting 相應的值。運行這個(gè)程序后,能看到輸出為: 圖 4. 程序 ScopeTest 的輸出 除了能在 Java 平臺與腳本程序之間的提供共享屬性之外,ScriptContext 還允許用戶(hù)重定向引擎執行時(shí)的輸入輸出流: 表 4. ScriptContext 輸入輸出重定向 | 函數 | 描述 | void setErrorWriter(Writer writer) | 重定向錯誤輸出,默認是標準錯誤輸出 | void setReader(Reader reader) | 重定向輸入,默認是標準輸入 | void setWriter(Writer writer) | 重定向輸出,默認是標準輸出 | Writer getErrorWriter() | 獲取當前錯誤輸出字節流 | Reader getReader() | 獲取當前輸入流 | Writer getWriter() | 獲取當前輸出流 | 清單 4 展示了如何通過(guò) ScriptContext 將其對應的 ScriptEngine 標準輸出重定向到一個(gè) PrintWriter 中,用戶(hù)可以通過(guò)與這個(gè) PrintWriter 連通的 PrintReader 讀取實(shí)際的輸出,使 Java 應用程序能獲取腳本運行輸出,滿(mǎn)足更加多樣的應用需求。 清單 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 分別為這兩個(gè)接口提供了一個(gè)簡(jiǎn)單的實(shí)現供用戶(hù)使用。SimpleBindings 通過(guò)組合模式實(shí)現 Map 接口,它提供了兩個(gè)構造函數。無(wú)參構造函數在內部構造一個(gè) HashMap 實(shí)例來(lái)實(shí)現 Map 接口要求的功能;同時(shí),SimpleBindings 也提供了一個(gè)以 Map 接口作為參數的構造函數,允許任何實(shí)現 Map 接口的類(lèi)作為其組合的實(shí)例,以滿(mǎn)足不同的要求。SimpleScriptContext 提供了 ScriptContext 簡(jiǎn)單實(shí)現。默認情況下,它使用了標準輸入、標準輸出和標準錯誤輸出,同時(shí)維護一個(gè) SimpleBindings 作為其引擎級別的 Bindings,它的默認全局級別 Bindings 為空。
腳本引擎可選的接口 在 Java 腳本 API 中還有兩個(gè)腳本引擎可以選擇是否實(shí)現的接口,這個(gè)兩個(gè)接口不是強制要求實(shí)現的,即并非所有的腳本引擎都能支持這兩個(gè)函數,不過(guò) Java SE 6 自帶的 JavaScript 引擎支持這兩個(gè)接口。無(wú)論如何,這兩個(gè)接口提供了非常實(shí)用的功能,它們分別是: - Invocable 接口:允許 Java 平臺調用腳本程序中的函數或方法。
- Compilable 接口:允許 Java 平臺編譯腳本程序,供多次調用。
Invocable 接口 有時(shí)候,用戶(hù)可能并不需要運行已有的整個(gè)腳本程序,而僅僅需要調用其中的一個(gè)過(guò)程,或者其中某個(gè)對象的方法,這個(gè)時(shí)候 Invocable 接口就能發(fā)揮作用。它提供了兩個(gè)函數 invokeFunction 和 invokeMethod,分別允許 Java 應用程序直接調用腳本中的一個(gè)全局性的過(guò)程以及對象中的方法,調用后者時(shí),除了指定函數名字和參數外,還需要傳入要調用的對象引用,當然這需要腳本引擎的支持。不僅如此,Invocable 接口還允許 Java 應用程序從這些函數中直接返回一個(gè)接口,通過(guò)這個(gè)接口實(shí)例來(lái)調用腳本中的函數或方法,從而我們可以從腳本中動(dòng)態(tài)的生成 Java 應用中需要的接口對象。清單 5 演示了如何使用一個(gè) Invocable 接口: 清單 5. 調用腳本中的函數 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 } } } } | 在調用函數前,可以先通過(guò) instanceof 操作判斷腳本引擎是否支持編譯操作,防止類(lèi)型轉換時(shí)拋出運行時(shí)異常,需要特別注意的時(shí),如果調用了腳本程序中不存在的函數時(shí),運行時(shí)會(huì )拋出一個(gè) NoSuchMethodException 的異常,實(shí)際開(kāi)發(fā)中應該注意處理這種特殊情況。 Compilable 接口 一般來(lái)說(shuō),腳本語(yǔ)言都是解釋型的,這也是腳本語(yǔ)言區別與編譯語(yǔ)言的一個(gè)特點(diǎn),解釋性意味著(zhù)腳本隨時(shí)可以被運行,開(kāi)發(fā)者可以邊開(kāi)發(fā)邊查看接口,從而省去了編譯這個(gè)環(huán)節,提供了開(kāi)發(fā)效率。但是這也是一把雙刃劍,當腳本規模變大,重復解釋一段穩定的代碼又會(huì )帶來(lái)運行時(shí)的開(kāi)銷(xiāo)。有些腳本引擎支持將腳本運行編譯成某種中間形式,這取決與腳本語(yǔ)言的性質(zhì)以及腳本引擎的實(shí)現,可以是一些操作碼,甚至是 Java 字節碼文件。實(shí)現了這個(gè)接口的腳本引擎能把輸入的腳本預編譯并緩存,從而提高多次運行相同腳本的效率。 Java 腳本 API 還為這個(gè)中間形式提供了一個(gè)專(zhuān)門(mén)的類(lèi),每次調用 Compilable 接口的編譯函數都會(huì )返回一個(gè) CompiledScript 實(shí)例。CompiledScript 類(lèi)被用來(lái)保存編譯的結果,從而能重復調用腳本而沒(méi)有重復解釋的開(kāi)銷(xiāo),實(shí)際效率提高的多少取決于中間形式的徹底程度,其中間形式越接近低級語(yǔ)言,提高的效率就越高。每一個(gè) CompiledScript 實(shí)例對應于一個(gè)腳本引擎實(shí)例,一個(gè)腳本引擎實(shí)例可以含有多個(gè) CompiledScript(這很容易理解),調用 CompiledScript 的 eval 函數會(huì )傳遞給這個(gè)關(guān)聯(lián)的 ScriptEngine 的 eval 函數。關(guān)于 CompiledScript 類(lèi)需要注意的是,它運行時(shí)對與之對應的 ScriptEngine 狀態(tài)的改變可能會(huì )傳遞給下一次調用,造成運行結果的不一致。清單 6 演示了如何使用 Compiable 接口來(lái)調用腳本: 清單 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 類(lèi)似,也應該先通過(guò) instanceof 操作判斷腳本引擎是否支持編譯操作,防止預料外的異常拋出。并且我們可以發(fā)現同一段編譯過(guò)的腳本,在第二次運行時(shí) greeting 變量的內容被上一次的運行改變了,導致輸出不一致: 圖 5. 程序 CompilableTest 的輸出
jrunscript 工具 Java SE 6 還為運行腳本添加了一個(gè)專(zhuān)門(mén)的工具 —— jrunscript。jrunscript 支持兩種運行方式:一種是交互式,即邊讀取邊解析運行,這種方式使得用戶(hù)可以方便調試腳本程序,馬上獲取預期結果;還有一種就是批處理式,即讀取并運行整個(gè)腳本文件。用戶(hù)可以把它想象成一個(gè)萬(wàn)能腳本解釋器,即它可以運行任意腳本程序,而且它還是跨平臺的,當然所有這一切都有一個(gè)前提,那就是必須告訴它相應的腳本引擎的位置。默認即支持的腳本是 JavaScript,這意味著(zhù)用戶(hù)可以無(wú)需任何設置,通過(guò) jrunscript 在任何支持 Java 的平臺上運行任何 JavaScript 腳本;如果想運行其他腳本,可以通過(guò) -l 指定以何種腳本引擎運行腳本。不過(guò)這個(gè)工具仍是實(shí)驗性質(zhì)的,不一定會(huì )包含在 Java 的后續版本中,無(wú)論如何,它仍是一個(gè)非常有用的工具。
結束語(yǔ) 在 Java 平臺上使用腳本語(yǔ)言編程非常方便,因為 Java 腳本 API 相對其他包要小很多。通過(guò) javax.script 包提供的接口和類(lèi)我們可以很方便為我們的 Java 應用程序添加對腳本語(yǔ)言的支持。開(kāi)發(fā)者只要遵照 Java 腳本 API 開(kāi)發(fā)應用程序,開(kāi)發(fā)中就無(wú)需關(guān)注具體的腳本語(yǔ)言細節,應用程序就可以動(dòng)態(tài)支持任何符合 JSR 233 標準的腳本語(yǔ)言,不僅如此,只要按照 JSR 233 標準開(kāi)發(fā),用戶(hù)甚至還能為 Java 平臺提供一個(gè)自定義腳本語(yǔ)言的解釋器。在 Java 平臺上運行自己的腳本語(yǔ)言,這對于眾多開(kāi)發(fā)者來(lái)說(shuō)都是非常有誘惑力的。
參考資料 |