出處 仙人掌工作室 eNet矽谷動力
一、指令碼解譯器概述
在一些Java應用的需求中,整合某種指令碼語言的支援能夠帶來很大的方便。例如,使用者可能想要編寫指令碼程式驅動應用、擴充應用,或為了簡化操作而編寫迴圈和其他流程式控制制邏輯。在這些情況下,一種理想的解決方案是在Java應用中提供對指令碼語言解譯器的支援,讓指令碼語言解譯器讀取使用者編寫的指令碼並在應用提供的類上運行這些指令碼。為了實現這個目標,你可以在Java應用所啟動並執行JVM中,運行一個基於Java的指令碼語言解譯器。
一些支援庫,例如IBM的Bean Scripting Framework,能夠協助你把不同的指令碼語言整合到Java程式。這些支援架構能夠讓你的Java應用在不作大量修改的情況下,運行Tcl、Python和其他語言編寫的指令碼。
在Java應用中整合了指令碼解譯器之後,使用者編寫的指令碼能夠直接引用Java應用的類,就如這些指令碼屬於Java程式的一部分一樣。這種思路既有優點也有缺點。其優點在於,如果你想要用指令碼驅動的方式對應用進行迴歸測試,或者想要通過指令碼對應用進行低級調用,它能夠帶來很大的方便;其缺點在於,如果使用者的指令碼直接操作Java程式的內部結構而不是經過認可的API,它可能影響Java程式的完整性和應用的安全。因此,應當仔細地規劃那些允許使用者針對其編寫指令碼的API,並聲明程式的其餘部分不允許用指令碼操作。另外,你還可以對那些不想讓使用者針對其進行指令碼編程的類和方法名稱進行模糊處理,只留出那些允許指令碼編程的API類和方法名字。這樣,你就能夠有效地降低喜歡冒險的使用者直接用指令碼操作受保護的類和方法的可能性。
在Java程式中支援多種指令碼語言有著非同尋常的意義,但如果你正在編寫的是一個商業應用,則應當謹慎考慮——儘管你為使用者提供了最完善的功能,但同時也帶來了最多的出錯機會。必須考慮到配置和管理問題,因為至少有一部分的指令碼解譯器在定期地進行升級和更新,這樣你就必須花很大的力氣管理各個解譯器的哪些版本適合於Java應用的哪些版本。如果使用者為瞭解決舊指令碼解譯器中存在的BUG,對其中某個指令碼解譯器進行了升級,你的Java應用就會運行在一種未經完全測試的配置下。數天或數星期之後,使用者也許會發現由於指令碼引擎升級而產生的問題,但他們很可能不會把指令碼引擎升級的事情告訴你,這時你就很難再次重複實驗出使用者報告的錯誤了。
另外,使用者很可能堅持認為你必須為Java應用支援的指令碼解譯器提供補丁。一些指令碼解譯器按照原始碼開放的模式及時進行維護和更新;對於這些指令碼解譯器,可能有專家協助你解決問題、修補解譯器,或在新的發行版中引入補丁。這是很重要的,因為指令碼解譯器是一個很複雜的工具,包含大量的代碼,如果沒有專家的支援,對於自己修改指令碼解譯器這一令人煩惱的任務,你很可能束手無策。
為了避免出現這種問題,你應該對於每一種準備在Java應用中提供支援的指令碼解譯器進行全面的測試。對於每一種解譯器,確保它能夠順利地處理絕大多數常見的使用情形,確保它即使在極端苛刻的條件下運行大量的指令碼也不會出現大的記憶體漏洞,確保當你對Java程式和指令碼解譯器進行嚴格的Beta測試時不會出現任何意外的情況。當然,這種前期測試需要投入時間和其他資源;但不管怎樣,測試投入總是物有所值的。
二、保持系統簡潔
如果你必須在Java應用中提供指令碼支援,首先必須選擇一個最符合應用要求和使用者基礎的指令碼解譯器。選擇合適的解譯器能夠簡化整合解譯器的代碼,減少客戶支援方面的支出,以及提高應用的穩定性。最困難的問題在於:如果只能選用一種解譯器,應該選用哪一種呢?
我比較了幾種指令碼解譯器,開始時考慮的指令碼語言套件括Tcl、Python、Perl、JavaScript和BeanShell。接著,在深入分析之前,我放棄了Perl。為什麼呢?因為Perl沒有用Java寫的解譯器。假設你選擇了一個用機器碼實現的指令碼解譯器,例如Perl,則Java應用和指令碼代碼之間的互動就不再直接進行;另外,對於每一個你想要支援的作業系統,都必須提供一個指令碼解譯器的二進位程式碼程式庫。由於許多開發人員選擇Java是因為看中了它的跨平台可移植性,為了保證Java應用有這種優點,所以最好選擇一種不依賴於機器碼的解譯器。和Perl不同,Tcl、Python、JavaScript和BeanShell都有基於Java的解譯器,所以這些語言的代碼可以與Java應用在同一個JVM和進程之內運行。
基於以上標準,參與本文評測的指令碼解譯器包括:
Jacl:Tcl的Java實現。
Jython:Python的Java實現。
Rhino:JavaScript的Java實現。
BeanShell:一個用Java編寫的Java原始碼解譯器。
限定了待比較的解譯器種類之後,接下來就可以從各個方面對它們進行比較了。
三、評測之一:可用性
第一個評測項目是可用性。這項評測分析了是否存在某種解譯器停用情形。用每一種語言各編寫一個簡單的測試程式,然後分別用相應的解譯器運行,結果發現,所有解譯器都通過了測試,每一種解譯器都能夠穩定地工作或能夠方便地與之互動。既然每一種解譯器都值得考慮,那麼,有哪些因素可能使開發人員偏愛其中一種呢?
Jacl:如果你想要在Tk指令碼代碼中建立使用者介面元素,請訪問Swank project,它把Java的Swing組件封裝到了Tk裡面。發行版不包含Jacl指令碼的調試器。
Jython:支援用Python文法編寫的指令碼。Python利用縮排層次表示代碼塊的結構,而不是象其他許多語言一樣用花括弧或開始-結束符號表示控制流程程。至於這種改變究竟是好事還是壞事,這就要看你和使用者的習慣了。發行版不包含Jython指令碼的調試器。
Rhino:許多程式員總是把JavaScript和Web頁面編程關聯起來,但這個版本的JavaScript不需要在瀏覽器中運行。在使用過程中,我沒有發現任何問題。它的發行版帶有一個簡單但實用的指令碼調試器。
BeanShell:Java程式員很快會對這個原始碼解譯器產生一種親切的感覺。BeanShell的文檔寫得很不錯,但開發組很小。然而,只有當BeanShell的開發人員改變了他們的興趣,卻又沒有其他人填補他們轉換興趣後留下的空白時,開發組太小才會成為一個問題。它的發行版不包含BeanShell指令碼調試器。
四、評測之二:效能
第二個評測項目是效能。這項測試是要分析各個指令碼解譯器執行一些簡單程式的速度。本次測試沒有要求解譯器排序大型數組,也沒有執行複雜的數學計算,而是執行了一些簡單的、常見的操作,例如迴圈、整數比較,以及分配和初始化大型數組和二維數組。測試程式都很簡單,且這些操作都是每一個商業應用或多或少要用到的。另外,本項測試還分析了每一個解譯器初始化和執行簡單指令碼所需要的記憶體。
為一致起見,測試程式的每一種指令碼語言的版本都盡量地相似。測試在一台Toshiba Tecra 8100筆記本上進行,CPU是700-MHz的Pentium III處理器,RAM是256 MB。調用JVM時,堆棧大小使用預設值。
為了便於理解和比較指令碼程式的執行速度,本項評測還在Java 1.3.1下運行了類似功能的Java程式,又在Tcl本機解譯器內運行了為Jacl指令碼解譯器編寫的Tcl指令碼。因此,在下面的表格中,你還可以看到這兩次測試的結果。
表格一:從1到1000000計數的for迴圈:
解譯器類型 時間
-----------------------
Java 10 毫秒
Tcl 1.4 秒
Jacl 140 秒
Jython 1.2 秒
Rhino 5 秒
BeanShell 80 秒
--------------------
表格二:比較整數是否相等,1000000次:
解譯器類型 時間
-----------------------
Java 10 毫秒
Tcl 2 秒
Jacl 300 秒
Jython 4 秒
Rhino 8 秒
BeanShell 80 秒
--------------------
表格三:分配並初始化100000個元素的數組:
解譯器類型 時間
-----------------------
Java 10 毫秒
Tcl .5 秒
Jacl 25 秒
Jython 1 秒
Rhino 1.3 秒
BeanShell 22 秒
--------------------
表格四:分配並初始化500 X 500 個元素的數組:
解譯器類型 時間
--------------------
Java 20 毫秒
Tcl 2 秒
Jacl 45 秒
Jython 1 秒
Rhino 7 秒
BeanShell 18 秒
--------------------
表格五:在JVM內初始化解譯器所需要的記憶體:
解譯器類型 記憶體佔用
----------------------
Jacl 大約 1 MB
Jython 大約 2 MB
Rhino 大約 1 MB
BeanShell 大約 1 MB
----------------------
本項評測證明Jython具有最好的效能,與其他解譯器拉開了相當可觀的差距,Rhino第二,BeanShell稍慢,而Jacl墊底。然而,對於你來說,這些效能資料到底能夠產生多大的影響,這與你想要用指令碼語言完成的任務密切相關。如果指令碼函數中包含大量的迭代操作,那麼Jacl或BeanShell可能是令人難以接受的。如果指令碼程式重複執行代碼的機會很少,那麼這些解譯器在速度上的相對差異就不那麼重要了。值得指出的是,Jython看來沒有為聲明二維數組提供內建的直接支援,但這個問題可以通過一個“數組的數組”結構解決。
五、評測之三:整合的難易程度
本項評測包含兩個任務。第一個任務是比較對各種指令碼語言解譯器進行執行個體化時需要多少代碼;第二個任務是編寫一個完成如下操作的指令碼:執行個體化一個Java JFrame,放入一個JTree,調整大小並顯示出JFrame。儘管這些任務都很簡單,但由此我們可以看出開始使用一個解譯器要做多少工作,還可以看出為解譯器編寫的指令碼代碼在調用Java類時到底是什麼樣子。
■ Jacl
要把Jacl整合到Java應用,首先要把Jacl的Jar檔案加入到Java的CLASSPATH,然後在執行指令碼之前,建立Jacl解譯器的執行個體。下面是建立Jacl解譯器執行個體的代碼:
import tcl.lang.*;
public class SimpleEmbedded { public static void main(String args[]) { try { Interp interp = new Interp(); } catch (Exception e) { } }
|
下面的Jacl指令碼代碼顯示了如何建立一個JTree,把它放入JFrame,調整大小並顯示JFrame:
package require java set env(TCL_CLASSPATH) set mid [java::new javax.swing.JTree] set f [java::new javax.swing.JFrame] $f setSize 200 200 set layout [java::new java.awt.BorderLayout] $f setLayout $layout $f add $mid $f show
|
■ Jython
要把Jython整合到Java應用,首先要把Jython的Jar檔案加入到Java的CLASSPATH,然後在執行指令碼之前,建立一個Jython解譯器的執行個體。完成這個任務的代碼很簡單:
import org.python.util.PythonInterpreter; import org.python.core.*;
public class SimpleEmbedded { public static void main(String []args) throws PyException { PythonInterpreter interp = new PythonInterpreter(); } }
|
下面的Jython指令碼代碼顯示了如何建立JTree,把它放入JFrame,然後顯示出JFrame。下面的代碼不包含調整大小的操作:
from pawt import swing import java, sys frame = swing.JFrame('Jython example', visible=1) tree = swing.JTree() frame.contentPane.add(tree) frame.pack()
|
■ Rhino
和其他解譯器一樣,整合Rhino時首先要把Rhino的Jar檔案加入到Java的CLASSPATH,然後在執行指令碼之前,建立Rhino解譯器的執行個體:
import org.mozilla.javascript.*; import org.mozilla.javascript.tools.ToolErrorReporter;
public class SimpleEmbedded { public static void main(String args[]) { Context cx = Context.enter(); } }
|
下面簡單的Rhino指令碼顯示了如何建立JTree,把它放入JFrame,調整大小並顯示出JFrame:
importPackage(java.awt); importPackage(Packages.javax.swing); frame = new Frame("JavaScript"); frame.setSize(new Dimension(200,200)); frame.setLayout(new BorderLayout()); t = new JTree(); frame.add(t, BorderLayout.CENTER); frame.pack(); frame.show();
|
■ BeanShell
整合BeanShell也和整合其他解譯器一樣簡單。先把BeanShell的Jar檔案加入到Java的CLASSPATH,然後在執行指令碼代碼之前建立一個BeanShell解譯器的執行個體:
import bsh.Interpreter;
public class SimpleEmbedded { public static void main(String []args) throws bsh.EvalError { Interpreter i = new Interpreter(); } }
|
下面的BeanShell指令碼代碼顯示了如何建立一個JTree,把它放入JFrame,調整大小並顯示出JFrame。代碼很簡單,且具有熟悉的Java風格:
frame = new JFrame(); tree = new JTree(); frame.getContentPane().add(tree); frame.pack(); frame.show();
|
從上面的說明可以看出,在Java應用中整合任何一種解譯器都是很容易的。同時,只要你掌握了指令碼語言的文法,就能夠高效地編寫出指令碼程式。前面幾個簡單的例子顯示出,用BeanShell和JavaScript編寫的指令碼在格式上與Java最相似,而Jacl和Jython則顯得有些不同,但Jacl和Jython指令碼也不是難以理解的。正如上面為各個指令碼解譯器編寫的指令碼所顯示的,在指令碼代碼和Java應用的類之間不存在任何防火牆。因此必須注意:指令碼代碼直接在Java應用的類的基礎上運行。應當確信這就是你想要的效果。如果你想要在運行時對應用的某些部分進行保護,避免指令碼代碼訪問某些部分,就應當採取對非公開的代碼進行模糊處理之類的措施,避免人們直接對不公開的API進行編程。
六、評測之四:支援和許可問題
儘管整合指令碼解譯器賦予Java應用額外的能力,但同時它也使得Java應用依賴於那些指令碼庫。在確定選用某一種指令碼解譯器之前,考慮一下將來的某一天你必須修改被整合的代碼的機會。如果指令碼解譯器的開發人員很少更新或升級解譯器,這不是一個好的跡象。它或者意味著當時的解譯器實現代碼已經很完美,或者負責這些代碼的開發人員已經轉移到其他軟體項目上。至於哪一種情況的可能性比較大,答案非常明顯。
另外,還有必要看看實現解譯器需要多少原始碼。試圖掌握解譯器的每一行代碼並對它進行擴充或改進是不切實際的,因為解譯器的代碼規模實在太大了。儘管如此,瞭解解譯器的規模仍是必要的,因為在某些時候,你可能需要修改解譯器的原始碼,也可能為了掌握解譯器的具體工作原理而需要對解譯器代碼作比較深入的瞭解。
下面就來看看每一種解譯器的程式碼程式庫支援問題。
Jacl
Jacl有一個活躍的支援和開發組。儘管開發網站上的下載連結指向了一個數年前的發行版,但新的版本可通過CVS版本控制系統找到。Jacl包含約37000行Java代碼。
Jython
Jython的支援、維護和更新看起來都很活躍。Jython大約包含55000行Java代碼。
Rhino
Rhino的更新和發行都比較頻繁,它大約包含44000行Java代碼。
BeanShell
BeanShell也定期地進行更新,它大約包含25000行Java代碼,另外還有不少BeanShell指令碼提供。
可以看出,所有這些解譯器都很龐大。如果你能夠依賴於解譯器的開發和支援組織提供的改進和BUG補丁,你自己的麻煩就會少一些。在選擇一個解譯器之前,不妨看看解譯器升級和發行是否很頻繁。也許你可以與某個開發人員取得聯絡,瞭解他們的長遠計劃以及BUG修正過程。
這些解譯器都是可以免費下載的。然而,如果要把它們嵌入到商業應用之中,它們的許可規則又是怎樣的呢?好在對於所有這些解譯器來說,軟體許可都不存在什麼問題。閱讀Jacl、Jython、JavaScript和BeanShell的許可協議可以發現,使用者必須遵從GNU LGP或等價的許可。這就意味著,即使你的Java應用不是免費的,仍舊可以在發布應用時帶上指令碼解譯器。但是,你不能刪除原始碼檔案和指令檔中的著作權資訊,而且還要明確地告訴使用者,與Java應用捆綁在一起的指令碼解譯器屬於其他人所有。
七、結束語
如果你打算在Java應用中整合指令碼編程支援,我建議你只選用一個指令碼解譯器。在你的產品中,每次額外增加一種指令碼支援都會帶來相應的代價,因此應該避免在Java應用中整合一種以上的指令碼解譯器。為Java應用添加指令碼支援時,選用基於Java的解譯器而不是Perl之類的本機解譯器能夠簡化以後的工作,能夠使你的產品具有更好的可移植性,並為Java程式和解譯器的整合工作帶來方便。
如果客戶想要用某種特定的指令碼語言來定製你的產品,務必認真地檢查一下如果整合了支援該語言的指令碼解譯器是否會出現問題。如果你不必局限於某種特定的指令碼語言,則應當從多個不同的角度對解譯器進行比較,看看哪一個更適合Java應用所面臨的主要任務。
例如,與其他解譯器相比,Jacl的發展速度看起來特別慢,但如果你必須使用Tcl指令碼,使用Jacl解譯器仍舊是值得的。如果你要把一個應用從Tcl/Tk移植到Java,Jacl使得新的Java應用能夠運行原來的Tcl指令碼,這種能力的價值可能超越其他方面的不足。另外,Tcl屬於流行的程式設計語言,很多開發人員已經熟悉它,而且關於Tcl編程的書也容易買到。
如果你喜歡Java風格的指令碼代碼,並且力求減少整合過程中的麻煩,BeanShell看來很不錯。它的不足之處是,BeanShell文法和編程方面的使用者指南僅僅局限於發行版所包含的內容,而且BeanShell與其他一些指令碼解譯器相比運行速度較慢。另一方面,我覺得BeanShell比較容易使用。BeanShell的庫組織得很好,從而簡化了整合工作。如果你選擇指令碼解譯器時效能不是關鍵的考慮因素,那麼你可以考慮BeanShell。
Rhino運行速度明顯比BeanShell快,而且它也同樣支援Java風格的指令碼。另外,它看起來具有較高的開發品質和支援服務,有關JavaScript文法和編程的書也很容易找到。如果你對效能、Java風格的文法和強大的支援服務有著差不多平衡的需求, Rhino無疑是推薦考慮的。
在本文評測的四種指令碼解譯器中,Jython是速度最快的一種,擁有一些強大的編程功能。唯一真正令人擔心的是Jyphon的流程式控制制文法,不過,你可能會在乎這些文法上的差異,也可能不會在乎。就象Jacl一樣,由於需要學習的新知識比較多,用Jython編寫指令碼可能需要比JavaScript和BeanShell更長的學習時間。如果你想要用Python編寫比較複雜的指令碼,就應該買一本書。Python是一種廣受歡迎的程式設計語言,因此可供選擇的書籍也相當多。