在java中执行js代码
需求:
解决自定义代码块的实现,前端会传递代码块和相关输入参数
使用jdk自带的nashorn引擎
nashorn引擎能够解决以上需求,但是不支持ES5语法,用户使用时需要熟悉相关的语法且伴随着jdk的迭代,在java11这个已经被废除,支持使用graalvm
以下是通过nashorn引擎通过导入的方法实现的
- 可以内置相关js函数供调用
单例模式
public class CtScriptEngine {
private CtScriptEngine() {
}
private static final EngineWrapper engineWrapper = new EngineWrapper();
private ScriptEngine scriptEngine;
public static CtScriptEngine getInstance() {
CtScriptEngine instance = engineWrapper.getInstance();
if (instance == null) {
synchronized (engineWrapper) {
if (instance == null) {
instance = new CtScriptEngine();
engineWrapper.setInstance(instance);
}
}
}
return instance;
}
private void initEngine() throws FileNotFoundException, ScriptException {
InputStream resourceAsStream = this.getClass().getClassLoader().getResourceAsStream("script.js");
if(resourceAsStream == null){
throw new ServiceException(FlowErrorEnum.SCRIPT_ENGINE_ERROR);
}
// String path = this.getClass().getClassLoader().getResource("script.js").getPath();
InputStreamReader reader = new InputStreamReader(resourceAsStream);
ScriptEngine engine = new ScriptEngineManager().getEngineByName("nashorn");
engine.eval(reader);
this.scriptEngine = engine;
}
public Invocable getEngine() {
if (scriptEngine != null && scriptEngine instanceof Invocable) {
return (Invocable) scriptEngine;
}
synchronized (engineWrapper) {
if (scriptEngine != null && scriptEngine instanceof Invocable) {
return (Invocable) scriptEngine;
}
try {
initEngine();
} catch (FileNotFoundException | ScriptException e) {
throw new ServiceException(CommonErrorEnum.INTERNAL_SERVER_ERROR_ARGS.getCode(), e.getMessage());
}
if (scriptEngine != null && scriptEngine instanceof Invocable) {
return (Invocable) scriptEngine;
}
}
throw new ServiceException(CommonErrorEnum.INTERNAL_SERVER_ERROR_ARGS.getCode(), "函数引擎未能获取");
}
private static class EngineWrapper {
private CtScriptEngine instance;
public CtScriptEngine getInstance() {
return instance;
}
public void setInstance(CtScriptEngine instance) {
this.instance = instance;
}
}
核心代码
Invocable invocable = CtScriptEngine.getInstance().getEngine();
String finalCode = code + "\n return output";
//不支持并发
synchronized (invocable){
Map<String, Object> jsOutData = (Map<String, Object>) invocable.invokeFunction("javascript", inputData, finalCode);
finalJsonStr = JSONObject.toJSONString(jsOutData);
}
使用graalvm
官方文档www.graalvm.org/latest/refe…
有两种方式实现
上下文
Context context = Context.newBuilder("js")
.allowHostAccess(HostAccess.ALL)
//allows access to all Java classes
.allowHostClassLookup(className -> true)
.build();
context.eval("js", jsSourceCode);
引擎
ScriptEngine eng = new ScriptEngineManager()
.getEngineByName("graal.js");
Object fn = eng.eval("(function() { return this; })");
Invocable inv = (Invocable) eng;
Object result = inv.invokeMethod(fn, "call", fn);
在使用中出现的问题,传入java的map对象到js函数中无法获取传入对象的值而nashorn支持
相关解决方案
- 传入对象改为json字符串
- 使用ProxyObject.fromMap将map对象转为ProxyObject对象
代码实现
public class JsRun {
private final String codeTemplate = "(function(input){\n" +
" console.log(input);\n" +
"{%s}\n"+
" return output;\n" +
"})";
public static void main(String[] args) {
JsRun jsRun = new JsRun();
try {
jsRun.method2();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public void method2() throws Exception {
//https://www.graalvm.org/latest/reference-manual/js/JavaInteroperability/#enabling-java-interoperability
//https://stackoverflow.com/questions/53831207/graalvm-java-complex-object-cant-be-parsed-like-javascript-object
GraalJSScriptEngine engine = (GraalJSScriptEngine)new ScriptEngineManager().getEngineByName("graal.js");
Map<String, Object> inputData = new HashMap<>();
inputData.put("a",1);
ProxyObject proxyObject = ProxyObject.fromMap(inputData);
String code = "output = {"hello": input.a };";
Object fn = engine.eval(String.format(codeTemplate, code));
Invocable inv = (Invocable) engine;
Object result = inv.invokeMethod(fn, "call", engine.getBindings(javax.script.ScriptContext.ENGINE_SCOPE), proxyObject);
}
}
inv.invokeMethod(fn, "call", engine.getBindings(javax.script.ScriptContext.ENGINE_SCOPE), proxyObject);
js中的call方法用来调用函数,需要传入两个参数(上下文参数,真正的输入参数),因此第一个参数可以任意传递,第二个则是我们转换后的map对象