问题
I have a Java 7 program, that loads thousands of objects (components), each with many parameters (stored in a Map
), and executes various Rhino scripts on those objects to calculate other derived parameters which get stored back in the object's Map
. Before each script is run, a Scope
object is created, backed by the object's map, which is used as a JavaScript's scope for the duration of the script.
As a simple example, the following creates a HashMap
with a=10 and b=20, and executes the script c = a + b
, which results in c = 30.0
stored back in the map. Although the script looks like it is creating a global variable c
, the Scope
object captures it and stores it in the map; another script executed with a different Scope
object won't see this variable:
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";
}
}
}
The above script outputs {b=20, c=30.0, a=10}
, showing the variable c
has been stored in the Map
.
Now, I need to migrate this the Java 8, and use Nashorn. However, I am finding that Nashorn always stores global variables in a special "nashorn.global"
object. In fact, it seems to be treating all bindings as read-only, and attempts to change an existing variable instead results in a new global variable shadowing the existing binding.
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"));
}
}
Output:
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
The eval
output lines show the results are correctly computed and are being stored, but the map
output lines shows the results are not being stored where I desire them to be.
This is not acceptable, for a variety of reasons. The individual objects do not get the computed parameters stored back in their own local storage. Variables from other scripts executing on other objects will carry over from previous script executions, which could hide logic errors (a script could accidentally use an undefined variable name, but if it that name was actually used by a previous script, the old garbage value could be used instead of a ReferenceError
being generated, hiding the error).
Following the engine.eval()
with map.put("c", engine.get("c"))
would move the result to where I need it to be, but with an arbitrary script, I do not know what all the variable names would be, so is not an option.
So the question: is there anyway to capture the creation of global variables, and store them instead inside a Java object under control of the application, such as the original Binding object??
回答1:
I have a solution that seems to work, but it clearly is a hack.
Test program:
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);
}
}
The test program outputs map = {a=30.0, b=20, c=30.0}
as desired.
The GlobalMap
intercepts the storing of the Nashorn global object under the key "nashorn.global"
, so it doesn't get stored in the map. When the GlobalMap
is closed, it removes any new global variables from the Nashorn global object and stores them in the original map:
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));
}
}
}
I am still hoping to find a solution where the current scope could be set to a Map<String,Object>
or Bindings
object, and any new variables created by the script are stored directly in that object.
来源:https://stackoverflow.com/questions/35807683/capturing-nashorns-global-variables