0%

java-脚本引擎

脚本引擎

简介

脚本引擎就是一个计算机编程语言的解释器

JAVA脚本引擎是从JDK6.0之后添加的新功能。

脚本引擎介绍:

  • 使得Java应用程序可以通过一套固定的接口与各种脚本引擎交互,从而达到在Java平台上调用各种脚本语言的目的。
  • Java脚本API是连通Java平台和脚本语言的桥梁。
  • 可以把一些复杂异变的业务逻辑交给脚本语言处理,这又大大提高了开发效率

使用场景

在日常的Java项目中,我们免不了会遇到这样的需求:

  1. 动态地获取并运行自定义脚本文件,以实现特定的功能
  2. 对数据流执行用户自定义的数据有效性、公式计算、数据处理ETL(如数据截取、拼接)等不同业务逻辑
  3. 对用户输入的代码或脚本文件进行测试、运行,确保其语法正确、功能正常
  4. 处理需要灵活配置且不断变更的动态业务规则
  5. 代码的热更新、热修复

诸如此类的需求若采用硬编码实现,则迭代成本相当高,每次改动都需要进行开发、测试、部署。同时业务规则的频繁变更会导致代码的开发和维护成本大大提高

js语言简介

js属于的是解释性语言。

  • 支持动态类型,弱类型,在程序运行的时候才进行编译,效率不较低。

  • 不像编译性语言,源代码不能直接翻译成机器语言,先翻译成中间代码,再由解释器对中间代码进行解释运行。

  • 程序不需要编译,程序运行时才翻译成机器语言,每执行一次都要翻译一次。

  • 一般,编译性语言的运行效率比解释性语言更高;但是不能一概而论,部分解释性语言的解释器通过在运行时动态优化代码,甚至能使解释性语言的性能超过编译性语言;

简单的实现过程

查找脚本引擎

  1. 通过脚本名称获取:

    ScriptEngineManager:为ScriptEngine提供实例化机制。

      方法:getEngineByName(String shortName);//查找并创建指定名称的脚本引擎

    1
    ScriptEngine engine = new ScriptEngineManager().getEngineByName("JavaScript"); 
  2. 通过文件扩展名获取:

    ScriptEngine:是一个接口,该接口提供了基本的脚本功能(包括执行脚本,设置和获取值的方法)。

    1
    2
    3
    4
    5
    6
    7
    8
    ScriptEngine engine = new ScriptEngineManager().getEngineByExtension("js");  

    方法:
    get(String key);//获取指定key所对应的值,这里的key看做变量名称,值看做变量名所对应的值。

         eval(String script);//执行指定的脚本代码

         eval(Reader reader);//执行指定的脚本文件
  3. 通过MIME类型获取:

    1
    ScriptEngine engine = new ScriptEngineManager().getEngineByMimeType("text/javascript");  

语言绑定

脚本语言支持API使用语言绑定对象实现Java语言编写的程序与脚本语言间的数据传递。语言绑定对象实际上就是一个简单的哈希表,用来存放和获取需要共享的数据,其定义的接口为javax.script.Bindings,继承自java.util.Map接口。一个脚本引擎在执行过程中可能会使用多个语言绑定对象,不同语言绑定对象的作用域不同。ScriptEngine类提供out和get方法对脚本引擎中特定作用域的默认语言绑定对象进行操作。

使用默认的语言绑定对象:

1
2
3
4
5
6
7
8
public void useDefaultBinding() throws ScriptException {  
ScriptEngine engine = getJavaScriptEngine();
engine.put("name", "Alex");
engine.eval("var message = 'Hello, ' + name;"); //执行指定的脚本代码
engine.eval("println(message);");
Object obj = engine.get("message");
System.out.println(obj);
}

亦可以自定义语言绑定对象(如语言绑定对象中包含程序自己独有的数据等情形……):

1
2
3
4
5
6
public void useCustomBinding() throws ScriptException {  
ScriptEngine engine = getJavaScriptEngine();
Bindings bindings = new SimpleBindings();
bindings.put("hobby", "playing games");
engine.eval("println('I like ' + hobby);", bindings);
}

脚本引擎执行脚本方法

一、

  1. 执行脚本方法,首先肯定是要在脚本中定义方法,然后执行。
  2. 执行时需要将engine转换为Invcable,然后调用其中的invokeFun

  Invocable:由ScriptEngines实现的可选接口,其方法允许调用先前已执行的脚本中的程序(方法)。

  1. 调用Object invokeFunction(String name, Object… args);执行指定方法。

  name为方法名称,args为方法参数,返回是一个Object对象 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import javax.script.Invocable;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;


public class TestRhino {
public static void main(String[] args) throws ScriptException {
ScriptEngineManager sem = new ScriptEngineManager();
ScriptEngine engine = sem.getEngineByName("javascript");
//定义方法的字符串形式
String funAdd = "function add(a,b){"
+ " var sum = a + b;"
+ " return sum;"
+ " }";
engine.eval(funAdd);//这一步可以看做将方法写入脚本
Invocable invo = (Invocable)engine;//转换成Invocable
Object result = null;
try {
//执行脚本中方法
result = invo.invokeFunction("add",new Object[]{17,23});
} catch (NoSuchMethodException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(result);
}
}

二、

  1. 执行脚本文件,首先我们要指定文件的路径。
  2. 通过这个路径构建一个Reader对象
  3. 调用eval(Reader reader);//执行指定的脚本文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.Reader;

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


public class TestRhino {
public static void main(String[] args) throws ScriptException {
//E:\eclipse\Rhino
ScriptEngineManager sem = new ScriptEngineManager();
ScriptEngine engine = sem.getEngineByName("javascript");
//脚本文件路径
String path = "E:\\eclipse\\Rhino\\test.js";//
Reader read = null;
try {
//通过脚本文件路径构造Reader对象
read = new FileReader(new File(path));
} catch (FileNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
//执行脚本文件
engine.eval(read);
}
}

脚本执行的上下文

脚本引擎通过执行过程中的上下文对象获取与脚本执行相关的信息,同时允许程序员通过此对象配置脚本引擎的行为。其上下文对象来自javax.script.ScriptContext接口,类似于J2EE中javax.servlet.ServletContext接口,该接口主要包含3类信息:

输入输出

默认情况下,脚本输入输出都是在标准控制台中,可以通过setReader和setWriter方法对输出流进行重定向,可以通过setErrorWriter方法进行错误输出重定向。

1
2
3
4
5
6
7
//例:将输出重定向到文件  
public void scriptToFile() throws IOException, ScriptException {
ScriptEngine engine = getJavaScriptEngine();
ScriptContext context = engine.getContext();
context.setWriter(new FileWriter("output.txt"));
engine.eval("println('Hello World!');");
}

自定义属性

上下文中通过setAttribute和getAttribute方法获取和设置属性,类似于ServletContext中设置和获取属性操作。与ServletContext中不同的是,ScriptContext中的属性是有作用域之分的,ScriptContext按不同的顺序在不同的作用域中进行属性查找(类似于JSP中EL表达式属性的作用域)。通过ScriptContext的getScopes可以得到其中所有可用的作用域,其中预定义了两个作用域:常量ScriptContext.ENGINE_SCOPE(当前的脚本引擎)和ScriptContext.GLOBAL_SCOPE(从同一引擎工厂中创建的所有脚本引擎对象)。

1
2
3
4
5
6
7
public void scriptContextAttribute() {  
ScriptEngine engine = getJavaScriptEngine();
ScriptContext context = engine.getContext();
context.setAttribute("name", "Alex", ScriptContext.GLOBAL_SCOPE);
context.setAttribute("name", "Bob", ScriptContext.ENGINE_SCOPE);
context.getAttribute("name"); //值为Bob
}

语言绑定对象

语言绑定对象位于ScriptContext中,同样也有作用域之分,范围越小,优先级越高。执行如下代码,输出的name值为Bob。

1
2
3
4
5
6
7
8
9
10
11
public void scriptContextBindings() throws ScriptException {  
ScriptEngine engine = getJavaScriptEngine();
ScriptContext context = engine.getContext();
Bindings bindings1 = engine.createBindings();
bindings1.put("name", "Alex");
context.setBindings(bindings1, ScriptContext.GLOBAL_SCOPE);
Bindings bindings2 = engine.createBindings();
bindings2.put("name", "Bob");
context.setBindings(bindings2, ScriptContext.ENGINE_SCOPE);
engine.eval("println(name);"); //Bob
}

也可以通过ScriptContext获取语言绑定对象:

1
2
3
4
5
6
7
public void useScriptContextValues() throws ScriptException {  
ScriptEngine engine = getJavaScriptEngine();
ScriptContext context = engine.getContext();
Bindings bindings = context.getBindings(ScriptContext.ENGINE_SCOPE);
bindings.put("name", "Alex");
engine.eval("println(name);");
}

前面说到语言绑定对象存在于上下文环境中,故context中保存的自定义属性其实也是保存于语言绑定对象中的,如2中的语言绑定。

1
2
3
4
5
6
public void attributeInBindings() throws ScriptException {  
ScriptEngine engine = getJavaScriptEngine();
ScriptContext context = engine.getContext();
context.setAttribute("name", "Alex", ScriptContext.GLOBAL_SCOPE);
engine.eval("println(name);");
}

脚本编译

脚本语言一般均是解释执行的,相对于编译执行的语言,效率较低一些。当脚本语言需要多次重复执行时,可以先对煎熬本进行编译,避免重复解析,提高效率(注:脚本编译需要脚本引擎支持,实现javax.script.Compilable接口)。JavaSE中自带的JavaScript引擎是支持对脚本进行编译的,编译的脚本用javax.script.CompiledScript来表示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class ScriptCompile extends JsScriptRunner {  
//对脚本进行编译
public CompiledScript compile(String scriptText) throws ScriptException {
ScriptEngine engine = getJavaScriptEngine();
if (engine instanceof Compilable) {
CompiledScript script = ((Compilable) engine).compile(scriptText);
return script;
}
return null;
}

//先编译再执行
public void run(String scriptText) throws ScriptException {
CompiledScript script = compile(scriptText);
if (script == null) {
return;
}
for (int i = 0; i < 100; i++) {
script.eval();
}
}

public static void main(String[] args) {
ScriptCompile sc = new ScriptCompile();
try {
sc.run("println('Hello');");
} catch (ScriptException ex) {
Logger.getLogger(ScriptCompile.class.getName()).log(Level.SEVERE, null, ex);
}
}
}

方法调用

Java虚拟机支持脚本的意义在于实现函数式的编程,即脚本中最重要的便是方法。一些脚本引擎允许使用者单独调用脚本中的某个方法,支持此操作的脚本引擎可以通过实现javax.script.Invocable接口,支持顶层方法或者某对象中成员方法的调用。使用方法调用时最好先检查脚本引擎是否实现了Invocable接口,JavaSE中的JavaScript引擎已实现了Invocable接口。

  1. 在Java中调用脚本中的顶层方法

    1
    2
    3
    4
    5
    6
    7
    public void invokeFunction() throws ScriptException, NoSuchMethodException {  
    ScriptEngine engine = getJavaScriptEngine();
    String scriptText = "function greet(name) { println('Hello, ' + name); } ";
    engine.eval(scriptText);
    Invocable invocable = (Invocable) engine;
    invocable.invokeFunction("greet", "Alex");
    }
  2. 调用脚本中某对象的成员方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    public void invokeMethod() throws ScriptException, NoSuchMethodException {  
    ScriptEngine engine = getJavaScriptEngine();
    String scriptText = "var obj = { getGreeting : function(name) { return 'Hello, ' + name; } }; ";
    engine.eval(scriptText);
    Invocable invocable = (Invocable) engine;
    Object scope = engine.get("obj");
    Object result = invocable.invokeMethod(scope, "getGreeting", "Alex"); //第一个参数为方法所属对象
    System.out.println(result);
    }
  3. 指定脚本中的方法为Java接口的实现

    Greet是Java实现的接口,包含一个方法getGreeting,通过Invocable.getInterface()方法指定脚本中的方法为Java接口的实现。

    1
    2
    3
    4
    5
    6
    7
    8
    public void useInterface() throws ScriptException {  
    ScriptEngine engine = getJavaScriptEngine();
    String scriptText = "function getGreeting(name) { return 'Hello, ' + name; } ";
    engine.eval(scriptText);
    Invocable invocable = (Invocable) engine;
    Greet greet = invocable.getInterface(Greet.class);
    System.out.println(greet.getGreeting("Alex"));
    }

Nashorn JavaScript脚本引擎