捕获Nashorn的全局变量

时间:2016-03-04 23:05:06

标签: javascript java rhino nashorn

我有一个Java 7程序,它加载了数千个对象(组件),每个对象都有许多参数(存储在Map中),并在这些对象上执行各种Rhino脚本来计算存储的其他派生参数回到对象的Map。在运行每个脚本之前,会创建一个Scope对象,该对象由对象的地图支持,该对象在脚本的持续时间内用作JavaScript的范围。

作为一个简单示例,以下内容创建了一个HashMap,其中a = 10且b = 20,并执行脚本c = a + b,这会导致c = 30.0存储在地图中。虽然脚本看起来像是在创建全局变量c,但Scope对象会捕获它并将其存储在地图中;使用不同Scope对象执行的另一个脚本不会看到此变量:

public class Rhino {

    public static void main(String[] args) throws ScriptException {
        Context cx = Context.enter();
        Scriptable root_scope = cx.initStandardObjects();

        Map<String, Object> map = new HashMap<>();
        map.put("a", 10);
        map.put("b", 20);

        Scope scope = new Scope(root_scope, map);
        cx.evaluateString(scope, "c = a + b", "<expr>", 0, null);
        System.out.println(map); // --> {b=20, c=30.0, a=10}

        Context.exit();
    }

    static class Scope extends ScriptableObject {

        private Map<String, Object> map;

        public Scope(Scriptable parent, Map<String, Object> map) {
            setParentScope(parent);
            this.map = map;
        }

        @Override
        public boolean has(String key, Scriptable start) {
            return true;
        }

        @Override
        public Object get(String key, Scriptable start) {
            if (map.containsKey(key))
                return map.get(key);
            return Scriptable.NOT_FOUND;
        }

        @Override
        public void put(String key, Scriptable start, Object value) {
            map.put(key, value);
        }

        @Override
        public String getClassName() {
            return "MapScope";
        }
    }
}

以上脚本输出{b=20, c=30.0, a=10},显示变量c已存储在Map

现在,我需要将其迁移到Java 8,并使用Nashorn。但是,我发现Nashorn总是将全局变量存储在特殊的"nashorn.global"对象中。实际上,它似乎将所有绑定视为只读,并且尝试更改现有变量会导致新的全局变量遮蔽现有绑定。

public class Nashorn {

    private final static ScriptEngineManager MANAGER = new ScriptEngineManager();

    public static void main(String[] args) throws ScriptException {
        new Nashorn().testBindingsAsArgument();
        new Nashorn().testScopeBindings("ENGINE_SCOPE", ScriptContext.ENGINE_SCOPE);
        new Nashorn().testScopeBindings("GLOBAL_SCOPE", ScriptContext.GLOBAL_SCOPE);
    }

    private ScriptEngine engine = MANAGER.getEngineByName("nashorn");
    private Map<String, Object> map = new HashMap<>();
    private Bindings bindings = new SimpleBindings(map);

    private Nashorn() {
        map.put("a", 10);
        map.put("b", 20);
    }

    private void testBindingsAsArgument() throws ScriptException {
        System.out.println("Bindings as argument:");
        engine.eval("c = a + b; a += b", bindings);
        System.out.println("map = " + map);
        System.out.println("eval('c', bindings) = " + engine.eval("c", bindings));
        System.out.println("eval('a', bindings) = " + engine.eval("a", bindings));
    }

    private void testScopeBindings(String scope_name, int scope) throws ScriptException {
        System.out.println("\n" + scope_name + ":");
        engine.getContext().setBindings(bindings, scope);
        engine.eval("c = a + b; a += b");
        System.out.println("map = " + map);
        System.out.println("eval('c') = " + engine.eval("c"));
        System.out.println("eval('a') = " + engine.eval("a"));
    }
}

输出:

Bindings as argument:
map = {a=10, b=20, nashorn.global=[object global]}
eval('c', bindings) = 30.0
eval('a', bindings) = 30.0

ENGINE_SCOPE:
map = {a=10, b=20, nashorn.global=[object global]}
eval('c') = 30.0
eval('a') = 30.0

GLOBAL_SCOPE:
map = {a=10, b=20}
eval('c') = 30.0
eval('a') = 30.0

eval输出行显示结果已正确计算并存储,但map输出行显示结果未存储在我希望的位置。

由于各种原因,这是不可接受的。单个对象不会将计算出的参数存储回自己的本地存储中。来自其他对象上执行的其他脚本的变量将从先前的脚本执行中继承,这可能会隐藏逻辑错误(脚本可能会意外地使用未定义的变量名称,但如果该名称实际上是由前一个脚本使用的,则旧的垃圾值可以用来代替生成的ReferenceError,隐藏错误。)

使用engine.eval() map.put("c", engine.get("c"))将结果移动到我需要的位置,但是使用任意脚本,我不知道所有变量名称是什么,所以不是选项。

所以问题是:无论如何都要捕获全局变量的创建,并将它们存储在应用程序控制下的Java对象中,例如原始的Binding对象??

1 个答案:

答案 0 :(得分:4)

我有一个似乎有效的解决方案,但它显然是一个黑客。

测试程序:

public class Nashorn {
    public static void main(String[] args) throws ScriptException {
        ScriptEngine engine = new ScriptEngineManager().getEngineByName("nashorn");

        Map<String, Object> map = new HashMap<>();
        map.put("a", 10);
        map.put("b", 20);

        try (GlobalMap globals = new GlobalMap(map)) {
            engine.eval("c = a + b; a += b;", globals);
        }

        System.out.println("map = " + map);
    }
}

测试程序根据需要输出map = {a=30.0, b=20, c=30.0}

GlobalMap拦截在密钥"nashorn.global"下存储Nashorn全局对象,因此它不会存储在地图中。当GlobalMap关闭时,它会从Nashorn全局对象中删除任何新的全局变量并将它们存储在原始地图中:

public class GlobalMap extends SimpleBindings implements Closeable {

    private final static String NASHORN_GLOBAL = "nashorn.global";
    private Bindings global;
    private Set<String> original_keys;

    public GlobalMap(Map<String, Object> map) {
        super(map);
    }

    @Override
    public Object put(String key, Object value) {
        if (key.equals(NASHORN_GLOBAL) && value instanceof Bindings) {
            global = (Bindings) value;
            original_keys = new HashSet<>(global.keySet());
            return null;
        }
        return super.put(key, value);
    }

    @Override
    public Object get(Object key) {
        return key.equals(NASHORN_GLOBAL) ? global : super.get(key);
    }

    @Override
    public void close() {
        if (global != null) {
            Set<String> keys = new HashSet<>(global.keySet());
            keys.removeAll(original_keys);
            for (String key : keys)
                put(key, global.remove(key));
        }
    }
}

我仍然希望找到一个解决方案,其中当前范围可以设置为Map<String,Object>Bindings对象,并且脚本创建的任何新变量都直接存储在该对象中。