脚本语言能够为java应用带来前所未有的能力。然而,在Java应用中支持脚本语言也必然会面临相应的风险和代价。选择一种合适的脚本语言能够将这些风险和代价降低到最低程度。
脚本解释器概述
在一些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循环:
表二:比较整数是否相等,1000000次:
表三:分配并初始化100000个元素的数组:
表四:分配并初始化500 X 500 个元素的数组:
表五:在JVM内初始化解释器所需要的内存
本项评测证实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