java

位置:IT落伍者 >> java >> 浏览文章

给 Java SE 注入脚本语言的活力


发布日期:2021年10月02日
 
给 Java SE 注入脚本语言的活力

在即将发布的 Java SE(Mustang)中增加了对脚本语言的支持通过对脚本语言的调用使得一些通常用 Java 比较难于实现的功能变得简单和轻便脚本语言与 Java 之间的互操作将变得优雅而直接

脚本语言与 Java

假设我们有一个简单的需求察看一份文档中 个字母组成的单词的个数用 Java 一般实现如下

import javaioBufferedReader;import javaioFileReader;import javaioIOException;public class FindWords {    public static void main(String[] args) throws IOException {        String result = ;        String line = null;        int num = ;        FileReader fr = new FileReader(filename);        BufferedReader br = new BufferedReader(fr);        while ((line = brreadLine()) != null) {            result += line;        }        brclose();        frclose();        String[] s = resultsplit( );        for (int i = ; i < slength; i++) {            if (s[i]matches(^\\w{}$)) {                num++;            }        }        Systemoutprintln(num);    }}

再看看 Perl 语言实现同样功能的代码

open FILE <filename ;while (<FILE>) {     for (split) {         $num++ if /^\w{}$/     } }print $num;

那么有没有一种优雅的方式将 Java 与脚本语言结合呢在今年秋季即将发布的 Java SE(代号 Mustang)中这将成为现实

Mustang 的脚本引擎

JSR 为 Java 设计了一套脚本语言 API这一套 API 提供了在 Java 程序中调用各种脚本语言引擎的接口任何实现了这一接口的脚本语言引擎都可以在 Java 程序中被调用在 Mustang 的发行版本中包括了一个基于 Mozilla Rhino 的 JavaScript 脚本引擎

Mozilla Rhino

Rhino 是一个纯 Java 的开源的 JavaScript 实现他的名字来源于 OReilly 关于 JavaScript 的书的封面

Rhino 项目可以追朔到 当时 Netscape 计划开发一个纯 Java 实现的 Navigator为此需要一个 Java 实现的 JavaScript —— Javagator它也就是 Rhino 的前身起初 Rhino 将 JavaScript 编译成 Java 的二进制代码执行这样它会有最好的性能后来由于编译执行的方式存在垃圾收集的问题并且编译和装载过程的开销过大不能满足一些项目的需求Rhino 提供了解释执行的方式随着 Rhino 开放源代码越来越多的用户在自己的产品中使用了 Rhino同时也有越来越多的开发者参与了 Rhino 的开发并做出了很大的贡献如今 RhinoR 版本将被包含在 Java SE 中发行更多的 Java 开发者将从中获益

Rhino 提供了如下功能

对 JavaScript 的完全支持

直接在 Java 中使用 JavaScript 的功能

一个 JavaScript shell 用于运行 JavaScript 脚本

一个 JavaScript 的编译器用于将 JavaScript 编译成 Java 二进制文件

支持的脚本语言

在可以找到官方的脚本引擎的实现项目这一项目基于BSD License 表示这些脚本引擎的使用将十分自由目前该项目已对包括 Groovy JavaScript Python Ruby PHP 在内的二十多种脚本语言提供了支持这一支持列表还将不断扩大

在 Mustang 中对脚本引擎的检索使用了工厂模式首先需要实例化一个工厂 —— ScriptEngineManager

// create a script engine managerScriptEngineManager factory = new ScriptEngineManager();

ScriptEngineManager 将在 Thread Context ClassLoader 的 Classpath 中根据 jar 文件的 METAINF 来查找可用的脚本引擎它提供了 种方法来检索脚本引擎

// create engine by nameScriptEngine engine = factorygetEngineByName (JavaScript);// create engine by nameScriptEngine engine = factorygetEngineByExtension (js);// create engine by nameScriptEngine engine = factorygetEngineByMimeType (application/javascript);

下面的代码将会打印出当前的 JDK 所支持的所有脚本引擎

ScriptEngineManager factory = new ScriptEngineManager();for (ScriptEngineFactory available : factorygetEngineFactories()) {    Systemoutprintln(availablegetEngineName());}

以下各章节代码将以 JavaScript 为例

在 Java 中解释脚本

有了脚本引擎实例就可以很方便的执行脚本语言按照惯例我们还是从一个简单的 Hello World 开始

public class RunJavaScript {    public static void main(String[] args){        ScriptEngineManager factory = new ScriptEngineManager();        ScriptEngine engine = factorygetEngineByName (JavaScript);        engineeval(print(Hello World));    }}

这段 Java 代码将会执行 JavaScript 并打印出 Hello World如果 JavaScript 有语法错误将会如何?

engineeval(if(true){println (hello));

故意没有加上}执行这段代码 Java 将会抛出一个 javaxscriptScriptException 并准确的打印出错信息

Exception in thread main javaxscriptScriptException: mozillajavascriptinternalEvaluatorException: missing } in compound statement (<Unknown source>#) in <Unknown source> at line number at

如果我们要解释一些更复杂的脚本语言或者想在运行时改变该脚本该如何做呢?脚本引擎支持一个重载的 eval 方法它可以从一个 Reader 读入所需的脚本

ScriptEngineManager factory = new ScriptEngineManager();ScriptEngine engine = factorygetEngineByName (JavaScript);engineeval(new Reader(HelloWorldjs));

如此这段 Java 代码将在运行时动态的寻找 HelloWorldjs 并执行用户可以随时通过改变这一脚本文件来改变 Java 代码的行为做一个简单的实验Java 代码如下

public class RunJavaScript {    public static void main(String[] args) throws FileNotFoundException             ScriptException InterruptedException {        ScriptEngineManager factory = new ScriptEngineManager();        ScriptEngine engine = factorygetEngineByName (JavaScript);        while (true) {            engineeval(new FileReader(HelloWorldjs));            Threadsleep();        }    }}

HelloWorldjs 内容为简单的打印一个 Hello World print(Hello World);

运行 RunJavaScript 将会每一秒钟打印一个 Hello World这时候修改 HelloWorldjs 内容为 print(Hello Tony);

打印的内容将变为 Hello Tony由此可见 Java 程序将动态的去读取脚本文件并解释执行对于这一简单的 Hello World 脚本来说IO 操作将比直接执行脚本损失 % 左右的性能(在我的 Think Pad 上)但他带来的灵活性——在运行时动态改变代码的能力在某些场合是十分激动人心的

脚本语言与 Java 的通信

ScriptEngine 的 put 方法用于将一个 Java 对象映射成一个脚本语言的变量现在有一个 Java Class它只有一个方法功能就是打印一个字符串 Hello World

package tony;public class HelloWorld {    String s = Hello World;    public void sayHello(){        Systemoutprintln(s);    }}

那么如何在脚本语言中使用这个类呢?put 方法可以做到

import javaxscriptScriptEngine;import javaxscriptScriptEngineManager;import javaxscriptScriptException;public class TestPut {    public static void main(String[] args) throws ScriptException {        ScriptEngineManager factory = new ScriptEngineManager();        ScriptEngine engine = factorygetEngineByName(JavaScript);        HelloWorld hello = new HelloWorld();        engineput(script_hello hello);        engineeval(script_hellosayHello());    }}

首先我们实例化一个 HelloWorld然后用 put 方法将这个实例映射为脚本语言的变量 script_hello那么我们就可以在 eval() 函数中像 Java 程序中同样的方式来调用这个实例的方法同样的假设我们有一个脚本函数它进行一定的计算并返回值我们在 Java 代码中也可以方便的调用这一脚本

package tony;import javaxscriptInvocable;import javaxscriptScriptEngine;import javaxscriptScriptEngineManager;import javaxscriptScriptException;public class TestInv {    public static void main(String[] args) throws ScriptException            NoSuchMethodException {        ScriptEngineManager factory = new ScriptEngineManager();        ScriptEngine engine = factorygetEngineByName(JavaScript);        String script = function say(firstsecond) { print(first + + second); };        engineeval(script);        Invocable inv = (Invocable) engine;        invinvokeFunction(say Hello Tony);    }}

在这个例子中我们首先定义了一个脚本函数 say它的作用是接受两个字符串参数将他们拼接并返回这里我们第一次遇到了 ScriptEngine 的两个可选接口之一 —— InvocableInvocable 表示当前的 engine 可以作为函数被调用这里我们将 engine 强制转换为 Invocable 类型使用 invokeFunction 方法将参数传递给脚本引擎invokeFunction这个方法使用了可变参数的定义方式可以一次传递多个参数并且将脚本语言的返回值作为它的返回值下面这个例子用JavaScript实现了一个简单的max函数接受两个参数返回较大的那个为了便于断言结果正确性这里继承了JUnit Testcase关于JUnit请参考

package tony;import javaxscriptInvocable;import javaxscriptScriptEngine;import javaxscriptScriptEngineManager;import javaxscriptScriptException;import junitframeworkTestCase;public class TestScripting extends TestCase {        public void testInv() throws ScriptException NoSuchMethodException {        ScriptEngineManager factory = new ScriptEngineManager();        ScriptEngine engine = factorygetEngineByName(JavaScript);        String script = function max(firstsecond)     + { return (first > second) ?first:second;};        engineeval(script);        Invocable inv = (Invocable) engine;        Object obj = invinvokeFunction(max );        assertEquals( objtoString());}}

Invocable 接口还有一个方法用于从一个 engine 中得到一个 Java Interface 的实例它的定义如下

<T> T getInterface(Class<T> clasz)

它接受一个 Java 的 Interface 类型作为参数返回这个 Interface 的一个实例也就是说你可以完全用脚本语言来写一个 Java Interface 的所有实现以下是一个例子首先定一了个 Java Interface它有两个简单的函数分别为求最大值和最小值

package tony;public interface MaxMin {    public int max(int a int b);    public int min(int a int b);}

这个 Testcase 用 JavaScript 实现了 MaxMin 接口然后用 getInterface 方法返回了一个实例并验证了结果

public void testInvInterface() throws ScriptException        NoSuchMethodException {    ScriptEngineManager factory = new ScriptEngineManager();    ScriptEngine engine = factorygetEngineByName(JavaScript);    String script = function max(firstsecond)     + { return (first > second) ?first:second;};    script += function min(firstsecond) { return (first < second) ?first:second;};    engineeval(script);    Invocable inv = (Invocable) engine;    MaxMin maxMin = invgetInterface(MaxMinclass);    assertEquals( maxMinmax( ));    assertEquals( maxMinmin( ));}

脚本的编译执行

到目前为止我们的脚本全部都是解释执行的相比较之下编译执行将会获得更好的性能这里将介绍 ScriptEngine 的另外一个可选接口 —— Compilable实现了这一接口的脚本引擎支持脚本的编译执行下面这个例子实现了一个判断给定字符串是否是 email 地址或者 ip 地址的脚本

public void testComplie() throws ScriptException {    ScriptEngineManager manager = new ScriptEngineManager();    ScriptEngine engine = managergetEngineByName(JavaScript);    String script = var email=/^[azAZ_]+@[azAZ_]         + +(\\[azAZ_]+)+$/;;    script += var ip = /^(\\d{}|\\d\\d|[]\\d|[])        +(\\(\\d{}|\\d\\d|[]\\d|[])){}$/;;    script += if(emailtest(str)){println(it is an email)}            + else if(iptest(str)){println(it is an ip address)}            + else{println(I don\\t know)};    engineput(str email@addresstony);    Compilable compilable = (Compilable) engine;    CompiledScript compiled = pile(script);    compiledeval();}

脚本编译的过程如下首先将 engine 转换为 Compilable 接口然后调用 Compilable 接口的 compile 方法得到一个 CompiledScript 的实例这个实例就代表一个编译过的脚本如此用 CompiledScript 的 eval 方法即为调用编译好的脚本了在我的 Think Pad 上这段代码编译后的调用大约比直接调用 engineeval 要快 随着脚本复杂性的提升性能的提升会更加明显

脚本上下文与绑定

真正将脚本语言与 Java 联系起来的不是 ScriptEngine而是 ScriptContext它作为 Java 与 ScriptEngine 之间的桥梁而存在

一个 ScriptEngine 会有一个相应的 ScriptContext它维护了一个 Map这个 Map 中的每个元素都是脚本语言对象与 Java 对象之间的映射同时这个 Map 在我们的 API 中又被称为 Bindings一个 Bindings 就是一个限定了 key 必须为 String 类型的 Map —— Map<String Object>所以一个 ScriptContext 也会有对应的一个 Bindings它可以通过 getBindings 和 setBindings 方法来获取和更改

一个 Bindings 包括了它的 ScriptContext 中的所有脚本变量那么如何获取脚本变量的值呢?当然从 Bindings 中 get 是一个办法同时 ScriptContext 也提供了 getAttribute 方法在只希望获得某一特定脚本变量值的时候它显然是十分有效的相应地 setAttribute 和 removeAttribute 可以增加修改或者删除一个特定变量

在 ScriptContext 中存储的所有变量也有自己的作用域它们可以是 ENGINE_SCOPE 或者是 GLOBAL_SCOPE前者表示这个 ScriptEngine 独有的变量后者则是所有 ScriptEngine 共有的变量例如我们执行 engineput(key value) 方法之后这时便会增加一个 ENGINE_SCOPE 的变量如果要定义一个 GLOBAL_SCOPE 变量可以通过 setAttribute(key value ScriptContextGLOBAL_SCOPE) 来完成

此外 ScriptContext 还提供了标准输入和输出的重定向功能它可以用于指定脚本语言的输入和输出

在 JavaScript 中使用 Java 高级特性

这一部分不同于前述内容将介绍 JavaScript引擎 —— Rhino 独有的特性

使用 Java 对象

前面的部分已经介绍过如何在 JavaScript 中使用一个已经实例化的 Java 对象那么如何在 JavaScript 中去实例化一个 Java 对象呢?在 Java 中所有 Class 是按照包名分层次存放的而在 JavaScript 没有这一结构Rhino 使用了一个巧妙的方法实现了对所有 Java 对象的引用Rhino 中定义了一个全局变量—— Packages并且它的所有元素也是全局变量这个全局变量维护了 Java 类的层次结构例如 PackagesjavaioFile 引用了 Java 的 io 包中 File 对象如此一来我们便可以在 JavaScript 中方便的使用 Java 对象了new 和 Packages 都是可以被省略的

//The same as: var frame = new PackagesjavaioFile(filename);var frame = javaioFile(filename);

我们也可以像 Java 代码中一样把这个对象引用进来

importClass (javaioFile);var file = File(filename);

如果要将整个包下的所有类都引用进来可以用 importPackage

importPackage(javaio);

如果只需要在特定代码段中引用某些包可以使用 JavaImporter 搭配 JavaScript 的 with 关键字

var MyImport = JavaImporter(javaioFile);with (MyImport) {    var myFile = File(filename);}

用户自定义的包也可以被引用进来不过这时候 Packages 引用不能被省略

importPackage(Packagestony);var hello = HelloWorld();hellosayHello();

注意这里只有 public 的成员和方法才会在 JavaScript 中可见例如对 hellos 的引用将得到 undefined下面简单介绍一些常用的特性

使用 Java 数组

需要用反射的方式构造

var a = javalangreflectArraynewInstance(javalangString );

对于大部分情况可以使用 JavaScript 的数组将一个 JavaScript 的数组作为参数传递给一个 Java 方法时 Rhino 会做自动转换将其转换为 Java 数组

实现一个 Java 接口

除了上面提到的 Invocable 接口的 getInterface 方法外我们也可以在脚本中用如下方式

//Define a JavaScript Object which has corresponding methodobj={max:function(ab){return (a > b) ?a:b;}}; //Pass this object to an InterfacemaxImpl=comtonyMaxMin(obj); //Invocationprint (maxImplmax());

如果接口只有一个方法需要实现那么在 JavaScript 中你可以传递一个函数作为参数

function func(){    println(Hello World);}t=javalangThread(func);tstart();

对于 JavaBean 的支持

Rhino 对于 JavaBean 的 get 和 is 方法将会自动匹配例如调用 hellostring如果不存在 string 这个变量Rhino 将会自动匹配这个实例的 isString 方法然后再去匹配 getString 方法若这两个方法均不存在才会返回 undefined

命令行工具 jrunscript

在 Mustang 的发行版本中还将包含一个脚本语言的的命令行工具它能够解释所有当前 JDK 支持的脚本语言同时它也是一个用来学习脚本语言很好的工具你可以l找到这一工具的详细介绍

结束语

脚本语言牺牲执行速度换来更高的生产率和灵活性随着计算机性能的不断提高硬件价格不断下降可以预见的脚本语言将获得更广泛的应用在 JavaSE 的下一个版本中加入了对脚本语言的支持无疑将使 Java 程序变得更加灵活也会使 Java 程序员的工作更加有效率

关于作者

吴玥颢目前就职于 IBM 中国开发中心 Harmony 开发团队 除了对 Java 和脚本语言的热爱之外他的兴趣还包括哲学神话历史与篮球此外他还是个电脑游戏高手您可以通过联系到他

               

上一篇:动态Proxy与Java ACL用户访问控制机制实现

下一篇:Java代码调用JavaFX的例子