我们都知道在使用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")));
}
}
}
这里要注意的一点是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);
}
}
}
另外在给一个测试多线程使用的例子:
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.");
}
当我们改变一下脚本内容,改成这样:
String script = "function transform(arr){" +
" var arr2=[]; for( 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 |
|
另外还有一个ScriptContext的概念,这个可能很少用到吧,它是用来连接ScriptEngine和Bindings的工具。按照JDK的解释:该接口的实现类被用来连接ScriptEngine和宿主应用程序中的对象(如有范围的Bindings)。