Java Solaris 加入 SDN 参与讨论 我的社区 注册说明
 
 
 
 
 
 
Java API 文档中文版
使用脚本实现设计和性能的平衡
 
By Dejan Bosanac, 1/21/09  
内容

众所周知,各种语言都有各自的特点,有的语言可以通过动态类型或动态闭包之类的特性为我们带来好处。许多资料都介绍了如何通过特定语言中的具体特性来简化编程,或者使用应用程序具有炫酷的功能,如此等等。但 是,用户可能只会使用脚本实现 Java 应用程序中的一部分,因此这里有一个经常被人忽略的问题:应该在何时以何种方式使用脚本语言?

在本文中,我将介绍 Scripting API 中的一些高级概念,并演示它们如何帮助您在 Java 应用程序中成功地使用脚本。

在 JVM 中使用脚本语言的基本条件是拥有一个可通过 Java 访问的解释程序(也称作引擎)。有两种常用的方法可实现此目的:在 Java 中实现引擎或在本机语言解释程序外设计一个 Java 封装器。脚本引擎的特性各不相同(详见下文),但是其中很少能代表每个解释程序的核心功能。每个引擎都应该能够为脚本的执行(evaluation)提供上下文,并且显然应该能够执行脚本。

在最简单的场景中,我们将在 Java 应用程序中针对一些语言实例化一个脚本引擎,并绑定一些变量为引擎提供上下文,然后在某个时刻执行脚本。此后,我们还可以从引擎上下文获取一些变量值,因 为被执行的代码可以修改(或设置)这些值。

显然,框架需要抽象出各种脚本引擎并借此为 Java 应用程序创建通用的脚本支持。Java SE 6 中的 Scripting API( javax.script)就是出于这种目的。它支持简单的引擎注册、通过工厂方法实例化引擎,以及在方法之间共享上下文。

在本文中,我将主要介绍其他一些方面。如今,许多可用的脚本引擎都向开发人员提供了较多的特性用于简化脚本执行。其中一个特性便是能够调用脚本中定义的独立的函数和对象方法。在一些语言中,您 甚至还可以在脚本中实现整个 Java 接口并在 Java 中把它们当作常规对象使用。我们将看到,这些特性可以极大地影响我们在应用程序中使用脚本的方式。但是首先,我必须了解 Scripting API 如何支持这些特性。

javax.script.Invocable

Scripting API 的主要设计目的之一就是尽可能的实现通用性。这样众多脚本引擎便可以依照它。鉴于此, javax.script.ScriptEngine 接口只定义了最基本的脚本引擎操作:变量绑定和脚本执行。脚本引擎可以实现的所有其他高级特性都封装在一个单独的接口中,这 样开发人员便可以轻易地判断特定引擎的特征并适当地使用它。这种体系结构还允许通过此 API 使用非常简单的引擎。

函数

在以下示例中,我将演示 Invocable 接口。首先,我将创建一个简单的脚本(本文的所有示例都将通过 JavaScript 实现,因此可以使用 JKD 6 中的 Rhino 引擎 来执行它们):

function sayHello(name) {

println("Hello " + name);
}

这个简单的 JavaScript 示例位于 function.js 文件中(并且可从本文的 参考资料 部分获得),其中定义了一个 sayHello 函数,用于向标准输出打印文本。现在,我们来看看如何使用 Scripting API 调用这种函数。

package net.scriptinginjava.invocable;


import java.io.FileReader;

import javax.script.Invocable;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;

public class InvocableTest {

public static void main(String[] args) throws Exception {
ScriptEngineManager factory = new ScriptEngineManager();
ScriptEngine engine = factory.getEngineByName("js");
if (engine instanceof Invocable) {
engine
.eval(new FileReader(
"src/net/scriptinginjava/invocable/function.js"));
((Invocable) engine).invokeFunction("sayHello", "World");
}
}

}

这个 Java 应用程序将实例化 ScriptEngineManager 类,并使用该实例获取需要的脚本引擎,然后使用该引擎处理脚本。

Invocable 接口的使用非常有趣。如您所见,如果引擎实现了 Invocable 接口,则 Java 开发人员可以使用它的 InvokeFunction 方法调用之前被处理脚本中的函数。该方法使用函数名称作为第一个参数,另一个参数是 Object 变量(varargs)的变量名称。在本例中,代码将调用 sayHello 函数和将 World 变量传递给该函数。结果,代码将通过标准输出打印出“Hello World”文本。

方法

对于面向对象的脚本语言,可以使用相同的接口调用处理脚本所创建的对象的方法。为演示此特性,我将创建一个适当的脚本(位于 method.js 文件中):

function Hello() {}


Hello.prototype.sayHello = function(value) {
with (this) println("Hello " + value);
}

var hello = new Hello();
hello.sayHello("World1");

该脚本将创建一个含有 sayHello 原型函数的 Hello 类。然后,它将实例化一个 hello 变量并调用它的方法。结果,这个脚本将通过标准输出打印“Hello World1”文本。

接下来,我们看看以下这段 Java 代码。

package net.scriptinginjava.invocable;


import java.io.FileReader;

import javax.script.Invocable;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;

public class MethodTest {

public static void main(String[] args) throws Exception {
ScriptEngineManager factory = new ScriptEngineManager();
ScriptEngine engine = factory.getEngineByName("js");
if (engine instanceof Invocable) {
engine
.eval(new FileReader(
"src/net/scriptinginjava/invocable/method.js"));
((Invocable) engine).invokeMethod(engine
.get("hello"), "sayHello", "World2");
}
}

}

同样,此示例与之前处理脚本并执行脚本中定义的函数的 Java 程序极为相似。惟一区别就是这个示例使用 invokeMethod 方法调用对象方法。请回想我们在本例中所使用的脚本:它将实例化一个 hello 对象。这样,我们便可以使用 engine.get("hello") 语句通过引擎的上下文获取该对象。这正是传递给 invokeMethod 方法的第一个参数;该对象定义在我们希望调用的一个处理脚本中。其他参数与 invokeFunction 方法相同:方法名称和对象变量编号将作为参数传递给该方法。在本例中,我们调用了 sayHello 方法将传递 World2 作为惟一的参数。由于 sayHello 方法被调用了两次(脚本调用了一次,Java 应用程序调用了一次),因此应用程序将通过标准输出打印以下内容:

Hello World1

Hello World2

接口

上述所有示例都小巧且简单,但是 Invocable 接口的真正威力在于它可用于通过脚本实现 Java 接口(当然,使用支持此特性的语言中)。我将回过头来讨论这个特性为何如此重要以及它可以为 Java 项目带来哪些好处。但是,首先我将使用一个简单的程序演示该特性。

我们先定义一个简单的接口:

package net.scriptinginjava.invocable;


public interface Hello {

public void sayHello(String name);

public void time();

}

现在,还记得第一个 JavaScript 示例如何使用 Scripting API 调用函数吗?它定义了一个 sayHello 函数,该函数只有且个参数。我们来看看如何使用这个简单的脚本实现上面的定义的接口。

package net.scriptinginjava.invocable;


import java.io.FileReader;

import javax.script.Invocable;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;

public class InterfaceTest {

public static void main(String[] args) throws Exception {
ScriptEngineManager factory = new ScriptEngineManager();
ScriptEngine engine = factory.getEngineByName("js");
if (engine instanceof Invocable) {
engine
.eval(new FileReader(
"src/net/scriptinginjava/invocable/function.js"));
Hello hello = ((Invocable)engine).getInterface(Hello.class);
hello.sayHello("World");
//hello.time();
}
}

}

执行这个应用程序,可以发现它将打印输出“Hello World”文本,这意味着我们使用三行脚本代码成功地实现了一个接口。

接下来,我将详细介绍这个示例。值得注意,本例所使用的 Rhino 引擎允许用户实现各个接口,其方式是在提供的脚本中定义一组实现接口方法的函数。还有,如果要成功实现一个接口,我 们不需要为所有方法都提供函数,而只需提供那些需要使用的函数。在本例中,我们为 sayHello 方法提供了一个实现并成功在 Java 应用程序中使用了它。您可以尝试调用 time 方法,应用程序将抛出异常显示 time 方法并未实现。当然,如果我们此时使用以下方式重写脚本:

function sayHello(name) {

println("Hello " + name);
}

function time() {
println(new Date());
}

然后重新执行 Java 应用程序,我们将获得以下输出结果:

Hello World

Wed Aug 29 2007 14:53:16 GMT+0200 (CEST)

各种语言实现 Java 接口的机制也各不相同。如果要在所选脚本引擎中实现接口,请先参考产品的说明文档(也可以参阅 参考资料 部分了解更多信息)。但是支持此特性所有引擎都有一个共同之处:它们支持使用合适的脚本快速方便地实现 Java 接口。

实际使用

我已经简要介绍了 Scripting API 的 Invocable 接口。接下来,我将讨论它可以为整个开发流程提供哪些价值。也许每个 Java 开发人员都清楚接口和干净 API 设计的重要性。使 用接口的软件开发设计方式强调将接口和它们的实现分离。一般而言,该方法是在 Java(和面向对象编程)中所使用的基本设计技巧。

如果要将应用程序的某个部分定义为一个 软件组件服务并将其公开以供本地或远程使用,则需要定义一个结构良好的 接口。这个接口的主要任务是定义 组件应实现的所有 操作。该接口的实现通常隐藏在系统底部,并且对于组件的最终用户是不可见的。当然,同一接口可以有许多实现,它们通过根据使用组件的上下文而有所不同。毫 无疑问,这样的应用程序程序设计方式更加干净简洁,并且是许多广泛使用的设计模式的基础(详见下文)。

我相信您多年前就已经清楚上面提到的原则,但是上面的讨论对于回答下面这个问题相当重要:使用脚本实现 Java 接口和 javax.script.Invocable 接口将如何改进我们的开发流程?我不想在这里争论动态类型、闭包、运行时修改和动态语言一般提供的其他特性(有关这些方面的更多信息,请 参阅 参考资料 一节)。我将谈论如何在 Java 应用程序合理地使用脚本。您可能已经猜到了, Invocable 接口将发挥重要的作用。

在 Java 应用程序的特定点实例化脚本引擎和执行脚本是很简单的事情。虽然这一实践在开发流程中有明确的位置,但是无结构(或结构松散)的脚本将对良好的面向对象设计造成障碍。应 用程序最可能使用良好的面向对象实践和许多设计模式(如 IoC),因此您需要使用不会对原来的努力造成影响的解决方案来实现总体良好的应用程序设计。因此, 使用接口的设计原则结合在脚本中实现的接口可以同时实现良好的面向对象设计软件和快速开发(使用脚本语言)。

当然,谈论到脚本时还有一个重要的问题需要注意,那就是性能。如果我们使用 JavaScript 或 Groovy 编写的脚本实现一些大部分接口,那么这对整个解决方案性能有什么影响呢?幸运的是,一 些机制和模式可以帮助我们解决此问题。

理想的情况是在开发阶段拥有动态语言的灵活性,而在生产过程拥有纯 Java 语言的性能。幸运的时,大多数脚本语言都提供了一些机制可实现这一目的。主要为将脚本编译为 Java 类的功能。这样,我 们就可以使用它们实例化常规 Java 对象。大多数“模型”脚本引擎都提供了这种功能,但是其中一些(比如说 Groov)还更进一步定义了一些 Ant 任务或 Maven 插件以供开发人员在构建流程中使用。

现在,我们在可执行源代码中拥有了脚本并在项目中拥有该脚本的编译过的版本。实现原始目标已经简单的许多。有多种方法可以实例化某个接口的具体实现,这取决于应用程序所满足的特定条件。此处,我 将演示最为著名的 工厂方法模式。

简而言之,该模式建议您不要直接实例对象,而是将此操作封装在一个合适的 工厂方法中,从而使流程更具灵活性。假设在一个编译环境中,实现 Hello 的脚本被编译为了 HelloImpl 类。我不会研究如何在不同的环境中实现此操作,因为该内容不在本文的讨论范围之内。请看下面这个 Java 示例:

package net.scriptinginjava.invocable;


import java.io.FileReader;

import javax.script.Invocable;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;

public class HelloFactory {

private boolean debug = true;

private ScriptEngine engine;

public HelloFactory(boolean debug) {
this.debug = debug;
ScriptEngineManager factory = new ScriptEngineManager();
engine = factory.getEngineByName("js");
}

public Hello getHello() throws Exception {
if (debug) {
System.out.println("scripted");
engine
.eval(new FileReader(
"src/net/scriptinginjava/invocable/interface.js"));
return ((Invocable) engine)
.getInterface(Hello.class);
} else {
System.out.println("compiled");
return new HelloImpl();
}
}

public static void main(String[] args) throws Exception {
HelloFactory client = new HelloFactory(true);
Hello hello = client.getHello();
hello.sayHello("World");
}

}

这个 Java 类代表一个工厂,负责实例化 Hello 接口的实现。这个示例还有一些重要的地方需要注意。首先,您会发现这个类有一个 debug 属性,用于判断是否应该执行某个脚本或实例化某个编译过的对象。该属性在类的构造函数中设置,在 getHello 方法中使用。

现在,如果运行此示例程序,则输出结果为:

scripted

Hello World

这表示我们通过执行脚本实例化了一对象。您可以尝试将 debug 参数的值修改为 false,然后再看看会发生什么。它的输出内容如下:

compiled

Hello World

通过这些技巧,我们实现了既定目标:即动态的开发环境和具有纯 Java 性能生产环境。当然, debug 参数只是一个简单的例子,您也可以使用适合自己开发环境的技巧(如 IoC 框架、属性文件配置等等),但是其中的基本原理是相同的。使用脚本实现接口,并 获得动态语言的灵活性(比如说在运行时修改应用程序的功能)。完成之后,编译脚本并重新配置应用程序使用编译过的接口实现(一个纯 Java 类)。

上面所介绍的设计模式是可以在 Java 应用程序应用的许多基于脚本的设计模式的基础。许多“传统”设计模式的元素都适合使用脚本实现(不过您也可以找到特定于脚本环境的模式)。我在 Scripting in Java 这本书的 第 8 章 中讨论其中的一些模式。

结束语

谈到 Java 应用程序中的脚本语言时,有两个关键的问题是不可逃避的:它将对软件体系结构造成哪些影响?它将对软件的性能造成哪些影响?以 使用接口的设计原则为基础,并应用本文所介绍的这个简单的模式,我们可以成功地解决这两个问题。在其余的工作中,您只需选择最适合您编程需求的语言,并 掌握在项目中使用脚本的时机的数量。

参考资料