【问题标题】:Capturing Nashorn's Global Variables捕获 Nashorn 的全局变量
【发布时间】:2016-06-18 20:59:51
【问题描述】:

我有一个 Java 7 程序,它加载数千个对象(组件),每个对象都有许多参数(存储在 Map 中),并在这些对象上执行各种 Rhino 脚本以计算其他派生参数,这些参数又存储在对象的Map。在每个脚本运行之前,会创建一个 Scope 对象,由对象的映射支持,在脚本运行期间用作 JavaScript 的作用域。

作为一个简单的例子,下面创建了一个 a=10 和 b=20 的 HashMap,并执行脚本 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 对象??

【问题讨论】:

    标签: javascript java rhino nashorn


    【解决方案1】:

    我有一个似乎可行的解决方案,但它显然是一个 hack。

    测试程序:

    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&lt;String,Object&gt;Bindings 对象,并且脚本创建的任何新变量都直接存储在该对象中。

    【讨论】:

    • 我们不得不求助于这个太愚蠢了,我敢打赌,nashorn 引擎和它的创造者一样顽固......
    • 在 IIFE 中调用表达式也可以解决问题(您需要使用 var 关键字来声明变量)。任何人都知道该方法是否有任何警告?
    • @faizan 感谢您的建议。使用带有var c = ... 的IIFE 确实可以防止c 被存储在全局范围内。这确实有助于隔离代码,防止不同的脚本意外看到其他脚本中计算的值。但这无助于解决最初的问题:实际捕获存储在Map 中的这些变量的计算值,这些变量由执行这些 JavaScript 代码片段的 Java 程序提供,以供稍后在程序的其他部分使用。
    猜你喜欢
    • 1970-01-01
    • 2016-07-02
    • 2020-04-27
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-07-07
    相关资源
    最近更新 更多