捕获 Nashorn 的全局变量
Capturing Nashorn's Global Variables
我有一个 Java 7 程序,它加载了数千个对象(组件),每个对象都有很多参数(存储在 Map
中),并在这些对象上执行各种 Rhino 脚本来计算其他派生参数存储回对象的 Map
。在每个脚本 运行 之前,会创建一个 Scope
对象,由对象的映射支持,在脚本运行期间用作 Java 脚本的作用域。
作为一个简单的例子,下面创建一个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 对象??
我有一个似乎有效的解决方案,但它显然是一个 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 全局对象在键 "nashorn.global"
下的存储,因此它不会存储在映射中。当 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
对象,并且脚本创建的任何新变量都直接存储在该对象中。
我有一个 Java 7 程序,它加载了数千个对象(组件),每个对象都有很多参数(存储在 Map
中),并在这些对象上执行各种 Rhino 脚本来计算其他派生参数存储回对象的 Map
。在每个脚本 运行 之前,会创建一个 Scope
对象,由对象的映射支持,在脚本运行期间用作 Java 脚本的作用域。
作为一个简单的例子,下面创建一个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 对象??
我有一个似乎有效的解决方案,但它显然是一个 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 全局对象在键 "nashorn.global"
下的存储,因此它不会存储在映射中。当 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
对象,并且脚本创建的任何新变量都直接存储在该对象中。