Перехват глобальных переменных Nashorn
У меня есть программа на Java 7, которая загружает тысячи объектов (компонентов), каждый из которых имеет множество параметров (хранится в Map
) и выполняет различные сценарии Rhino для этих объектов для вычисления других производных параметров, которые сохраняются в объекте Map
, Перед выполнением каждого сценария Scope
объект создается, опираясь на карту объекта, которая используется в качестве области действия JavaScript в течение всего сценария.
В качестве простого примера следующее создает HashMap
с а =10 и б =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 ответ
У меня есть решение, которое, кажется, работает, но оно явно является взломом.
Тестовая программа:
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
объект, и любые новые переменные, созданные сценарием, хранятся непосредственно в этом объекте.