在java中执行js代码

265 阅读2分钟

在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对象