java 中使用ScriptEngine执行javascript脚本代码和使用中应该注意的事项

我们都知道在使用elasticsearch的时候可以配置类似script_score这种执行一个脚本来改变文档得分,script_score可以指定lang参数,可选有groovy(默认值),javascript,native(通过java根据接口规则实现)。native的执行性能是最好的,就是要重启es服务。

脚本中可以使用一些提前传入的变量,入_score,doc等等可以获取目前的得分,或者获取原始文档的一些信息用来改变评分。总之就是它会执行你配置的这一段脚本代码。

有时候有一些多变的策略可能不适合写死在代码里,也通过类似脚本执行的方式来执行,可以使底层代码更稳定灵活与具体策略解耦,特地研究了一下java如何执行脚本语言。发现自JDK1.6开始,已经自带了一个ScriptEngine,可以用来执行如javascript何groovy脚本代码。

但是在实际场景中基本上都是在多线程环境下使用的,比如在servlet中执行一个脚本对推荐结果列表做二次转换后再返回给前端结果。

于是去搜索一些关于scriptEngine的使用最佳建议。发现ScriptEngine在多线程环境下使用是很讲究的,groovy脚本是线程安全的,javascript脚本不是线程安全的,我们可以通过执行一下代码可以查看你当前使用的jdk在支持的脚本的线程安全性:

import javax.script.ScriptEngineFactory;
import javax.script.ScriptEngineManager;

public class ScriptEngineTest {
  public static void main(String[] args) {
    final ScriptEngineManager mgr = new ScriptEngineManager();
    for(ScriptEngineFactory fac: mgr.getEngineFactories()) {
      System.out.println(String.format("%s (%s), %s (%s), %s", fac.getEngineName(),
          fac.getEngineVersion(), fac.getLanguageName(),
          fac.getLanguageVersion(), fac.getParameter("THREADING")));
    }
  }
}

官方也提供了一些在多线程环境下使用ScriptEngine的建议:https://blogs.oracle.com/nashorn/nashorn-multithreading-and-mt-safety

So, our agenda is two fold.  The first is to provide a "workers" library (timeline is not tied to JDK8) which uses an onevent model that JavaScripters are familiar with.  No synchronization/locking constructs to be added to the language.  Communication between threads (and potentially nodes/servers) is done using JSON (under the covers.) Going this route allows object isolation between threads, but also allows maximal use of CPU capacity.

The second part is, we will not guarantee MT-safe structures, but we have to face reality.  Developers will naturally be drawn to using threads.  Many of the Java APIs require use of threads.  So, the best we can do is provide guidelines on how to not shoot yourself in the foot.  These guidelines will evolve and we'll post them 'somewhere' after we think them through.  In the meantime,  I follow some basic rules;
  • Avoid sharing script objects or script arrays across threads (this includes global.)  Sharing script objects is asking for trouble.  Share only primitive data types, or Java objects.
  • If you want to pass objects or arrays across threads, use JSON.stringify(obj) andJSON.parse(string) to transport using strings.
  • If you really really feel you have to pass a script object, treat the object as a constant and only pass the object to new threads (coherency.)  Consider using Object.freeze(obj).
  • If you really really really feel you have to share a script object, make sure the object's properties are stabilized.  No adding or removing properties after sharing starts.  Consider using Object.seal(obj).
  • Given enough time, any other use of a shared script object will eventually cause your app to fail.
里面说到了几点值得注意的,就是不要共享脚本中的对象,可以共享原生数据类型或者java对象,比如你在script里面定义了一个var i=0,那么这个变量i是全局的变量,是所有线程共享的变量,当你在多线程情况下执行var i=0; i=i+1;时,每个线程得到的结果并不会都是1。当然如果你是在script的function中定义的变量,那么它不会被共享,例如你的script string是:function addone(){var i=0;i=i+1;return i;}那么多线程同时调用这个function时,返回的结果都是1。

这里要注意的一点是function中一定要用var 重新定义变量,否则还是全局的变量.

还有一点是如果你确实想共享一个对象,可以用JSON.stringfy和JSON.parse通过json序列化和反序列化来共享,这样其实内部是生成了java 的String对象,String对象都是新分配内存保存的,所以每个线程都持有的是不同的对象实例,改变互不影响。


sof上看了许多评论,都是说要尽量复用ScriptEngine,因为是将脚本语言编译成了java可执行的字节码来执行的,如果不复用的话,那么每次都要去编译脚本生成字节码,如果是一个固定的脚本的话,这样效率是很低的,https://stackoverflow.com/questions/27710407/reuse-nashorn-scriptengine-in-servlet这篇讨论里面也说到了如何去在servlet中复用ScriptEngine:

public class MyServlet extends HttpServlet {

  private ThreadLocal engineHolder;

  @Override
  public void init() throws ServletException {
    engineHolder = new ThreadLocal() {
      @Override
      protected ScriptEngine initialValue() {
        return new ScriptEngineManager().getEngineByName("nashorn");
      }
    };
  }

  @Override
  public void doGet(HttpServletRequest req, HttpServletResponse res) throws IOException {
    try (PrintWriter writer = res.getWriter()) {
      ScriptContext newContext = new SimpleScriptContext();
      newContext.setBindings(engineHolder.get().createBindings(), ScriptContext.ENGINE_SCOPE);
      Bindings engineScope = newContext.getBindings(ScriptContext.ENGINE_SCOPE);
      engineScope.put("writer", writer);
      Object value = engineHolder.get().eval("writer.print('Hello, World!');", engineScope);
      writer.close();
    } catch (IOException | ScriptException ex) {
      Logger.getLogger(MyServlet.class.getName()).log(Level.SEVERE, null, ex);
    }
  }
}

使用一个ThreadLocal来共享ScriptEngine,这样在后来的请求可以复用这个ScriptEngine实例。即每个线程都有自己的ScriptEngine实例。由于线程池的存在,线程可以被复用,从而ThreadLocal里的ScriptEngine也被复用了。


另外在给一个测试多线程使用的例子:

import jdk.nashorn.api.scripting.NashornScriptEngine;
import jdk.nashorn.api.scripting.ScriptObjectMirror;

import javax.script.ScriptEngineManager;
import javax.script.ScriptException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.concurrent.*;

public class JavaExecScriptMTDemo {

    public static void main(String[] args) throws Exception {

        ScriptEngineManager sem = new ScriptEngineManager();
        NashornScriptEngine engine = (NashornScriptEngine) sem.getEngineByName("javascript");
        String script = "function transform(arr){" +
                " var arr2=[]; for(var i=0;i addition = new Callable() {
            @Override
            public Collection call() {
                try {
                    ScriptObjectMirror mirror= (ScriptObjectMirror)engine.invokeFunction("transform", Arrays.asList(1, 2, 3));

                    return  mirror.values();
                } catch (ScriptException | NoSuchMethodException e) {
                    throw new RuntimeException(e);
                }
            }
        };

        ExecutorService executor = Executors.newCachedThreadPool();
        ArrayList> results = new ArrayList<>();

        for (int i = 0; i < 50; i++) {
            results.add(executor.submit(addition));
        }

        int miscalculations = 0;
        for (Future result : results) {
            Collection jsResult = result.get();
            System.out.println(jsResult);
//            if (jsResult != 2) {
//                System.out.println("Incorrect result from js, expected 1 + 1 = 2, but got " + jsResult);
//                miscalculations += 1;
//            }

        }

        executor.awaitTermination(1, TimeUnit.SECONDS);
        executor.shutdownNow();

//        System.out.println("Overall: " + miscalculations + " wrong values for 1 + 1.");
    }

脚本内容是一个函数,传入一个整型数组,然后将数组中的每个元素+1,返回一个新的数组。启动50个线程同时执行这个函数,会发现输出的结果都是[2,3,4]。说明这种方式的用法是线程安全的,因为我们没有共享脚本变量。

当我们改变一下脚本内容,改成这样:

 String script = "function transform(arr){" +
                " var arr2=[]; for( i=0;i

就是把for循环上的var i=0改成了i=0.然后再执行测试代码。会发现抛出数组越界异常。说明这种情况下多线程不安全了,因为这个i是一个共享脚本变量,每个脚本下都可见。


script包下最主要的是ScriptEngineManager、ScriptEngine、CompiledScript和Bindings 4个类或接口。

ScriptEngineManager是一个工厂的集合,可以通过name或tag的方式获取某个脚本的工厂并生成一个此脚本的ScriptEngine,目前只有javascript的工厂。通过工厂函数得到了ScriptEngine之后,就可以用这个对象来解析脚本字符串了,直接调用Object obj = ScriptEngine.eval(String script)即可,返回的obj为表达式的值,比如true、false或int值。

CompiledScript可以将ScriptEngine解析一段脚本的结果存起来,方便多次调用。只要将ScriptEngine用Compilable接口强制转换后,调用compile(String script)就返回了一个CompiledScript对象,要用的时候每次调用一下CompiledScript.eval()即可,一般适合用于js函数的使用。

Bindings的概念算稍微复杂点,我的理解Bindings是用来存放数据的容器。它有3个层级,为Global级、Engine级和Local级,前2者通过ScriptEngine.getBindings()获得,是唯一的对象,而Local Binding由ScriptEngine.createBindings()获得,很好理解,每次都产生一个新的。Global对应到工厂,Engine对应到ScriptEngine,向这2者里面加入任何数据或者编译后的脚本执行对象,在每一份新生成的Local Binding里面都会存在。

给个代码的例子,其中的functionScript可以从标准输入stdin或者配置文件等地方获得,这样就可以动态的控制Java代码的运行结果

1
2
3
4
5
6
7
8
9
10
11
try {
	ScriptEngine engine = new ScriptEngineManager().getEngineByName("javascript");
	Compilable compilable = (Compilable) engine;
	Bindings bindings = engine.createBindings(); //Local级别的Binding
	String script = "function add(op1,op2){return op1+op2} add(a, b)"; //定义函数并调用
	CompiledScript JSFunction = compilable.compile(script); //解析编译脚本函数
	bindings.put("a", 1);bindings.put("b", 2); //通过Bindings加入参数
	Object result = JSFunction.eval(bindings);
	System.out.println(result); //调用缓存着的脚本函数对象,Bindings作为参数容器传入
}
catch (ScriptException e) {}

另外还有一个ScriptContext的概念,这个可能很少用到吧,它是用来连接ScriptEngine和Bindings的工具。按照JDK的解释:该接口的实现类被用来连接ScriptEngine和宿主应用程序中的对象(如有范围的Bindings)。



你可能感兴趣的:(JAVA)