年底Sun 公司发布了 Java Standard Edition (Java SE )的最终正式版代号 Mustang(野马)跟 Tiger(Java SE )相比Mustang 在性能方面有了不错的提升与 Tiger 在 API 库方面的大幅度加强相比虽然 Mustang 在 API 库方面的新特性显得不太多但是也提供了许多实用和方便的功能在脚本WebServiceXML编译器 API数据库JMX网络 和 Instrumentation 方面都有不错的新特性和功能加强
本系列 文章主要介绍 Java SE 在 API 库方面的部分新特性通过一些例子和讲解帮助开发者在编程实践当中更好的运用 Java SE 提高开发效率本文是系列文章的第 篇介绍了 Java SE 在脚本编程方面的新特性
Java 脚本 API 概述
Java SE 引入了对 Java Specification Request(JSR) 的支持JSR 旨在定义一个统一的规范使得 Java 应用程序可以通过一套固定的接口与各种脚本引擎交互从而达到在 Java 平台上调用各种脚本语言的目的javaxscript 包定义了这些接口即 Java 脚本编程 APIJava 脚本 API 的目标与 Apache 项目 Bean Script Framework(BSF)类似通过它 Java 应用程序就能通过虚拟机调用各种脚本同时脚本语言也能访问应用程序中的 Java 对象和方法Java 脚本 API 是连通 Java 平台和脚本语言的桥梁首先通过它为数众多的现有 Java 库就能被各种脚本语言所利用节省了开发成本缩短了开发周期其次可以把一些复杂异变的业务逻辑交给脚本语言处理这又大大提高了开发效率
在 javaxscript 包中定义的实现类并不多主要是一些接口和对应的抽象类图 显示了其中包含的各个接口和类
图 javaxscript 包概况
这个包的具体实现类少的根本原因是这个包只是定义了一个编程接口的框架规范至于对如何解析运行具体的脚本语言还需要由第三方提供实现虽然这些脚本引擎的实现各不相同但是对于 Java 脚本 API 的使用者来说这些具体的实现被很好的隔离隐藏了Java 脚本 API 为开发者提供了如下功能
获取脚本程序输入通过脚本引擎运行脚本并返回运行结果这是最核心的接口
发现脚本引擎查询脚本引擎信息
通过脚本引擎的运行上下文在脚本和 Java 平台间交换数据
通过 Java 应用程序调用脚本函数
在详细介绍这四个功能之前我们先通过一个简单的例子来展示如何通过 Java 语言来运行脚本程序这里仍然以经典的Hello World开始
清单 Hello World
import javaxscript*;
public class HelloWorld {
public static void main(String[] args) throws ScriptException {
ScriptEngineManager manager = new ScriptEngineManager();
ScriptEngine engine = managergetEngineByName(JavaScript);
engineeval(print (Hello World));
}
}
这个例子非常直观只要通过 ScriptEngineManager 和 ScriptEngine 这两个类就可以完成最简单的调用首先ScriptEngineManager 实例创建一个 ScriptEngine 实例然后返回的 ScriptEngine 实例解析 JavaScript 脚本输出运行结果运行这段程序终端上会输出Hello World在执行 eval 函数的过程中可能会有 ScriptEngine 异常抛出引发这个异常被抛出的原因一般是由脚本输入语法有误造成的在对整个 API 有了大致的概念之后我们就可以开始介绍各个具体的功能了
使用脚本引擎运行脚本
Java 脚本 API 通过脚本引擎来运行脚本整个包的目的就在于统一 Java 平台与各种脚本引擎的交互方式制定一个标准Java 应用程序依照这种标准就能自由的调用各种脚本引擎而脚本引擎按照这种标准实现就能被 Java 平台支持每一个脚本引擎就是一个脚本解释器负责运行脚本获取运行结果ScriptEngine 接口是脚本引擎在 Java 平台上的抽象Java 应用程序通过这个接口调用脚本引擎运行脚本程序并将运行结果返回给虚拟机
ScriptEngine 接口提供了许多 eval 函数的变体用来运行脚本这个函数的功能就是获取脚本输入运行脚本最后返回输出清单 的例子中直接通过字符串作为 eval 函数的参数读入脚本程序除此之外ScriptEngine 还提供了以一个 javaioReader 作为输入参数的 eval 函数脚本程序实质上是一些可以用脚本引擎执行的字节流通过一个 Reader 对象eval 函数就能从不同的数据源中读取字节流来运行这个数据源可以来自内存文件甚至直接来自网络这样 Java 应用程序就能直接利用项目原有的脚本资源无需以 Java 语言对其进行重写达到脚本程序与 Java 平台无缝集成的目的清单 即展示了如何从一个文件中读取脚本程序并运行其中如何通过 ScriptEngineManager 获取 ScriptEngine 实例的细节会在后面详细介绍
清单 Run Script
public class RunScript {
public static void main(String[] args) throws Exception {
String script = args[];
String file = args[];
FileReader scriptReader = new FileReader(new File(file));
ScriptEngineManager manager = new ScriptEngineManager();
ScriptEngine engine = managergetEngineByName(script);
engineeval(scriptReader);
}
}
清单 代码从命令行分别获取脚本名称和脚本文件名程序通过脚本名称创建对应的脚本引擎实例通过脚本名称指定的脚本文件名读入脚本程序运行运行下面这个命令就能在 Java 平台上运行所有的 JavaScript 脚本
java RunScript javascript runjs
通过这种方式Java 应用程序可以把一些复杂易变的逻辑过程用更加灵活的弱类型的脚本语言来实现然后通过 javaxScript 包提供的 API 获取运行结果当脚本改变时只需替换对应的脚本文件而无需重新编译构建项目好处是显而易见的即节省了开发时间又提高了开发效率
EngineScript 接口分别针对 String 输入和 Reader 输入提供了三个不同形态的 eval 函数用于运行脚本
表 ScriptEngine 的 eval 函数
函数 | 描述 | Object eval(Reader reader) 从一个 Reader 读取脚本程序并运行Object eval(Reader reader
Bindings n) 以 n 作为脚本级别的绑定
从一个 Reader 读取脚本程序并运行Object eval(Reader reader
ScriptContext context) 在 context 指定的上下文环境下
从一个 Reader 读取脚本程序并运行Object eval(String script) 运行字符串表示的脚本Object eval(String script
Bindings n) 以 n 作为脚本级别的绑定
运行字符串表示的脚本Object eval(String script
ScriptContext context) 在 context 指定的上下文环境下
运行字符串表示的脚本
Java 脚本 API 还为 ScriptEngine 接口提供了一个抽象类 —— AbstractScriptEngine这个类提供了其中四个 eval 函数的默认实现它们分别通过调用 eval(ReaderScriptContext) 或 eval(String ScriptContext) 来实现这样脚本引擎提供者只需继承这个抽象类并提供这两个函数实现即可AbstractScriptEngine 有一个保护域 context 用于保存默认上下文的引用SimpleScriptContext 类被作为 AbstractScriptEngine 的默认上下文关于上下文环境将在后面进行详细介绍
发现和创建脚本引擎
在前面的两个例子中ScriptEngine 实例都是通过调用 ScriptEngineManager 实例的方法返回的而不是常见的直接通过 new 操作新建一个实例JSR 中引入 ScriptEngineManager 类的意义就在于将 ScriptEngine 的寻找和创建任务委托给 ScriptEngineManager 实例处理达到对 API 使用者隐藏这个过程的目的使 Java 应用程序在无需重新编译的情况下支持脚本引擎的动态替换通过 ScriptEngineManager 类和 ScriptEngineFactory 接口即可完成脚本引擎的发现和创建
ScriptEngineManager 类自动寻找 ScriptEngineFactory 接口的实现类
ScriptEngineFactory 接口创建合适的脚本引擎实例
ScriptEngineManager 类本身并不知道如何创建一个具体的脚本引擎实例它会依照 Jar 规约中定义的服务发现机制查找并创建一个合适的 ScriptEngineFactory 实例并通过这个工厂类来创建返回实际的脚本引擎首先ScriptEngineManager 实例会在当前 classpath 中搜索所有可见的 Jar 包然后它会查看每个 Jar 包中的 META INF/services/ 目录下的是否包含 javaxscriptScriptEngineFactory 文件脚本引擎的开发者会提供在 Jar 包中包含一个 ScriptEngineFactory 接口的实现类这个文件内容即是这个实现类的完整名字ScriptEngineManager 会根据这个类名创建一个 ScriptEngineFactory 接口的实例最后通过这个工厂类来实例化需要的脚本引擎返回给用户举例来说第三方的引擎提供者可能升级更新了新版的脚本引擎实现通过 ScriptEngineManager 来管理脚本引擎无需修改一行 Java 代码就能替换更新脚本引擎用户只需在 classpath 中加入新的脚本引擎实现(Jar 包的形式)ScriptEngineManager 就能通过 Service Provider 机制来自动查找到新版本实现创建并返回对应的脚本引擎实例供调用图 所示时序图描述了其中的步骤
图 脚本引擎发现机制时序图
ScriptEngineFactory 接口的实现类被用来描述和实例化 ScriptEngine 接口每一个实现 ScriptEngine 接口的类会有一个对应的工厂类来描述其元数据(meta data)ScriptEngineFactory 接口定义了许多函数供 ScriptEngineManager 查询这些元数据ScriptEngineManager 会根据这些元数据查找需要的脚本引擎表 列出了可供使用的函数
表 ScriptEngineFactory 提供的查询函数
函数 | 描述 | String getEngineName() 返回脚本引擎的全称String getEngineVersion() 返回脚本引擎的版本信息String getLanguageName() 返回脚本引擎所支持的脚本语言的名称String getLanguageVersion() 返回脚本引擎所支持的脚本语言的版本信息List<String> getExtensions() 返回一个脚本文件扩展名组成的 List
当前脚本引擎支持解析这些扩展名对应的脚本文件List<String> getMimeTypes() 返回一个与当前引擎关联的所有 mimetype 组成的 ListList<String> getNames() 返回一个当前引擎所有名称的 List
ScriptEngineManager 可以根据这些名字确定对应的脚本引擎
通过 getEngineFactories() 函数ScriptEngineManager 会返回一个包含当前环境中被发现的所有实现 ScriptEngineFactory 接口的具体类通过这些工厂类中保存的脚本引擎信息检索需要的脚本引擎第三方提供的脚本引擎实现的 Jar 包中除了包含 ScriptEngine 接口的实现类之外还需要提供 ScriptEngineFactory 接口的实现类以及一个 javaxscriptScriptEngineFactory 文件用于指明这个工厂类这样Java 平台就能通过 ScriptEngineManager 寻找到这个工厂类并通过这个工厂类为用户提供一个脚本引擎实例Java SE 默认提供了 JavaScirpt 脚本引擎的实现如果需要支持其他脚本引擎需要将它们对应的 Jar 包包含在 classpath 中比如对于前面 清单 中的代码只需在运行程序前将 Groovy 的脚本引擎添加到 classpath 中然后运行
java RunScript groovy rungroovy
无需修改一行 Java 代码就能以 Groovy 脚本引擎来运行 Groovy 脚本在 这里 为 Java SE 提供了许多着名脚本语言的脚本引擎对 JSR 的支持这些 Jar 必须和脚本引擎配合使用使得这些脚本语言能被 Java 平台支持到目前为止它提供了至少 种脚本语言的支持其中包括了 GroovyRubyPython 等当前非常流行的脚本语言这里需要再次强调的是负责创建 ScriptEngine 实例的 ScriptEngineFactory 实现类对于用户来说是不可见的ScriptEngingeManager 实现负责与其交互通过它创建脚本引擎
脚本引擎的运行上下文
如果仅仅是通过脚本引擎运行脚本的话还无法体现出 Java 脚本 API 的优点在 JSR 中还为所有的脚本引擎定义了一个简洁的执行环境我们都知道在 Linux 操作系统中可以维护许多环境变量比如 classpathpath 等不同的 shell 在运行时可以直接使用这些环境变量它们构成了 shell 脚本的执行环境在 javaxscript 支持的每个脚本引擎也有各自对应的执行的环境脚本引擎可以共享同样的环境也可以有各自不同的上下文通过脚本运行时的上下文脚本程序就能自由的和 Java 平台交互并充分利用已有的众多 Java API真正的站在巨人的肩膀上javaxscriptScriptContext 接口和 javaxscriptBindings 接口定义了脚本引擎的上下文
Bindings 接口
继承自 Map定义了对这些键值对的查询添加删除等 Map 典型操作Bingdings 接口实际上是一个存放数据的容器它的实现类会维护许多键值对它们都通过字符串表示Java 应用程序和脚本程序通过这些键值对交换数据只要脚本引擎支持用户还能直接在 Bindings 中放置 Java 对象脚本引擎通过 Bindings 不仅可以存取对象的属性还能调用 Java 对象的方法这种双向自由的沟通使得二者真正的结合在了一起
ScriptContext 接口
将 Bindings 和 ScriptEngine 联系在了一起每一个 ScriptEngine 都有一个对应的 ScriptContext前面提到过通过 ScriptEnginFactory 创建脚本引擎除了达到隐藏实现的目的外还负责为脚本引擎设置合适的上下文ScriptEngine 通过 ScriptContext 实例就能从其内部的 Bindings 中获得需要的属性值ScriptContext 接口默认包含了两个级别的 Bindings 实例的引用分别是全局级别和引擎级别可以通过 GLOBAL_SCOPE 和 ENGINE_SCOPE 这两个类常量来界定区分这两个 Bindings 实例其中 GLOBAL_SCOPE 从创建它的 ScriptEngineManager 获得顾名思义全局级别指的是 Bindings 里的属性都是全局变量只要是同一个 ScriptEngineMananger 返回的脚本引擎都可以共享这些属性对应的引擎级别的 Bindings 里的属性则是局部变量它们只对同一个引擎实例可见从而能为不同的引擎设置独特的环境通过同一个脚本引擎运行的脚本运行时能共享这些属性
ScriptContext 接口定义了下面这些函数来存取数据
表 ScriptContext 存取属性函数
函数 | 描述 | Object removeAttribute(String name
int scope) 从指定的范围里删除一个属性void setAttribute(String name
Object value
int scope) 在指定的范围里设置一个属性的值Object getAttribute(String name) 从上下文的所有范围内获取优先级最高的属性的值Object getAttribute(String name
int scope) 从指定的范围里获取属性值
ScriptEngineManager 拥有一个全局性的 Bindings 实例在通过 ScriptEngineFactory 实例创建 ScriptEngine 后它把自己的这个 Bindings 传递给所有它创建的 ScriptEngine 实例作为 GLOBAL_SCOPE同时每一个 ScriptEngine 实例都对应一个 ScriptContext 实例这个 ScriptContext 除了从 ScriptEngineManager 那获得的 GLOBAL_SCOPE自己也维护一个 ENGINE_SCOPE 的 Bindings 实例所有通过这个脚本引擎运行的脚本都能存取其中的属性除了 ScriptContext 可以设置属性改变内部的 BindingsJava 脚本 API 为 ScriptEngineManager 和 ScriptEngine 也提供了类似的设置属性和 Bindings 的 API
图 Bindings 在 Java 脚本 API 中的分布
从 图 中可以看到共有三个级别的地方可以存取属性分别是 ScriptEngineManager 中的 BindingsScriptEngine 实例对应的 ScriptContext 中含有的 Bindings以及调用 eval 函数时传入的 Bingdings离函数调用越近其作用域越小优先级越高相当于编程语言中的变量的可见域即 Object getAttribute(String name) 中提到的优先级从 清单 这个例子中可以看出各个属性的存取优先级
清单 上下文属性的作用域
import javaxscript*;
public class ScopeTest {
public static void main(String[] args) throws Exception {
String script= println(greeting) ;
ScriptEngineManager manager = new ScriptEngineManager();
ScriptEngine engine = managergetEngineByName(javascript);
//Attribute from ScriptEngineManager
managerput(greeting Hello from ScriptEngineManager);
engineeval(script);
//Attribute from ScriptEngine
engineput(greeting Hello from ScriptEngine);
engineeval(script);
//Attribute from eval method
ScriptContext context = new SimpleScriptContext();
contextsetAttribute(greeting Hello from eval method
ScriptContextENGINE_SCOPE);
engineeval(scriptcontext);
}
}
JavaScript 脚本 println(greeting) 在这个程序中被重复调用了三次由于三次调用的环境不一样导致输出也不一样greeting 变量每一次都被优先级更高的也就是距离函数调用越近的值覆盖从这个例子同时也演示了如何使用 ScriptContext 和 Bindings 这两个接口在例子脚本中并没有定义 greeting 这个变量但是脚本通过 Java 脚本 API 能方便的存取 Java 应用程序中的对象输出 greeting 相应的值运行这个程序后能看到输出为
图 程序 ScopeTest 的输出
除了能在 Java 平台与脚本程序之间的提供共享属性之外ScriptContext 还允许用户重定向引擎执行时的输入输出流
表 ScriptContext 输入输出重定向
函数 | 描述 | void setErrorWriter(Writer writer) 重定向错误输出
默认是标准错误输出void setReader(Reader reader) 重定向输入
默认是标准输入void setWriter(Writer writer) 重定向输出
默认是标准输出Writer getErrorWriter() 获取当前错误输出字节流Reader getReader() 获取当前输入流Writer getWriter() 获取当前输出流
清单 展示了如何通过 ScriptContext 将其对应的 ScriptEngine 标准输出重定向到一个 PrintWriter 中用户可以通过与这个 PrintWriter 连通的 PrintReader 读取实际的输出使 Java 应用程序能获取脚本运行输出满足更加多样的应用需求
清单 重定向脚本输出
import javaio*;
import javaxscript*;
public class Redirectory {
public static void main(String[] args) throws Exception {
ScriptEngineManager manager = new ScriptEngineManager();
ScriptEngine engine = managergetEngineByName(javascript);
PipedReader pr = new PipedReader();
PipedWriter pw = new PipedWriter(pr);
PrintWriter writer = new PrintWriter(pw);
enginegetContext()setWriter(writer);
String script = println(Hello from JavaScript);
engineeval(script);
BufferedReader br =new BufferedReader(pr);
Systemoutprintln(brreadLine());
}
}
Java 脚本 API 分别为这两个接口提供了一个简单的实现供用户使用SimpleBindings 通过组合模式实现 Map 接口它提供了两个构造函数无参构造函数在内部构造一个 HashMap 实例来实现 Map 接口要求的功能同时SimpleBindings 也提供了一个以 Map 接口作为参数的构造函数允许任何实现 Map 接口的类作为其组合的实例以满足不同的要求SimpleScriptContext 提供了 ScriptContext 简单实现默认情况下它使用了标准输入标准输出和标准错误输出同时维护一个 SimpleBindings 作为其引擎级别的 Bindings它的默认全局级别 Bindings 为空
脚本引擎可选的接口
在 Java 脚本 API 中还有两个脚本引擎可以选择是否实现的接口这个两个接口不是强制要求实现的即并非所有的脚本引擎都能支持这两个函数不过 Java SE 自带的 JavaScript 引擎支持这两个接口无论如何这两个接口提供了非常实用的功能它们分别是
Invocable 接口允许 Java 平台调用脚本程序中的函数或方法
Compilable 接口允许 Java 平台编译脚本程序供多次调用
Invocable 接口
有时候用户可能并不需要运行已有的整个脚本程序而仅仅需要调用其中的一个过程或者其中某个对象的方法这个时候 Invocable 接口就能发挥作用它提供了两个函数 invokeFunction 和 invokeMethod分别允许 Java 应用程序直接调用脚本中的一个全局性的过程以及对象中的方法调用后者时除了指定函数名字和参数外还需要传入要调用的对象引用当然这需要脚本引擎的支持不仅如此Invocable 接口还允许 Java 应用程序从这些函数中直接返回一个接口通过这个接口实例来调用脚本中的函数或方法从而我们可以从脚本中动态的生成 Java 应用中需要的接口对象清单 演示了如何使用一个 Invocable 接口
清单 调用脚本中的函数
import javaxscript*;
public class CompilableTest {
public static void main(String[] args) throws ScriptException
NoSuchMethodException {
String script = function greeting(message){println (message);};
ScriptEngineManager manager = new ScriptEngineManager();
ScriptEngine engine = managergetEngineByName(javascript);
engineeval(script);
if (engine instanceof Invocable) {
Invocable invocable = (Invocable) engine;
invocableinvokeFunction(greeting hi);
// It may through NoSuchMethodException
try {
invocableinvokeFunction(nogreeing);
} catch (NoSuchMethodException e) {
// expected
}
}
}
}
在调用函数前可以先通过 instanceof 操作判断脚本引擎是否支持编译操作防止类型转换时抛出运行时异常需要特别注意的时如果调用了脚本程序中不存在的函数时运行时会抛出一个 NoSuchMethodException 的异常实际开发中应该注意处理这种特殊情况
Compilable 接口
一般来说脚本语言都是解释型的这也是脚本语言区别与编译语言的一个特点解释性意味着脚本随时可以被运行开发者可以边开发边查看接口从而省去了编译这个环节提供了开发效率但是这也是一把双刃剑当脚本规模变大重复解释一段稳定的代码又会带来运行时的开销有些脚本引擎支持将脚本运行编译成某种中间形式这取决与脚本语言的性质以及脚本引擎的实现可以是一些操作码甚至是 Java 字节码文件实现了这个接口的脚本引擎能把输入的脚本预编译并缓存从而提高多次运行相同脚本的效率
Java 脚本 API 还为这个中间形式提供了一个专门的类每次调用 Compilable 接口的编译函数都会返回一个 CompiledScript 实例CompiledScript 类被用来保存编译的结果从而能重复调用脚本而没有重复解释的开销实际效率提高的多少取决于中间形式的彻底程度其中间形式越接近低级语言提高的效率就越高每一个 CompiledScript 实例对应于一个脚本引擎实例一个脚本引擎实例可以含有多个 CompiledScript(这很容易理解)调用 CompiledScript 的 eval 函数会传递给这个关联的 ScriptEngine 的 eval 函数关于 CompiledScript 类需要注意的是它运行时对与之对应的 ScriptEngine 状态的改变可能会传递给下一次调用造成运行结果的不一致清单 演示了如何使用 Compiable 接口来调用脚本
清单 编译脚本
import javaxscript*;
public class CompilableTest {
public static void main(String[] args) throws ScriptException {
String script = println (greeting); greeting= Good Afternoon! ;
ScriptEngineManager manager = new ScriptEngineManager();
ScriptEngine engine = managergetEngineByName(javascript);
engineput(greeting Good Morning!);
if (engine instanceof Compilable) {
Compilable compilable = (Compilable) engine;
CompiledScript compiledScript = pile(script);
compiledScripteval();
compiledScripteval();
}
}
}
与 InovcableTest 类似也应该先通过 instanceof 操作判断脚本引擎是否支持编译操作防止预料外的异常抛出并且我们可以发现同一段编译过的脚本在第二次运行时 greeting 变量的内容被上一次的运行改变了导致输出不一致
图 程序 CompilableTest 的输出
jrunscript 工具
Java SE 还为运行脚本添加了一个专门的工具 —— jrunscriptjrunscript 支持两种运行方式一种是交互式即边读取边解析运行这种方式使得用户可以方便调试脚本程序马上获取预期结果还有一种就是批处理式即读取并运行整个脚本文件用户可以把它想象成一个万能脚本解释器即它可以运行任意脚本程序而且它还是跨平台的当然所有这一切都有一个前提那就是必须告诉它相应的脚本引擎的位置默认即支持的脚本是 JavaScript这意味着用户可以无需任何设置通过 jrunscript 在任何支持 Java 的平台上运行任何 JavaScript 脚本如果想运行其他脚本可以通过 l 指定以何种脚本引擎运行脚本不过这个工具仍是实验性质的不一定会包含在 Java 的后续版本中无论如何它仍是一个非常有用的工具
结束语
在 Java 平台上使用脚本语言编程非常方便因为 Java 脚本 API 相对其他包要小很多通过 javaxscript 包提供的接口和类我们可以很方便为我们的 Java 应用程序添加对脚本语言的支持开发者只要遵照 Java 脚本 API 开发应用程序开发中就无需关注具体的脚本语言细节应用程序就可以动态支持任何符合 JSR 标准的脚本语言不仅如此只要按照 JSR 标准开发用户甚至还能为 Java 平台提供一个自定义脚本语言的解释器在 Java 平台上运行自己的脚本语言这对于众多开发者来说都是非常有诱惑力的