大数跨境
0
0

Javascript 执行神器 —— Mozilla Rhino

Javascript 执行神器 —— Mozilla Rhino 曼昂网络爬虫
2015-10-15
2
导读:Rhino 是一个纯 Java 的开源的 JavaScript 实现。他的名字来源于 O'Reilly 关于 JavaScript 的书

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

Mozilla Rhino

Rhino 是一个纯 Java 的开源的 JavaScript 实现。他的名字来源于 O'Reilly 关于 JavaScript 的书的封面:


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

Rhino 提供了如下功能

  • 对 JavaScript 1.5 的完全支持

  • 直接在 Java 中使用 JavaScript 的功能

  • 一个 JavaScript shell 用于运行 JavaScript 脚本

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

支持的脚本语言

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

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

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

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

// create engine by name
ScriptEngine engine = factory.getEngineByName ("JavaScript");
// create engine by name
ScriptEngine engine = factory.getEngineByExtension ("js");
// create engine by name
ScriptEngine engine = factory.getEngineByMimeType ("application/javascript");

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

ScriptEngineManager factory = new ScriptEngineManager();
for (ScriptEngineFactory available : factory.getEngineFactories()) {
    System.out.println(available.getEngineName());
}

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

在 Java 中解释脚本

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

public class RunJavaScript {
    public static void main(String[] args){
        ScriptEngineManager factory = new ScriptEngineManager();
        ScriptEngine engine = factory.getEngineByName ("JavaScript");
        engine.eval("print('Hello World')");
    }
}

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

engine.eval("if(true){println ('hello')");


故意没有加上”}”,执行这段代码 Java 将会抛出一个 javax.script.ScriptException 并准确的打印出错信息:

Exception in thread "main" javax.script.ScriptException: 
sun.org.mozilla.javascript.internal.EvaluatorException: 
missing } in compound statement (<Unknown source>#1) in <Unknown source> 
at line number 1
at ...

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

ScriptEngineManager factory = new ScriptEngineManager();
ScriptEngine engine = factory.getEngineByName ("JavaScript");
engine.eval(new Reader("HelloWorld.js"));


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

public class RunJavaScript {
    public static void main(String[] args) throws FileNotFoundException, 
            ScriptException, InterruptedException {
        ScriptEngineManager factory = new ScriptEngineManager();
        ScriptEngine engine = factory.getEngineByName ("JavaScript");
        while (true) {
            engine.eval(new FileReader("HelloWorld.js"));
            Thread.sleep(1000);
        }
    }
}

HelloWorld.js 内容为简单的打印一个 Hello World: print('Hello World');

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

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

脚本语言与 Java 的通信

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

package tony;

public class HelloWorld {
    String s = "Hello World";
    public void sayHello(){
        System.out.println(s);
    }
}

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

import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;

public class TestPut {
    public static void main(String[] args) throws ScriptException {
        ScriptEngineManager factory = new ScriptEngineManager();
        ScriptEngine engine = factory.getEngineByName("JavaScript");
        HelloWorld hello = new HelloWorld();
        engine.put("script_hello", hello);
        engine.eval("script_hello.sayHello()");
    }
}

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

package tony;

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

public class TestInv {
    public static void main(String[] args) throws ScriptException,
            NoSuchMethodException {
        ScriptEngineManager factory = new ScriptEngineManager();
        ScriptEngine engine = factory.getEngineByName("JavaScript");
        String script = "function say(first,second) { print(first +' '+ second); }";
        engine.eval(script);
        Invocable inv = (Invocable) engine;
        inv.invokeFunction("say", "Hello", "Tony");
    }
}

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

package tony;

import javax.script.Invocable;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;
import junit.framework.TestCase;

public class TestScripting extends TestCase {
    
    public void testInv() throws ScriptException, NoSuchMethodException {
        ScriptEngineManager factory = new ScriptEngineManager();
        ScriptEngine engine = factory.getEngineByName("JavaScript");
        String script = "function max(first,second) "
      + "{ return (first > second) ?first:second;}";
        engine.eval(script);
        Invocable inv = (Invocable) engine;
        Object obj = inv.invokeFunction("max", "1", "0");
        assertEquals("1", obj.toString());
 }
}

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 = factory.getEngineByName("JavaScript");
    String script = "function max(first,second) "
      + "{ return (first > second) ?first:second;}";
    script += "function min(first,second) { return (first < second) ?first:second;}";
    engine.eval(script);
    Invocable inv = (Invocable) engine;
    MaxMin maxMin = inv.getInterface(MaxMin.class);
    assertEquals(1, maxMin.max(1, 0));
    assertEquals(0, maxMin.min(1, 0));
}

脚本的编译执行

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

public void testComplie() throws ScriptException {
    ScriptEngineManager manager = new ScriptEngineManager();
    ScriptEngine engine = manager.getEngineByName("JavaScript");
    String script = "var email=/^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]" 
        + "+(\\.[a-zA-Z0-9_-]+)+$/;";
    script += "var ip = /^(\\d{1,2}|1\\d\\d|2[0-4]\\d|25[0-5])"
        +"(\\.(\\d{1,2}|1\\d\\d|2[0-4]\\d|25[0-5])){3}$/;";
    script += "if(email.test(str)){println('it is an email')}"
            + "else if(ip.test(str)){println('it is an ip address')}"
            + "else{println('I don\\'t know')}";
    engine.put("str", "email@address.tony");
    Compilable compilable = (Compilable) engine;
    CompiledScript compiled = compilable.compile(script);
    compiled.eval();
}

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

脚本上下文与绑定

真正将脚本语言与 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 共有的变量。例如我们执行 engine.put(key, value) 方法之后,这时便会增加一个 ENGINE_SCOPE 的变量,如果要定义一个 GLOBAL_SCOPE 变量,可以通过 setAttribute(key, value, ScriptContext.GLOBAL_SCOPE)来完成。

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


【声明】内容源于网络
0
0
曼昂网络爬虫
我们是程序员开发者联盟,利用业余时间提供网络爬虫软件定制、微站、H5、网站等各类软件开发服务,有意者请留言!
内容 96
粉丝 0
曼昂网络爬虫 我们是程序员开发者联盟,利用业余时间提供网络爬虫软件定制、微站、H5、网站等各类软件开发服务,有意者请留言!
总阅读103
粉丝0
内容96