站内搜索: 请输入搜索关键词

当前页面: 开发资料首页Java 专题为Java创建你自己的脚本语言-JSR 223介绍

为Java创建你自己的脚本语言-JSR 223介绍

摘要: 为Java创建你自己的脚本语言-JSR 223介绍
内容: 摘要
即将发布的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.html
Matrix:http://www.matrix.org.cn/resource/article/44/44604_JSR+223.html
关键字:JSR 223

BoolScript引擎
图一显示了我们的示例的各个部分以及他们是如何相互关联的。这篇文章里的示例定义了一个简单的语言,我称之为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 Rhino
Bool Script Engine
answer of boolean expression is: false
answer of boolean expression is: true
answer 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 = True
y = 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 ScriptEngine
bsEngine.put("x", BoolTermEvaluator.tTrue);
bsEngine.put("y", BoolTermEvaluator.tTrue);
bsEngine.eval("x & y\n\n");


这段代码先将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 ScriptEngine
Compilable compiler = (Compilable) bsEngine;
CompiledScript compiledScript = compiler.compile("x & y\n\n");
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代码
后一篇: 在Eclipse RCP中实现控制反转(IoC)