2016-03-04 19 views
5

Ho un programma Java 7, che carica migliaia di oggetti (componenti), ognuno con molti parametri (memorizzati in un Map) ed esegue vari script di Rhino su quegli oggetti per calcolare altri parametri derivati ​​che vengono memorizzati di nuovo nell'oggetto Map. Prima di eseguire ogni script, viene creato un oggetto Scope, supportato dalla mappa dell'oggetto, che viene utilizzato come ambito JavaScript per la durata dello script.Catturare le variabili globali di Nashorn

Come semplice esempio, il seguente crea un HashMap con a = 10 e b = 20, ed esegue lo script c = a + b, che si traduce in c = 30.0 memorizzati indietro nella mappa. Sebbene lo script assomigli alla creazione di una variabile globale c, l'oggetto Scope lo acquisisce e lo memorizza nella mappa; altro script eseguito con un diverso Scope oggetto non vedrà questa variabile:

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"; 
     } 
    } 
} 

Lo script sopra uscite {b=20, c=30.0, a=10}, mostrando la variabile c immagazzinate nella Map.

Ora, ho bisogno di migrare questo Java 8 e utilizzare Nashorn. Tuttavia, sto riscontrando che Nashorn memorizza sempre le variabili globali in uno speciale oggetto "nashorn.global". Infatti, sembra che tutti i binding vengano trattati come di sola lettura e che i tentativi di modificare una variabile esistente generino invece una nuova variabile globale che ombreggia il binding esistente.

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")); 
    } 
} 

uscita:

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 

Le linee di uscita eval mostrano i risultati siano correttamente calcolati e vengono memorizzati, ma le linee di uscita map mostra i risultati non vengono memorizzati dove desidero essere.

Questo non è accettabile, per una serie di motivi. I singoli oggetti non riportano i parametri calcolati memorizzati nella loro memoria locale. Le variabili da altri script in esecuzione su altri oggetti verranno trasferite dalle precedenti esecuzioni di script, che potrebbero nascondere gli errori logici (uno script potrebbe accidentalmente utilizzare un nome di variabile non definito, ma se quel nome era effettivamente utilizzato da uno script precedente, il vecchio valore di garbage pot essere utilizzato al posto di un ReferenceError generato, nascondendo l'errore).

In seguito allo engine.eval() con map.put("c", engine.get("c")) spostare il risultato nel punto in cui ho bisogno di essere, ma con uno script arbitrario, non so quali sarebbero tutti i nomi delle variabili, quindi non è un'opzione.

Quindi la domanda: c'è comunque da catturare la creazione di variabili globali e memorizzarle invece all'interno di un oggetto Java sotto controllo dell'applicazione, come l'oggetto Binding originale ??

risposta

3

Ho una soluzione che sembra funzionare, ma è chiaramente un trucco.

programma di test:

uscite del programma
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); 
    } 
} 

La prova map = {a=30.0, b=20, c=30.0} come desiderato.

Il GlobalMap intercetta la memorizzazione dell'oggetto globale Nashorn sotto la chiave "nashorn.global", in modo che non venga memorizzato nella mappa.Quando il GlobalMap è chiuso, esso rimuove eventuali nuove variabili globali dall'oggetto globale Nashorn e li memorizza nella mappa originale:

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)); 
     } 
    } 
} 

sto ancora sperando di trovare una soluzione in cui la portata attuale potrebbe essere impostata su un Map<String,Object> oppure l'oggetto Bindings e qualsiasi nuova variabile creata dallo script viene archiviata direttamente in quell'oggetto.

+0

È così stupido che dobbiamo ricorrere a questo, il motore Nashorn è testardo come i suoi creatori. Scommetto ... – grimmeld

+2

Invocare l'espressione all'interno di un IIFE fa anche il trucco (devi usare la parola chiave 'var' per dichiarare variabili). Qualcuno sa se questo metodo ha qualche avvertimento? – faizan

+0

@faizan Grazie per il suggerimento. L'uso di un IIFE con 'var c = ...' mantiene 'c' dall'essere immagazzinato nello scope globale. Ciò aiuta a isolare il codice, impedendo a diversi script di vedere accidentalmente i valori calcolati in altri script. Ma questo non aiuta il problema originale: in realtà catturare i valori calcolati memorizzati in queste variabili nella 'Mappa 'fornita dal programma Java che esegue questi snippits di codice JavaScript, da usare successivamente in altre parti del programma. – AJNeufeld