:: JavaScript in Java ::
Introduzione
Quando si scrivono programmi, può venire utile avere un linguaggio di scripting che permetta di estenderne le funzionalità o fargli fare le cose che vogliamo, senza stare lì a ricompilare.
Ci sono diversi linguaggi di scripting, che si possono "embeddare" nei linguaggi "maggiori", come C, C++ etc. Per quanto riguarda il C++, ho trovato AngelScript che ha dalla sua una sintassi simile al C, ed una semplice integrazione con il C++ nativo (non semplicissima, ma rispetto ad altre cose è veramente facile).
In cerca di una roba del genere per Java, ho scoperto con somma meraviglia che JavaScript è già incluso, sottoforma di Rhino, nella JDK 6!
Qui metto quello che ho scoperto sull'integrazione di Rhino in un programma Java.
Nota su JavaScript
A dispetto del nome, non c'entra molto con Java. Ha una sintassi C-like, e quindi Java-like, ma è ben diverso. Se si lasciano stare i concetti balordi, una rapida occhiata alla fonte permette di farsi un'idea di che aspetto abbia.
OpenJDK
Può darsi che si tratti di un problema circostanziato, ma a quanto pare se si usa la OpenJDK occorre installare a parte il package Rhino, perché per qualche oscura ragione il Classpath non include le classi specifiche di Rhino.
Creare lo ScriptEngine
Tiriamo in piedi il nostro programma Java, ed aggiungiamo le seguenti, semplici righe:
ScriptEngineManager manager = new ScriptEngineManager();
ScriptEngine engine = manager.getEngineByName("JavaScript");
Con queste due righe, creo innanzitutto uno ScriptEngineManager che gestisce tutti i motori che voglio creare, e poi tiro fuori da esso un motore, istanza di ScriptEngine, e glielo faccio recuperare per nome: JavaScript. Aaah che bello:)
Posso anche creare diversi ScriptEngine dallo stesso ScriptEngineManager, volendo, però ora non interessa nessuno.
Eseguire codice JavaScript
Siamo già al passo fondamentale: come far eseguire codice JavaScript al nostro ScriptEngine fresco di fabbrica! Ecco come:
engine.eval("println("Ciao:)");
Se invece voglio fargli eseguire un intero files, glielo posso passare in questo modo:
engine.eval(new FileReader("nomefile.js"));
Very easy!
Portare valori dentro lo script
Quando eseguo lo script, posso desiderare che ad esso siano visibili anche delle variabili che vengono impostate all'esterno dello script.
Per fare ciò, ci occorre un altro oggetto chiamato Bindings. Un modo per tirarlo fuori in modo semplice è questo:
Bindings b = engine.getBindings(ScriptContext.ENGINE_SCOPE);
Questa riga prende i bindings del contesto ENGINE_SCOPE (ovvero relativi all'engine corrente), e li restituisce come un oggetto di tipo Bindings.
A questo punto, posso inserire nei bindings le cose che voglio io, in questo modo:
Ho inserito la variabile Valore, che sarà accessibile nel mio script, e le ho assegnato il valore 20. Questo va fatto prima di chiamare engine.eval();.
Dall'interno del codice JavaScript potrò recuperare il valore di Valore in modo semplice:
println("Valore = " + Valore)
Portare valori fuori dallo script
Una volta che ho bindato la variabile Valore, la posso anche tirare fuori dallo script usando i soliti bindings. Ho bindato, ed evaluato, e nello script ho queste righe:
Valore = 132; // Ora la variabile Valore vale 132, sia dentro che fuori lo script
Al termine di eval() posso recuperare il valore in questo modo:
System.out.println("Valore = " + (Double)b.get("Valore"));
Bindare oggetti Java in JavaScript
Tramite i Bindings è possibile anche portare dentro Java degli oggetti, ovvero delle classi compilate.
Supponiamo di avere una classe Esperimento, così scritta:
package provarhino;
public class Esperimento {
public Esperimento() {
System.out.println("Classe Esperimento, scritta in Java");
A = 0;
}
public void setA(int A) {
this.A = A;
}
public int getA() {
return A;
}
private int A;
}
Posso bindare un'istanza di questa classe nei Bindings, in questo modo:
b.put("e", new Esperimento());
e da dentro lo script posso accedere ai metodi di Esperimento in modo naturale:
e.setA(400)
println("e.A = " + e.A)
Creare istanze di una classe all'interno dello script
Nel caso appena visto, la classe viene create in Java, e viene resa visibile allo script. Posso però anche volere che una classe Java sia istanziabile direttamente nello script.
La mia classe Esperimento, come si può vedere sopra, fa parte del package provarhino. Ecco come si rende visibile questo package allo script
importPackage(Packages.provarhino)
In questo modo, tutti i membri del package provarhino saranno visibili allo script. Sarà quindi possibile scrivere del codice javascript così:
var brao = new Esperimento()
brao.setA(10)
println("A = " + brao.getA())
Se, invece di importare l'intero package provarhino, voglio importare solo una sua classe, allora uso questa istruzione:
importClass(Packages.provarhino.Esperimento)
e solo la classe Esperimento sarà visibile all'interno dello script.
Usare la distribuzione di Rhino
Nel caso non si voglia utilizzare il Rhino incluso nella JDK, è sempre possibile scaricare da qui l'ultima distribuzione di Rhino. Del contenuto dello zip a noi serve solo il js.jar, il quale va messo nel classpath. Per ottenere ciò, occorre mettere nelle proprietà di esecuzione del progetto la seguente riga:
-Djava.library.path=/percorso/di/rhino
Nelle varie IDE c'è naturalmente l'opzione per selezionare le librerie da importare nel progetto, ma la riga sopra va comunque specificata nelle opzioni di esecuzione.
Utilizzo di Rhino
La procedura per far partire l'interprete è diversa, perché la distribuzione separata di Rhino non viene inclusa nell'interfaccia generica degli script che Java offre.
Nella classe che ospiterà Rhino, occorre avere almeno queste tre variabili, che metto private per conformità all'OOP:
private Context myContext;
private ScriptableObject myScope;
private Script script;
Ed ecco poi quello che ci serve:
// Creo il contesto per Rhino
ContextFactory cf = new ContextFactory();
myContext = cf.enterContext();
// Recupero lo scope
myScope = new ImporterTopLevel(myContext);
// Inizializzo gli oggetti standard
myContext.initStandardObjects(myScope);
Per eseguire effettivamente gli script, posso scegliere se compilarli oppure valutarli direttamente (compilati dovrebbero essere un po' più performanti). Ecco quello che si fa per compilarli:
script = myContext.compileReader(new FileReader("./scripts/main.js"), "main.js", 0, null);
script.exec(myContext, myScope);
Analizziamo un po' i parametri di compileReader:
- new FileReader("./scripts/main.js") è il reader da cui prendere il file
- "main.js" è il nome che quello script avrà all'interno del motore Rhino. Ad esempio, eventuali errori di interpretazioni saranno presentati come appartenenti al file "main.js"
- 0: numero di riga da cui partire a contare le righe del file
- null: è il SecurityDomain a cui dovrà appartenere lo script. Se ce ne disinteressiamo, come in questo caso, allora lasciamo null.
Ed ecco invece quello che si fa per eseguirli al volo:
myContext.evaluateReader(myScope, new FileReader("blabla.js"), "blabla", 0, null);
Simile a prima, ma con la differenza che lo scope viene indicato direttamente nella chiamata.
Bind di classi
Per esportare classi da Java a JavaScript, invece di usare il sistema dei Bindings visto sopra, si fa così:
// Wrapping di System.out
Object wrappedOut = Context.javaToJS(System.out, myScope);
ScriptableObject.putProperty(myScope, "out", wrappedOut);
Con questo esempio ho voluto esportare nel contesto di JavaScript la gerarchia System.out, così da poter scrivere in JS cose come "out.println("Valore = " + numero)".
Programmazione