文章詳情頁
為Java創建你自己的腳本語言-JSR 223介紹
瀏覽:96日期:2024-06-05 15:10:43
內容: 摘要即將發布的Java6.0包含了Java平臺腳本(JSR 223)的實現。這個JSR關注于程序設計語言以及他們與Java的整合。本文通過一個簡單的“Boolean語言的實現展示了JSR 223的能力和潛力。通過這個例子,你將看到如何使用Scripting API(javax.script.*)編寫程序,如何打包并發布一個符合腳本引擎發現機制的語言實現,以及如何使你的腳本引擎對JSR 223是可編譯和可調用的。在Java平臺腳本(JSR 223)——以及他的前任BSF(Bean Scripting Framework)——之前,已經有很多種語言可以與Java交互。其中一些可以接受一段Java程序的文本代碼作為輸入,然后將代碼的執行結果返回給Java程序。而另外一些可以保持Java程序里的對象的引用,并可以執行這個對象的方法,或者創建一個新的Java Class的實例。由于每個語言都有自己的與Java交互的方法,開發人員如果想要在他們的Java程序中使用腳本引擎就必須學習每個腳本引擎特殊的編程接口。為了解決這個問題,JSR 223定義了一個約定,所有遵循這個規范的腳本引擎都必須遵守這個約定。這個約定由一組Java接口和類、以及一個打包和部署腳本引擎的機制組成。當使用遵循JSR 223的腳本引擎時,只需要使用標準定義的一組接口。由于腳本引擎的具體實現會被良好地封裝,你根本不需要考慮他們。JSR 223不僅使腳本引擎使用更簡單,而且也使腳本引擎的開發更簡單。如果你設計實現了一個程序語言,只需要實現JSR 223的接口來包裝(wrap)你的腳本引擎,就可以使你的腳本引擎更容易使用并擁有更多的使用者。在我們看JSR 223的接口和本文對他們的實現之前,我先要指出:雖然JSR 223的名稱和本文的標題里都含有“腳本這個詞,但是并不意味著會限制在可以使用JSR 223進行整合的語言。你可以使用任何你喜歡的語言,并且用一個遵守JSR 223約定的層包裝它。這個語言可以是面向對象的、函數的、或者符合任何編程范型的程序語言。他可以是強類型、弱類型或者根本沒有類型限制。事實上,在寫這篇文章前,我已經為Scheme(一個弱類型的函數程序語言)實現了一個遵循JSR 223的包裝器,并已經放到了SourceForge上。在這篇文章里,我們使用一個更簡單的語言,這樣我們就可以集中精力于JSR 223,而不用費神于語言的細節。不用擔心你是否有自己創建一個程序語言的經歷。這篇文章不討論程序語言,只是討論JSR 223定義的程序語言和Java之間需要遵循的約定。版權聲明:任何獲得Matrix授權的網站,轉載時請務必保留以下作者信息和鏈接作者:Chaur Wu;niuji原文:http://www.javaworld.com/javaworld/jw-04-2006/jw-0424-scripting.htmlMatrix:http://www.matrix.org.cn/resource/article/44/44604_JSR+223.html關鍵字:JSR 223BoolScript引擎圖一顯示了我們的示例的各個部分以及他們是如何相互關聯的。這篇文章里的示例定義了一個簡單的語言,我稱之為BoolScript。我稱編譯執行BoolScript代碼的程序為BoolScript引擎。除了編譯和執行BoolScript代碼,BoolScript引擎還實現了JSR 223的約定,是一個符合JSR 223規范的腳本引擎。如圖所示,Boolscript引擎的代碼打包在boolscript.jar中。 圖一:BoolScript概覽。在這篇文章里,當我提到JSR 223時都是指JSR 223的規范,JSR 223框架就是指這個規范的實現。這篇文章里使用的JSR 223框架已經包含于Java Standard Edition 6.0中(Java SE是sun的J2SE新名字)。我們的例子里包含了一個使用BoolScript引擎的Java程序,代碼詳見BoolScriptHostApp.java文件。注意圖一展示了Java程序總是通過JSR 223框架間接的和腳本引擎打交道。你需要Java SE 6.0 beta和這篇文章的二進制文件來運行這個示例。我使用的Java SE 6.0的版本是build 77。你可以在java.net下載,你也可以使用Sun Developer Network提供的Java SE 6.0。示例代碼在資源中提供下載,其中包含了以下文件:+BoolScriptEngine-Source.zip BoolScript引擎的源代碼+BoolScriptHostExample-Source.zip Java示例程序源代碼+BoolScriptHostExample.zip BoolScript引擎和Java示例程序的二進制代碼示例程序是BoolScriptHostExample.zip 中的BoolScriptHostApp.class ,解壓縮到任意文件夾執行即可。這個zip文件里還包含了3個jar文件,執行時需要將他們加到Java的classpath中。具體可參照run.bat文件里的代碼,這個文件也包含在BoolScriptHostExample.zip 文件里。示例執行產生如下輸出: Mozilla RhinoBool Script Engineanswer of boolean expression is: falseanswer of boolean expression is: trueanswer of boolean expression is: false BoolScript 語言在深入JSR 223的細節前,我們先快速地了解一下BoolScript語言。BoolScript非常簡單,他唯一能做的就是計算布爾表達式的值。下面是BoolScript代碼的例子:(True | False) & True(True & x) | y可以看到,BoolScript支持兩個操作符:&(邏輯與)和|(邏輯或)。BoolScript還支持三個操作數:True、False和變量,變量的值只能是True或False。腳本引擎發現機制為了看清楚JSR 223框架在Java程序和腳本引擎間做了哪些工作,我們先假設你想要在你的程序里使用一個腳本引擎。首先,你需要創建腳本引擎的實例;然后,需要將腳本代碼傳給引擎,讓引擎求值(或者編譯這段腳本代碼供以后執行)。我們仔細看下這些步驟,記住:不論我們做什么,我們只能通過JSR 223框架使用腳本。創建腳本引擎的實例前我們首先要創建javax.script.ScriptEngineManager的實例,然后用這個實例查詢腳本引擎的實例。你可以根據腳本引擎的名字、MIME類型或文件擴展名查詢引擎實例。例如我們的BoolScript代碼保存文件是*.bool,那么文件擴展名就是bool。下面的代碼演示了如何使用擴展名查詢腳本引擎實例。ScriptEngineManager engineMgr = new ScriptEngineManager();ScriptEngine bsEngine = engineMgr.getEngineByExtension('bool');但是我們在哪指定我們的腳本引擎的名稱、MIME類型和文件擴展名呢?我們使用了BoolScriptEngineFactory類來指定這些屬性。這個類實現了javax.script.ScriptEngineFactory接口的getExtensions()、getMimeTypes()、和 getNames()方法。我們在這幾個方法里聲明了BoolScript引擎的名稱、MIME類型和文件擴展名。下面是BoolScriptEngineFactory的getExtensions()方法:public List getExtensions() { ArrayList extList = new ArrayList(); extList.add('bool'); return extList;}你可能會奇怪為什么使用ScriptEngineManager創建BoolScriptEngine的實例,而不是用下面的方法直接創建:ScriptEngine bsEngine = new BoolScriptEngine();當然,你可以用這種方法創建。事實上,我在開發示例代碼的時候為了測試也這樣使用過。直接創建腳本引擎實例在測試的時候是可以的,但在實際運行環境中,這樣做違反了必須通過JSR 223框架操作腳本引擎的規定,破壞了JSR 223隱藏腳本引擎信息的目的。JSR 223通過使用工廠方法(Factory Method)模式隱藏腳本引擎的詳細信息,從而達到將腳本引擎從Java程序中解耦的目的。直接創建腳本引擎的另一個問題是略過了ScriptEngineManager可能會對腳本引擎實例做的初始化工作。接下來我們就會看到ScriptEngineManager做的這種工作。ScriptEngineManager是如何根據bool查找BoolScriptEngine并創建他的實例的呢?答案就是JSR 223里的腳本引擎發現機制。在后面對這個機制的討論過程中,你將看到ScriptEngineManager會對腳本引擎做哪些初始化工作,還有他為什么要做這些工作。根據腳本引擎發現機制,一個腳本引擎的提供者在打包腳本引擎實現的類文件的時候還需要打包一個附加的文件到jar文件中。這個文件必須放在jar文件的META-INF/services目錄下面,而且名稱必須是javax.script.ScriptEngineFactory。打開boolcripts.jar你就可以看到這樣的目錄結構。文件META-INF/services/javax.script.ScriptEngineFactory的內容必須包含實現了ScriptEngineFactory的類的全名。在我們的例子中只有一個這樣的類,我們文件內容如下:net.sf.model4lang.boolscript.engine.BoolScriptEngineFactory當一個腳本引擎提供者將他/她的腳本引擎打包成jar文件并發布后,用戶只需要將這個jar文件放入Java的classpath里就可以完成安裝。圖二展示了Java程序通過JSR 223框架發現腳步引擎時發生的事件。 圖二 Java程序如何發現腳本引擎當使用名字、MIME類型或文件擴展名查詢指定的腳本引擎時,ScriptEngineManager將遍歷classpath中所有的的ScriptEngineFactory類。如果找到符合的,ScriptEngineManager就會創建這個引擎工廠的實例,然后用這個工廠實例創建腳本引擎的實例。腳本引擎工廠使用getScriptEngine()創建腳本引擎,腳本引擎提供者需要實現這個方法。你可以看下BoolScriptEngineFactory的代碼,其中getScriptEngine()的實現如下:public ScriptEngine getScriptEngine() { return new BoolScriptEngine();}這個方法很簡單,只是創建了一個腳本引擎的實例并將這個實例返回給ScriptEngineManager(或者任何調用這個方法的類)。我們感興趣的是在ScriptEngineManager得到這個實例后,在將這個實例返回給Java程序前,ScriptEngineManager為了初始化這個引擎而調用的setBindings()方法。這時我們需要了解JSR 223的一個核心概念:Java綁定。在我解釋了綁定、范圍和上下文的概念和構造后,你就能明白setBindings()為腳本引擎做了哪些初始化工作。綁定、范圍、上下文回憶下,BoolScript語言允許使用下面的代碼:(True & x) | y但是他未提供任何構造讓你給變量x、y賦值。我應該將語言設計成可以使用如下的代碼:x = Truey = False(True & x) | y但是我是故意忽略了賦值操作符=,使BoolScript代碼必須在包含定義了變量的值的上下文中執行。這意味著當Java程序用BoolScript腳本引擎計算一段文本代碼的值時,同時需要將一個上下文提供給腳步引擎,或者至少要讓腳本引擎知道需要使用哪個上下文。你可以認為上下文是Java程序和腳本引擎交換數據的袋子。JSR 223用接口javax.script.ScriptContext定義上下文這個結構。如果我們向這個袋子里放入很多而又不加以組織的話,這個袋子就會變得非常凌亂。因此腳本上下文(例如ScriptContext的實例)將自己的數據劃分到了多個范圍里。JSR 223用接口javax.script.Bindings定義范圍這個結構。圖三展示了上下文、上下文的范圍、以及范圍中存放的數據。 圖三 腳本引擎管理器和腳本引擎中的上下文和范圍。圖三里有些信息十分重要:1. 一個腳本引擎包含一個腳本上下文。2. 一個腳本引擎管理器(例如ScriptEngineManager的實例)都可以用來創建多個腳本引擎。3. 腳本引擎管理器包含一個被稱為global scope的全局范圍,但是不包含上下文。4. 每個范圍基本上就是一個名稱-值對的集合。圖三中可以看到有個一個范圍里包含兩個這樣的名稱-值對,一個的名稱是x,另一個的名稱是y。要注意每個范圍都是javas.script.Bindings的一個實例。5. 腳本引擎里的上下文包含了一個全局范圍、一個引擎范圍和0個或多個其他范圍。6. 一個腳本引擎可以用來執行多個腳本(例如:腳本語言編寫的多段單獨的腳本片斷)。但是圖三種的全局范圍和引擎范圍是什么呢?全局范圍是一個在多個腳本引擎中共享的范圍。如果你想在多個腳本引擎中共享一部分數據,那就可以將這些數據放在全局范圍中。注意全局范圍并不是對所有的腳本引擎來說是全局的。它只對那些被它所在的腳本引擎管理器創建的腳步引起來說是全局的。引擎范圍是一個被多個腳本貢享的范圍。如果你想要在多個腳本中共享數據,那就可以將這些數據放在引擎范圍中。例如,假設我們有下面兩段腳本:(True & x) | y //Script A(True & x) //Script B如果我們要在這兩個腳本中共享x的值,我們可以把這個值放入執行腳本的腳本引擎包含的引擎范圍中。假設現在我們只想在腳本A中保存y的值,那我們就需要創建一個范圍,記住這個范圍只對腳本A可見,然后將y的值放入這個范圍中。作為例子,BoolScriptHostApp.java的main方法里用下面的代碼計算了(x & y)://bsEngine is an instance of ScriptEnginebsEngine.put('x', BoolTermEvaluator.tTrue);bsEngine.put('y', BoolTermEvaluator.tTrue);bsEngine.eval('x & ynn');這段代碼先將x和y的值放入引擎范圍內,然后調用引起的eval()方法執行BoolScript代碼。如果你看下ScriptEngine接口,你會發現eval()方法有很多擁有不同參數的重載方法。如果像上面那樣使用字符串作為參數調用eval()方法,腳本引擎會在他的上下文中執行這段代碼。如果不希望在腳本引擎的上下文中執行,那就要在調用eval()時提供上下文。在我們的實現里,eval()的實際工作由BoolTermEvaluator里面的下面這段代碼執行:public static BoolTerm evaluate(BoolTerm term, ScriptContext context){ ... else if (term instanceof Var) { Var var = (Var) term; Bindings bindings = context.getBindings(ScriptContext.ENGINE_SCOPE); if (!bindings.containsKey(var.getName())) throw new IllegalArgumentException('Variable ' + var.getName() + ' not set.'); Boolean varValue = (Boolean) bindings.get(var.getName()); if (varValue == Boolean.TRUE) return BoolTermEvaluator.tTrue; else return BoolTermEvaluator.tFalse; } ...}這個方法通過計算Term(True、 False、或變量)來執行BoolScript代碼。當Term像上面的代碼那樣是一個變量的時候,方法將調用作為參數傳入的上下文的getBindings()方法得到引擎范圍的引用。由于一個上下文里可以有多個范圍,所以我們使用ScriptContex.ENGINE_SCOPE表示我們想要得到的是引擎范圍。在得到引擎范圍后,我們使用變量名在引擎范圍中查找變量的值。如果未找到變量的值,則拋出異常。反之,我們將計算這個變量然后返回他的值。最后,我準備解釋為什么腳本引擎管理器初始化腳本引擎的時候要調用腳本引擎的setBindings()方法:當腳本引擎管理器調用引擎的setBindings()方法的時候,它將自己的全局范圍作為參數傳遞給這個方法。引擎則將這個全局范圍存入自己的上下文中。在結束這個章節前,讓我們看一下腳本API里的幾個類。前面說過ScriptEngineManager包含了一個Bindings的實例作為全局范圍。如果你看一下javax.script.ScriptEngineManager的源代碼,你可以發現有一個getBindings()方法用于得到ScriptEngineManager里的Bindings,同樣,還存在一個setBindings()用于設置ScriptEngineManager的Bindings。和ScriptEngineManager相似,ScriptEngine包含了一個ScriptContext的實例。在接口javax.script.ScriptEngine里也對應存在一個getContext()方法和一個setBindings()方法。因此你可以很容易地在腳本引擎管理器之間共享全局范圍。你要做的僅僅是調用一個腳本引擎管理器的getBindings()方法得到它的全局范圍,然后調用另一個的setBindings()方法設置這個全局范圍。如果你看了示例里BoolScriptEngine的代碼,你會發現里面并沒有ScriptContext的引用。這是因為BoolScriptEngine繼承了AbstractScriptEngine,而AbstractScriptEngine里有一個成員是ScriptContext的實例。如果你自己實現一個腳本引擎,而且沒有繼承父類(如AbstractScriptEngine),那你就需要在你的腳本引擎里保存ScriptContext的實例,并且實現getContext()和setContext()方法。Compilable和Invocable到現在為止,我們已經達到了把BoolScript做為JSR 223腳本引擎所需要的最低的要求。每當Java程序需要使用我們的腳本引擎的時候,它需要將BoolScript代碼作為字符串傳遞給我們的引擎。引擎內部使用一個解析器將這段字符串解析成抽象語法樹(abstract syntax tree),然后將棵樹交給BoolTermEvaluator.evaluate()執行。以上的整個過程我們稱為解釋執行(interpretation),與之相對的是編譯執行(compilation)。在這個過程中,BoolScript引擎被稱為解釋器,與之相對的被稱為編譯器。如果要作為編譯器使用的話,BoolScript引擎要能做到將文本的BoolScript代碼轉換成中間形態,這樣在執行這段代碼時就不用再解析成抽象語法樹。本章節講述了如何實現這個功能。Java程序是編譯成稱為Java字節碼的中間形態存放在.class文件中。運行時classloader載入.class文件,然后由JVM執行字節碼。BoolScript將使用Java字節碼作為中間形態,這樣就可以不用自己定義中間形態和實現自己的虛擬機。JSR 223定義的編譯概念的模型是javax.script.Compilable,因此BoolScriptEngine需要實現這個接口。下面這段BoolScriptHostApp.java里的代碼展示了如何使用可編譯的腳本引擎編譯和執行腳本代碼:List boolAnswers = null;//bsEngine is an instance of ScriptEngineCompilable compiler = (Compilable) bsEngine;CompiledScript compiledScript = compiler.compile('x & ynn');Bindings bindings = new SimpleBindings();bindings.put('x', new Boolean(true));bindings.put('y', new Boolean(true));boolAnswers = (List) compiledScript.eval(bindings);printAnswers(boolAnswers);Invocable invocable = (Invocable) bsEngine;boolAnswers = (List) invocable.invoke('eval', new Boolean(true), new Boolean(false));printAnswers(boolAnswers);上面的代碼中,bsEngine是一個ScriptEngine的實例,它實現了Compilable接口。我們將它轉換成Compilable,然后調用compile()方法編譯代碼“x & y。compile()內部將“x & y轉換成下面的Java代碼:package net.sf.model4lang.boolscript.generated;import java.util.*;import java.lang.reflect.*;class TempBoolClass { public static List eval(boolean x, boolean y) { List resultList = new ArrayList(); boolean result = false; result = x & y; resultList.add(new Boolean(result)); return resultList; }}這個轉換將BoolScript代碼轉換成Java類中的一個方法。類名和方法名都是硬編碼的。BoolScript里的每個變量都將成為Java方法的一個參數。將BoolScript代碼轉換成Java代碼只完成了一半的工作。之后我們還需要將Java代碼編譯成Java字節碼。在這我使用Java編譯API(JSR 199,Java SE 6.0的另一個新特性)直接在內存中編譯Java代碼。本文不討論Java編譯API,感興趣的讀者可以參考資源部分查找更多的信息。Compilable接口規定了compile()方法必須返回一個CompiledScript的實例。CompiledScript在JSR 223里是用來定義編譯結果的模型。不論我們怎么編譯腳本代碼,當編譯結束的時候,我們必須將編譯的結果封裝成CompiledScript的實例。在示例代碼中,我們定義了一個類BoolCompiledScript繼承CompiledScript,將編譯后的BoolScript代碼存放到這個類中。當腳本代碼編譯后,Java程序可以調用CompiledScript實例的eval()方法反復地執行編譯后的代碼。在我們的例子中,我們調用CompiledScript的eval()方法的時候,需要將包含x和y變量的腳本上下文傳遞給這個方法,這在上面BoolScriptHostApp.java的代碼中已經列出。CompiledScript的eval()并不是唯一可以執行編譯后的腳本代碼的方法。如果腳本引擎實現了Invocable接口,我們就可以調用Invocable接口的invoke()方法執行腳本代碼。在我們的簡單示例中,調用這兩個方法執行腳本代碼看上去并沒有什么區別。但是,實際使用中腳本引擎用戶通常用CompiledScript執行整段腳本,而使用Invocable執行獨立的函數(Java里稱為方法)。如果你看一下Invocable的invoke()方法,你可以輕易地發現CompiledScript和Invocable的區別。和CompiledScript的eval()方法使用可選的腳本上下文作為參數不同的是,invoke()方法使用你想執行的函數名作為參數。在上面引用的BoolScriptHostApp.java的代碼中,腳本引擎實例bsEngine實現了Invocable接口。我們將它轉成Invocable并調用它的invoke()方法。調用編譯后的腳本的函數和用反射調用Java類的方法十分相似。你必須告訴invoke()方法你要調用的函數名,同時還要提供這個函數所需要的參數。我們已經知道我們的函數名已經硬編碼為eval了。因此我們將字符串“eval作為invoke的第一個參數,同時我們還需要將eval的兩個Boolean參數傳遞給invoke()方法。結論在這篇文章里,我討論了JSR 223的幾個主要特性:腳本引擎發現機制、Java綁定、Compilable和Invocable。JSR 223中的Web腳本這里沒有涉及。如果了我們在BoolScript引擎里實現了Web腳本,那我們的腳本引擎使用者就可以在servlet容器里創建Web內容了。即使不管和Java的整合,開發一個語言的編譯器和解釋器也是一個龐大的事業。根據語言的復雜度,編譯器和解釋器的開發可能會是一個非常麻煩的任務。感謝JSR 223,將我們的語言和Java整合從來沒有這么簡單過。關于作者Chaur Wu 是一個軟件開發人員并出版過書。他曾合著了有關設計模式和軟件建模的書籍。他同時也是開源項目Model4Lang管理員,這個項目致力于為語言設計和構造提供基于模型的解決方案。資源+本文中相關源代碼下載:http://www.javaworld.com/javaworld/jw-04-2006/scripting/jw-0424-scripting.zip+BSF網站:http://jakarta.apache.org/bsf/ +一個JSR 223腳本引擎:http://model4lang.sourceforge.net +JSR 223:http://www.jcp.org/en/jsr/detail?id=223 +Java SE 6.0源碼快照:http://download.java.net/jdk6 +Java SE 6.0 beta下載:http://java.sun.com/javase/6/download.jsp +JSR 199:http://www.jcp.org/en/jsr/detail?id=199 +Matrix:http://www.matrix.org.cn Java, java, J2SE, j2se, J2EE, j2ee, J2ME, j2me, ejb, ejb3, JBOSS, jboss, spring, hibernate, jdo, struts, webwork, ajax, AJAX, mysql, MySQL, Oracle, Weblogic, Websphere, scjp, scjd 摘要即將發布的Java6.0包含了Java平臺腳本(JSR 223)的實現。這個JSR關注于程序設計語言以及他們與Java的整合。本文通過一個簡單的“Boolean語言的實現展示了JSR 223的能力和潛力。通過這個例子,你將看到如何?
標簽:
Java
相關文章:
排行榜