君子博学而日参省乎己 则知明而行无过矣

博客园 首页 新随笔 联系 订阅 管理

JDK1.6开始,Java引入了jsr223,就是可以用一致的形式在JVM上执行一些脚本语言,如js。先来一个简单的例子以对在jvm上运行脚本有个初步的认识。

import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
public class TestScript1 {
    public static void main(String[] args) throws Exception {
        ScriptEngineManager scriptEngineManager = new ScriptEngineManager();
        ScriptEngine scriptEngine = scriptEngineManager.getEngineByName("javascript");
        String exp = "2*6-(6+5)";
        Object result = scriptEngine.eval(exp);
        System.out.println(exp + "=" + result);
    }
}


上面的例子是用来计算2*6-(6+5)这个表达式的值,使用了js引擎来计算,非常方便。另外,在工作流中,通常使用脚本引擎来计算一个节点到另一个节点之间的条件是否通过。如果不用脚本/表达式引擎,那就需要自己解析脚本了,若是算术算式,通常是构造成一个逆波兰式,这不是本文重点,就不展开了。

上面计算的表达式是“常量”,还可以计算带有变量的,如下:

import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
public class TestScript3 {
    public static void main(String[] args) throws Exception {
        ScriptEngineManager scriptEngineManager = new ScriptEngineManager();
        ScriptEngine scriptEngine = scriptEngineManager.getEngineByName("javascript");
        String exp = "a+b";
        scriptEngine.put("a", 4);
        scriptEngine.put("b", 15);
        Object result = scriptEngine.eval(exp);
        System.out.println(exp + "=" + result);
    }
}

那么,一种JVM究竟支持哪些脚本引擎呢?可以通过下面的代码枚举出来。

import java.util.List;
import javax.script.ScriptEngineFactory;
import javax.script.ScriptEngineManager;
public class TestScript2 {
    public static void main(String[] args) throws Exception {
        ScriptEngineManager scriptEngineManager = new ScriptEngineManager();
        List<ScriptEngineFactory> engineFactories = scriptEngineManager.getEngineFactories();
        if(engineFactories.size() == 0) {
            System.out.println("本JVM尚不支持任何脚本引擎");
            return;
        }
         
        System.out.println("本JVM支持的脚本引擎有:");
        for(ScriptEngineFactory engineFactory : engineFactories) {
            System.out.println("引擎名称:" + engineFactory.getEngineName());
            System.out.println("\t可被ScriptEngineManager识别的名称:" + engineFactory.getNames());
            System.out.println("\t该引擎支持的脚本语言名称:" + engineFactory.getLanguageName());
            System.out.println("\t是否线程安全:" + engineFactory.getParameter("THREADING"));
        }
    }
}

在我的环境中,运行结果如下:

本JVM支持的脚本引擎有:
引擎名称:JEXL Engine
	可被ScriptEngineManager识别的名称:[JEXL, Jexl, jexl]
	该引擎支持的脚本语言名称:JEXL
	是否线程安全:null
引擎名称:Mozilla Rhino
	可被ScriptEngineManager识别的名称:
	该引擎支持的脚本语言名称:ECMAScript
	是否线程安全:MULTITHREADED

各位看官的运行结果未必与我的相同,“是否线程安全”一项的解释在后文有说明。

至此,已经对脚本引擎有了个初步的认识,那么jsr223究竟是个什么东西?说通俗了,就是为各脚本引擎提供了统一的接口、统一的访问模式。在jsr223出现以前,如jdk1.4中,就已经可以直接调用js引擎(可从http://www.mozilla.org/rhino/下载)来运行js脚本了,只不过调用的时候是与js引擎的实现息息相关的,jsr223的引入,调用者与具体脚本引擎解耦了,调用类里,不再需要引入具体引擎相关的类。如果有两个脚本引擎所支持的语言语法相似,还可以直接更换scriptEngineManager.getEngineByName(“javascript”)中的引擎名称,以方便的切换引擎。

在上面TestScript2中,我的环境中运行后列出了一个JS,一个JEXL(可从http://commons.apache.org/jexl/下载),JS引擎是JDK1.6自带的,JVM认识它是可以理解的,但JEXL只是从JEXL的官网上下载了一个JEXL这个引擎的jar包,放到了运行TestScript2的classpath中,其它什么也没做,jvm就自动识别到了它,却是为何?其实在ScriptEngineFactory类的API中有一句简单的描述:

ScriptEngineManager 使用 Jar 文件规范 中描述的服务提供者机制来获取所有当前 
ClassLoader 中可用的 ScriptEngineFactory 实例

这到底是个什么含义?那就来看下scriptEngineManager这个类的源码吧。很容易地,发现了这么一个方法(initEngines,在new ScriptEngineManager的时候被调用):

private void initEngines(final ClassLoader loader) {
    Iterator itr = null;
    try {
        if (loader != null) {
            itr = Service.providers(ScriptEngineFactory.class, loader);
        } else {
            itr = Service.installedProviders(ScriptEngineFactory.class);
        }
    } catch (ServiceConfigurationError err) {
        System.err.println("Can't find ScriptEngineFactory providers: " +
                      err.getMessage());
        if (DEBUG) {
            err.printStackTrace();
        }
        return;
    }
 
    try {
        while (itr.hasNext()) {
            try {
                ScriptEngineFactory fact = (ScriptEngineFactory) itr.next();
                engineSpis.add(fact);
            } catch (ServiceConfigurationError err) {
                System.err.println("ScriptEngineManager providers.next(): "
                             + err.getMessage());
                if (DEBUG) {
                    err.printStackTrace();
                }
                continue;
            }
        }
    } catch (ServiceConfigurationError err) {
        System.err.println("ScriptEngineManager providers.hasNext(): "
                        + err.getMessage());
        if (DEBUG) {
            err.printStackTrace();
        }
        return;
    }
}

发现有这么一个调用sun.misc.Service.providers(ScriptEngineFactory.class, loader)和sun.misc.Service.installedProviders(ScriptEngineFactory.class),还有BSF实现(后面要讲)中使用的ServiceRegistry#lookupProviders(Class providerClass)和ServiceRegistry#lookupProviders(Class providerClass, ClassLoader loader)方式,它们做了什么?

可以解压JEXL的jar包,就会发现里面META-INF目录下有一个名为 services的子目录,在services子目录里有个文本文件,文件名就是上面方法中ScriptEngineFactory.class参数所表示类的全限定名,打开这个文本文件就会发现一个类名(若有多个可以分多行)。原来,上面的四个方法都会去扫描classpath中jar包里的META-INF/services下的特定名称(与Class参数所表示的类的全限定名一致)的文件,读取里面的类名,并创建一个这个类的对象加到返回值中。(jdbc4将不用使用Class.forName加载驱动,那么是不是用的这种方式加载classpath中的驱动呢?各位看官自己验证吧。如果对这个机制感兴趣,可搜索关键字“Java Service Provider Interface”以了解更多,此内容超出本文内容,不作详述)。

总结一下,要想让JVM识别某个脚本引擎,有两种方式
1、通过ScriptEngineManager#registerEngineXXX方法进行注册;
2、提供一个jar包,该jar包的META-INF/services下有个javax.script.ScriptEngineFactory文件,文件的内容是0个或多个特定脚本引擎的ScriptEngineFactory的全限定名。

说了这么多,都是在jdk1.6环境下运行的,那么在jdk1.4,1.5下如何也像jdk1.6一样使用通用的方式调用脚本引擎呢?
前面已经说到,这个通用接口是jsr223提供的,那么就可以直接去http://jcp.org中搜索223,就会发现提供了参考实现(Reference Implementation)的下载。里面有一个script-api.jar,这就是那些通用接口了;然后有一个js.jar,这个就是开源项目Rhino的js脚本引擎实现;解压js.jar并没有发现META-INF/services/javax.script.ScriptEngineFactory文件,要让JVM感知到js引擎的存在,要么就通过ScriptEngineManager#registerEngineXXX,要么就再提供一个jar包,里面指定js引擎的ScriptEngineFactory,这就是script-js.jar的作用了。这里,也发现js.jar中并没有自己提供ScriptEngineFactory的实现,而是在script-js.jar中实现的。

很多时候,参考实现(Reference Implementation)只是提供规范所描述的功能,而并不过多的考虑性能。那么,就有一些开源的或商用的实现专注于性能了。这个jsr223,只是提供一个通用接口,并不提供具体脚本引擎的实现,所以,一般说来不会有性能问题。不过,依然可以看到一个实现了该规范的开源项目,那就是BSF(Bean Scripting Framework),BSF的历史官网上都有,它从3.x版本开始实现jsr223。值得一提的是,其3.0版本提供了一系列脚本引擎的ScriptEngineFactory实现,包括

bsh.engine.BshScriptEngineFactory
com.sun.script.freemarker.FreeMarkerScriptEngineFactory
com.sun.script.groovy.GroovyScriptEngineFactory
com.sun.script.jacl.JaclScriptEngineFactory
com.sun.script.jaskell.JaskellScriptEngineFactory
com.sun.script.java.JavaScriptEngineFactory
com.sun.phobos.script.javascript.RhinoScriptEngineFactory
com.sun.phobos.script.javascript.EmbeddedRhinoScriptEngineFactory
com.sun.script.jawk.JawkScriptEngineFactory
com.sun.script.jelly.JellyScriptEngineFactory
com.sun.script.jep.JepScriptEngineFactory
com.sun.script.jexl.JexlScriptEngineFactory
com.sun.script.jruby.JRubyScriptEngineFactory
com.sun.script.judo.JudoScriptEngineFactory
com.sun.script.juel.JuelScriptEngineFactory
com.sun.script.jython.JythonScriptEngineFactory
com.sun.script.ognl.OgnlScriptEngineFactory
org.pnuts.scriptapi.PnutsScriptEngineFactory
com.sun.script.scheme.SchemeScriptEngineFactory
com.sun.script.velocity.VelocityScriptEngineFactory
com.sun.script.xpath.XPathScriptEngineFactory
com.sun.script.xslt.XSLTScriptEngineFactory

而在后续版本中,则不再提供了,需要到https://scripting.dev.java.net/下载。至于为什么后续版本不再提供,官网上是这么说的:

Version 3.0 was shipped with a set of engine factories, 
however this is no longer present in later versions of BSF.
This is because many languages are now provided with their own factories. 
Also, having all the factories in a single jar can cause problems at run-time. 
If other jars contain factories that implement a different version of the same 
language it may be difficult or impossible to choose which version is loaded. 

如果在classpath中加入了脚本引擎,bsf-api的jar包,却发现通过scriptEngineManager.getEngineByName调用返回了null,那么要确定两件事情:1、名称是否正确;2、classpath中是否有jar 包含并指定了该脚本引擎的ScriptEngineFactory的实现类。

其实,打开JSR223的规范文档,会发现该规范本身就是建立在BSF这样的开源项目之上,规范文档中有如下描述:

The specification builds on prior work in the the following open-source 
projects:
·    Bean Scripting Framework (http://jakarta.apache.org/bsf/index.html)
·    BeanShell (http://www.beanshell.org)
·    PHP (http://www.php.net)

在规范给出通用接口之后,BSF又实现了该规范,除此之外,BSF还有很多功能,这超出本文范围,详情可访问其官网。

如果脚本引擎实现了javax.script.Compilable接口,还可以将ScriptEngine强制转换成Compilable的引用,将脚本编译成无需重新编译就能反复执行的某种形式以提高效率,如js引擎就可以将js脚本编译成java字节码。

import java.util.Random;
import javax.script.Bindings;
import javax.script.Compilable;
import javax.script.CompiledScript;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.SimpleBindings;
public class TestScript4 {
    public static void main(String[] args) throws Exception {
        String exp = "a.nextInt(10)+b";
        ScriptEngineManager scriptEngineManager = new ScriptEngineManager();
        ScriptEngine scriptEngine = scriptEngineManager.getEngineByName("javascript");
        if(scriptEngine instanceof Compilable) {
            CompiledScript compiledScript = ((Compilable)scriptEngine).compile(exp);
            Bindings bindings = new SimpleBindings();
            bindings.put("a", new Random());
            bindings.put("b", 5);
            Object result = compiledScript.eval(bindings);
            System.out.println(exp + "=" + result);
             
            bindings = new SimpleBindings();
            bindings.put("a", new Random());
            bindings.put("b", 5);
            result = compiledScript.eval(bindings);
            System.out.println(exp + "=" + result);
        } else {
            scriptEngine.put("a", new Random());
            scriptEngine.put("b", 15);
            Object result = scriptEngine.eval(exp);
            System.out.println(exp + "=" + result);
        }
    }
}

关于ScriptEngine是否是线程安全的,该类的API文档上并没有直接说明,但ScriptEngine是有状态的,某些eval的调用,可能会改变它的状态。在ScriptEngineFactory中有一个getParameter方法,通过传入”THREADING”字符串作为参数,可以获知引擎是否是线程安全的,这也说明ScriptEngine并不是全部非线程安全或全部是线程安全的,到底是否是线程安全的,依赖于具体脚本引擎的实现。engineFactory.getParameter(“THREADING”)的返回值及其含义可能有以下几种:

1、null - 引擎实现不是线程安全的,并且无法用来在多个线程上并发执行脚本。

2、"MULTITHREADED" - 引擎实现是内部线程安全的,并且脚本可以并发执行,
	尽管在某个线程上执行脚本的效果对于另一个线程上的脚本是可见的。 

3、"THREAD-ISOLATED" - 该实现满足 "MULTITHREADED" 的要求,
	并且引擎为不同线程上执行的脚本中的符号维护独立的值。 

4、"STATELESS" - 该实现满足 "THREAD-ISOLATED" 的要求。
	此外,脚本执行不改变 Bindings 中的映射关系,该 Bindings 是 ScriptEngine 的引擎范围。
	具体来说,Bindings 及其关联值中的键在执行脚本之前和之后是相同的。

结合前文TestScript2的运行结果,可以发现JEXL不是线程安全的,Rhino是内部线程安全的。

eval方法有好几种重载的形式,主要区别就是传入变量值的方式不同,变量可以设置不同的作用域,如该引擎一直可用、当次调用可见、同一ScriptEngineFactory创建的不同ScriptEngine均可见等,详细信息参考API文档。

在结束本文之前说说个人的使用经验。js引擎和JEXL本人都在生产环境使用过,js引擎可以执行js脚本,也就是可以有多个语句组成,可以在里面调用java方法等等,但是它有个缺点,即使将js脚本编译成字节码,执行一个脚本原本只需几毫秒或是更少,但在高并发的情况下,这个耗时一下子就会猛增到两三秒甚至更长,给人的感觉是js引擎中的某个资源成了瓶颈(具体原因未分析),严重的影响了效率,在我的这个场景中因为js执行的是单条语句,而其与JEXL的语法又很类似,后来就无缝的切换到JEXL引擎,并发性非常好。

JDK1.6,1.7中的js引擎都是Rhino,按Oracle说法,会在JDK1.8中提供一个效率更高的js引擎Nashorn,但到目前为止,还没有看到它的影子;JDK1.7开始在字节码层面(引入invokedynamic指令)对在JVM上运行脚本提供了更强的支持。

最后,本文只是简单的介绍了jsr223以及一些简单的原理,相关接口的具体使用,请参照javax.script包中各类的API文档;关于各脚本引擎的语法参见各官方网站;要详细了解这个脚本框架,可以参考书籍《Scripting in Java: Languages, Frameworks, and Patterns》(中文版由翟育明、俞黎敏等翻译,机械工业出版社出版《Java脚本编程 语言、框架与模式》)。

posted on 2013-07-26 01:58  刺猬的温驯  阅读(5018)  评论(0)    收藏  举报