Scripting for Java: JavaScript, Groovy etc. in Java nutzen

In Java-Anwendungen können Skripte dynamischer Sprachen wie JavaScript, Groovy etc. aufgerufen werden. Die Basis wurde bereits vor langer Zeit mit dem JSR 223 gelegt: seit Java SE 6 ist die Einbindung der sog. Script Engines nach JSR 223 Teil der Java-Plattform.

Die Art der Skriptsprache ist nicht grundsätzlich eingeschränkt. Besonders interessant sind aber die Sprachen, die auf der Java VM laufen. Sie lassen sich mit der Java-Anwendung sehr eng verknüpfen: Es lassen sich Daten zwische Java und Skript austauschen, Skript-Funktionen aus Java heraus aufrufen und sogar Java-Objekte und Java-Methoden in einem Skript benutzen.

Script Engines

Zur Ausführung eines Skriptes wird eine passende Script Engine benötigt. Seit Java 6 ist eine Engine für JavaScript im Standardumfang von Java enthalten. Diese Rhino genanne Engine in den Versionen 6 und 7 wurde bei Java 8 durch Nashorn abgelöst.

Möchte man weitere Skriptsprachen einsetzen, muss die jeweils passende Implementierungsbibliothek in den Classpath aufgenommen werden. So ist bspw. für Groovy die folgende Maven Dependency nötig:

<dependency>
  <groupId>org.codehaus.groovy</groupId>
  <artifactId>groovy-jsr223</artifactId>
  <version>2.4.7</version>
  <scope>runtime</scope>
</dependency>

Den Einstiegspunkt zur Nutzung eines Skripts stellt die Klasse ScriptEngineManager dar. Mit ihrer Hilfe können zunächst mal die verfügbaren Script Engines aufgelistet werden:

ScriptEngineManager scriptEngineManager = new ScriptEngineManager();
for (ScriptEngineFactory scriptEngineFactory : scriptEngineManager.getEngineFactories()) {
  System.out.printf("Script Engine: %s (%s)\n", scriptEngineFactory.getEngineName(), scriptEngineFactory.getEngineVersion());
  System.out.printf("  Language: %s (%s)\n", scriptEngineFactory.getLanguageName(), scriptEngineFactory.getLanguageVersion());

  for (String alias : scriptEngineFactory.getNames())
    System.out.printf("  Alias: %s\n", alias);
}

Im Beispielprojekt erzeugt dies die folgende Ausgabe:

Script Engine: Oracle Nashorn (1.8.0_72)
  Language: ECMAScript (ECMA - 262 Edition 5.1)
  Alias: nashorn
  Alias: Nashorn
  Alias: js
  Alias: JS
  Alias: JavaScript
  Alias: javascript
  Alias: ECMAScript
  Alias: ecmascript
Script Engine: Groovy Scripting Engine (2.0)
  Language: Groovy (2.4.7)
  Alias: groovy
  Alias: Groovy

Die Namen und Aliase der Script Engines können zur Instanzierung einer konkreten Engine genutzt werden, mit der anschließend Skript-Code ausgeführt werden kann:

ScriptEngineManager scriptEngineManager = new ScriptEngineManager();
ScriptEngine scriptEngine = scriptEngineManager.getEngineByName("js");
scriptEngine.eval("print('Hello, JavaScript!');");

Datenaustausch zwischen Java und Skript

Nun ist das Starten von Skripten zwar interessant, verlöre aber schnell seinen Reiz, wenn man nicht zwischen dem aufrufenden Java-Programm und dem Skript Werte transferieren könnte. Dies ist zunächst einmal möglich mit Hilfe der sog. Bindings, die als Implementierung von Map in der Lage sind, Key-Value-Paare aufzunehmen. Es gibt je ein Bindings-Objekt auf globaler Ebene und in der genutzten Engine, zugreifbar mit der Methode getBindings in ScriptEngineManager bzw. ScriptEngine. Für den einfachen Zugriff auf die Key-Value-Paare gibt es in beiden Interfaces die von Map bekannten Methoden get und put.
Diese Bindings-Einträge lassen sich nun innerhalb des Skripts verwenden. Umgekehrt werden vom Skript erzeugte Werte im Bindings-Objekt der Engine abgelegt. Damit ist ein Austausch von Werten zwischen Java-Programm und Skript möglich:

scriptEngine.put("netto", 100);
scriptEngine.put("steuersatz", 0.19);

scriptEngine.eval("brutto = netto * (1+steuersatz)");

Object brutto = scriptEngine.get("brutto");

Das gezeigte Beispiel arbeitet mit dem Bindings-Objekt der ScriptEngine; die Methoden get und put sind Convenience-Methoden für den Aufruf der jeweils gleichnamigen Methode in getBindings(ScriptContext.ENGINE_SCOPE). Die in einem Bindings-Objekt enthaltenen Werte sind aus Sicht des Skriptes globale Daten vergleichbar mit den im Application Scope bzw. Session Scope einer Web-Anwendung abgelegten Werten.

Ausführung von Skript-Dateien

Einfacher Skript-Code lässt sich wie gezeigt direkt durch Übergabe des Codes als String an die Script Engine ausführen. Für umfangreichere Skripte ist kann statt dessen aber auch ein Reader an ScriptEngine.eval übergeben werden:

InputStream resourceAsStream = Thread.currentThread().getContextClassLoader().getResourceAsStream("scripts/hello.js");
scriptEngine.eval(new InputStreamReader(resourceAsStream));

Aufruf von Skript-Funktionen

Eine weitere Möglichkeit besteht darin, Skripte bzw. Teile daraus als Funktionen aufzurufen. Dazu wird das Skript zunächst einmalig ausgeführt und damit quasi kompiliert. Anschließend können im Skript definierte Funktionen aus dem Java-Programm heraus aufgerufen werden:

// scripts/factorial.js
function factorial(n) {
 return n == 1 ? 1 : n * factorial(n - 1)
}
InputStream resourceAsStream = Thread.currentThread().getContextClassLoader().getResourceAsStream("scripts/factorial.js");
scriptEngine.eval(new InputStreamReader(resourceAsStream));
Invocable invocable = (Invocable) this.scriptEngine;
Object fac5 = invocable.invokeFunction("factorial", 5);

In den Beispielen wurden nur primitive Daten bzw. Objekte der entsprechenden Wrapper-Klassen (int bzw. java.lang.Integer) zwischen Java und Skript ausgetauscht. Darauf ist man allerdings nicht eingeschränkt; es können vielmehr beliebige Objekte übergeben werden:

// scripts/showDate.js
function showDate(timestamp) {
  print('Jahr:  ' + timestamp.get(java.util.Calendar.YEAR))
  print('Monat: ' + (timestamp.get(java.util.Calendar.MONTH)+1))
  print('Tag:   ' + timestamp.get(java.util.Calendar.DAY_OF_MONTH))
}
InputStream resourceAsStream = Thread.currentThread().getContextClassLoader().getResourceAsStream("scripts/showDate.js");
scriptEngine.eval(new InputStreamReader(resourceAsStream));
Invocable invocable = (Invocable) this.scriptEngine;
invocable.invokeFunction("showDate", Calendar.getInstance());

Nutzung von Java-Objekten im Skript

Da das Skript auf der Java VM läuft, hat es auch zugriff auf die Standardbibliothek (oder andere Klassen im Classpath). Die Syntax zum Zugriff auf Java-Klassen ist natürlich sehr abhängig von der eingesetzten Skriptsprache. Groovy bietet z. B. eine Java-ähnliche Syntax an:

// scripts/fillList.groovy
list = new java.util.ArrayList();
list.add('Hugo')
list.add('Willi')
list.add('Otto')
InputStream resourceAsStream = Thread.currentThread().getContextClassLoader().getResourceAsStream("scripts/fillList.groovy");
scriptEngine.eval(new InputStreamReader(resourceAsStream));
List<?> list = (List<?>) scriptEngine.get("list");
list.forEach(System.out::println);

Weitere Informationen

Über Dirk Weil
Dirk Weil ist seit 1998 als Berater im Bereich Java tätig. Als Geschäftsführer der GEDOPLAN GmbH in Bielefeld ist er für die Konzeption und Realisierung von Informationssystemen auf Basis von Java EE verantwortlich. Seine langjährige Erfahrung in der Entwicklung anspruchsvoller Unternehmenslösungen machen ihn zu einem kompetenten Ansprechpartner und anerkannten Experten auf dem Gebiet Java EE. Er ist Autor in Fachmagazinen, hält Vorträge und leitet Seminare und Workshops aus einem eigenen Java-Curriculum.

Hinterlasse einen Kommentar