Groovy: Dynamische Integration
Eine der wesentlichen Triebfedern für die Integration von Skriptsprachen in Java-Anwendungen ist die Möglichkeit, Teile der Anwendung auf diese Weise flexibel gestalten zu können, so dass sie modifiziert werden können, ohne dass ein neues Build und Deployment der gesamten Anwendung erforderlich ist. Dies kann reizvoll bei lokal gültigen Geschäftsregeln, Umrechnungsformeln, Anwendungsoberflächen usw. sein, sofern sie nicht mehr ohne Weiteres durch Konfigurationsdaten darstellbar sind. Sie können einfach mit Hilfe von Skripten implementiert und dann jeweils den örtlichen und aktuellen Verhältnissen angepasst werden.
Möglichkeiten der dynamischen Integration von Groovy
[Bearbeiten]Groovy unterstützt die dynamische Integration in Java-Programme auf unterschiedliche Weise. Wir werden uns im Folgenden jedoch auf die wichtigsten Möglichkeiten beschränken, die uns die GroovyShell zur dynamischen Integration von Groovy-Skripten in Java-Programme bietet. Daran lassen sich die wesentlichen Aspekte der Integration soweit erläutern, dass dies für die normalen Anwendungsfälle ausreichen sollte. Die weitergehenden Integrationsmöglichkeiten ausführlich zu behandeln, würde den Rahmen dieses Buchs sprengen. Sie sollen aber kurz erwähnt werden.
- Mit der GroovyShell können Sie einfache Skripte aus Strings, Dateien und anderen Quellen in unkomplizierter Weise kompilieren und ausführen.
- Die GroovyScriptEngine ist besonders zur Ausführung von Skripten geeignet, die wiederum andere Skripte aufrufen.
- Die Klasse GroovyClassLoader stellt den Groovy-eigenen Classloader, der letztendlich für die Ausführung aller Groovy-Skripte verantwortlich ist, und der auch direkt benutzt werden kann.
- Daneben bestehen noch weitere Integrationsmöglichkeiten, die durch andere Werkzeuge geboten werden, zum Beispiel die Integration in den bekannten Container Spring, der nach dem Inversion-of-control-Pattern arbeitet.
- Seit Java 6.0 gibt es im JDK eine abstrakte API für die Einbindung über einem ScriptEngineManager. Um diese Möglichkeit zu nutzen, benötigen Sie eine Factory-Klasse, die das Interface javax.script.ScriptEngineFactory implementiert. Eine solche Klasse kann derzeit noch nicht Bestandteil der Groovy-Standardbibliothek sein, da diese noch mit Java 5 lauffähig sein soll. Es ist aber kein großes Problem, eine solche Factory-Klasse selbst zu erstellen.
Beachten Sie auch Anhang - Wichtige Klassen und Interfaces, wo die wichtigsten Klassen zur Integration ausführlicher dokumentiert sind.
Einfache Skripte übersetzen und ausführen
[Bearbeiten]Der wesentliche Unterschied zwischen der statischen und der dynamischen Integration besteht darin, dass das Groovy-Programm einmal als Bytecode und einmal als Quellcode vorliegt. Während im ersten Fall der Bytecode einer Klasse direkt eingebunden wird, muss im zweiten Fall erst einmal der Quellcode kompiliert werden, bevor der daraus resultierende Bytecode vom Programm aufgerufen werden kann. Mit Hilfe der GroovyShell geht das folgendermaßen:
// Java import groovy.lang.*; . . . String text = "1+1"; // Quelltext des Skripts GroovyShell shell = new GroovyShell(); // Eine Instanz der Groovy-Shell Script script = shell.parse(text); // Das Skript übersetzen Object ergebnis = script.run(); // Das Skript ausführen
Das Skript besteht hier aus einem einfachen arithmetischen Ausdruck. Durch den Aufruf der parse()-Methode der GroovShell mit dem Quellcode als Argument erhalten wir ein Objekt, das das Interface Script implementiert. Sie kennen es schon, da wir uns im Kapitel Objekte damit beschäftigt haben, das die Besonderheiten von Skripten gegenüber normalen Groovy-Klassen behandelt. Dieses Skript führen Sie aus, indem Sie seine Methode run() aufrufen, die das Ergebnis des Skripts als Rückgabewert hat. In diesem Fall ist das Ergebnis ein Integer-Objekt mit dem Wert 2.
Die parse()-Methode der GroovyShell kann auch mit einem File-Objekt oder einem InputStream als Eingabequellen für den Quelltext aufgerufen werden; am Ergebnis ändert dies jedoch nichts.
Sie können das obige Script-Objekt durchaus mehrmals ausführen. In diesem Fall ist dies nicht besonders sinnvoll, denn das Ergebnis wird immer dasselbe sein. Für diese Situation, dass ein Groovy-Skript nur einmal ausgeführt werden soll, gibt es eine Methode evaluate(), die das Parsen und das Ausführen gleich zusammenfasst. Sie ist besonders handlich für die Auswertung kurzer Formeln.
// Java Object ergebnis = sh.evaluate("1+1");
Unabhängig davon, ob Sie das Skript mit der evaluate()-Methode oder mit der Kombination aus parse() und run(), sind in jedem Fall zwei Dinge zu beachten:
- Kleiden Sie die die Methoden zum Parsen und Ausführen des Skripts in einen try-catch-Block ein und fangen Sie jedes Throwable ab, denn in einem dynamischen Skript können sich ja immer unvorhergesehene Fehler befinden.
- Das Ergebnis ist vom Typ Object. Bevor Sie es in einen Typ umwandeln, mit dem Sie weiterarbeiten könne, müssen Sie immer eine Typprüfung ausführen, denn Sie wissen nicht im Vorhinein, ob das Skript ein Ergebnis vom erwarteten Typ liefert.
Unter der Annahme, dass das Skript im letzten Beispiel immer einen Integer-Wert liefern soll, könnte der korrekt abgesicherte Aufruf also etwa so aussehen:
// Java String text = "1+1"; int ergebnis = 0; try { Object obj = sh.evaluate(text); if (obj instanceof Number) { ergebnis = ((Number)obj).intValue(); } else { throw new RuntimeException("Falscher Ergebnistyp"); } } catch (Throwable thr) { System.err.println("Fehler aufgetreten: "+thr); }
Die Absicherung ist noch nicht komplett, weil wir auf diese Weise nicht verhindern, dass bösartiger Code die Anwendung stört. Dies zu verhindern ist etwas komplizierter; im Abschnitt Sicherheitsfragen gehen wir auf dieses Problem näher ein.
Das Binding
[Bearbeiten]Sie wissen bereits, dass Sie in Groovy-Skripten undeklarierte Variablen verwenden können. Sie werden in einem als Binding oder Kontext bezeichneten, Map-ähnlichen Objekt gespeichert, das auch für den Austausch von Informationen zwischen dem Skript und seiner Umgebung verwendet werden kann. Auch die GroovyShell verfügt über ein solches Binding-Objekt; es wird an die durch parse() oder evaluate() erzeugten Skripte weitergereicht. Im Normalfall bleiben dadurch die Binding-Variablen von einem Skript-Aufruf zum nächsten erhalten, allerdings kann das Binding auch im Skript-Objekt ausgetauscht werden.
Das folgende Java-Programm MiniKonsole ist gewissermaßen eine Sparversion der interaktiven zeichenorientierten Skriptkonsole groovysh, die Sie schon aus Kapitel Erste Schritte kennen. Es ermöglicht uns, interaktiv jeweils eine Skriptzeile einzugeben, und zeigt das jeweilige Ergebnis und dessen Typ an, sofern es nicht null ist. Eine leere Eingabezeile beendet die Anwendung.
// Java import java.io.*; import java.util.*; import groovy.lang.*; public class MiniKonsole { public static void main(String[] args) throws Exception { GroovyShell shell = new GroovyShell(); BufferedReader in = new BufferedReader(new InputStreamReader(System.in)); shell.setVariable("start",new Date()); while (true) { System.out.print("groovy> "); String zeile = in.readLine(); if (zeile.trim().length()==0) break; try { Script script = shell.parse(zeile); Object ergebnis = script.run(); if (ergebnis!=null) { System.out.println("==> "+ergebnis+ " ("+ergebnis.getClass().getName()+")"); } } catch (MissingMethodException ex) { if (ex.getMethod().equals("main")) { // Klassendefinition ohne main()-Methode // stillschweigend ignorieren } else { ex.printStackTrace(); } } catch (Throwable th) { th.printStackTrace(); } } } }
Wie Sie sehen, werden hier die Skriptaufrufe korrekt gegen Fehler gesichert. Das Auftreten einer MissingMethodException wird gesondert behandelt, wenn die fehlende Methode den Namen main hat. In diesem Fall enthält das Skript typischerweise eine Klassendefinition ohne main()-Methode und lässt sich daher nicht ausführen. Wir überspringen diesen Fehler, denn der Sinn der Zeile besteht dann sicherlich nur darin, die entsprechende Klasse zu definieren, damit sie später verwendet werden kann.
Eine kurze Session mit der MiniKonsole zeigt, dass Klassendefinitionen und Binding-Variablen von einem Skriptaufruf zum nächsten erhalten bleiben und dass die in der MiniKonsole zu Beginn gesetzte Binding-Variable start im Skript verfügbar ist.
> java -cp %GROOVY_HOME%\embeddable\groovy-all-1.1.jar;. MiniKonsole groovy> println start Sun Jun 10 13:53:48 CEST 2007 groovy> class K1 { def prop; String toString() { "K1=$prop" } } groovy> k1 = new K1(prop:42) ==> K1=42 (K1) groovy> k1.properties.each { println it } prop=42 metaClass=groovy.lang.MetaClassImpl@126e85f[class K1] class=class K1 groovy>
Zusätzliche Funktionalität im Skript
[Bearbeiten]Sie können dem dynamischen Skript nicht nur zusätzliche Informationen in Form von Binding-Variablen mitgeben, sondern auch zusätzliche Funktionalität. Eine Möglichkeit besteht darin, eine eigene Basisklasse zu definieren, die vom Skript anstelle der Klasse java.lang.Script implementiert werden soll. Die Alternative besteht in der Zuweisung einer Closure als Binding-Variable.
Angenommen, Sie möchten Ihren Skripten gerne eine zusätzliche Methode namens dump() zur Verfügung stellen, die den Namen und den Typ aller aktuell im Binding enthaltenen Variablen anzeigt. Dazu können Sie beispielsweise eine neue Skriptklasse ExtScript anlegen, wie sie in Beispiel ## zu sehen ist.
public abstract class ExtScript extends Script { public void dump() { for (Object var : this.getBinding().getVariables().keySet()) { String name = (String)var; println(name+" : "+ this.getBinding().getVariable(name).getClass().getName()); } } }
Die Klasse muss abstrakt sein, denn die von Script geerbte abstrakte Methode run() wird erst im konkreten Skript implementiert. Jetzt müssen wir der GroovyShell noch die neue Basisklasse für Skripte bekannt machen; dazu müssen wir eine Instanz der Klasse CompilerConfiguration anlegen, der wir den vollständigen Klassennamen der neuen Basisklasse übergeben. Diese CompilerConfiguration wiederum übergeben wir im Konstruktor der GroovyShell.
// Java CompilerConfiguration config = new CompilerConfiguration(); config.setScriptBaseClass("ExtScript"); GroovyShell shell = new GroovyShell(config);
Nun können Sie Skriptzeilen mit der Anweisung dump() eingeben.
groovy> dump() start : java.util.Date k1 : K1
Alternativ können Sie auch Closures als Binding-Variablen zuweisen. Am einfachsten ist dies natürlich mit einem kleinen Skript:
// Java shell.evaluate("methode={ arg1,arg2 -> . . . }")
Im Skript können sie die Closure unter dem Variablennamen aufrufen, als wäre es eine Methode:
groovy> methode(x,y)
Dies geht natürlich nur, wenn die Informationen, auf die die Closure zugreift, innerhalb des Skripts - zu Beispiel als Binding-Variablen - verfügbar sind. Wenn dies nicht der Fall ist, können Sie aber recht einfach im Java-Progamm eine Closure erzeugen, die auf eine vorhandene Methode verweist:
// Java shell.setVariable("methode",new MethodClosure(this,"dieMethode"));