18.3Ausführen von Skripten
Java ist zwar eine tolle Allround-Programmiersprache, aber die explizite Typisierung und der Zwang, Klassen und Methoden zu deklarieren, machen Java nicht wirklich attraktiv für Skripte, wo eher die Kompaktheit zählt und wo keine lange zu pflegenden Programme entstehen. Um die JVM und die Java-Bibliotheken auch zur Automatisierung von Abläufen einzusetzen, lassen sich neben der Java-Syntax auch alternative Programmiersprachen einsetzen.
18.3.1Java-Programme mit JavaScript schreiben
JavaScript ist eine flexible, interessante Programmiersprache, die mittlerweile auch außerhalb von Browsern populär ist, serverseitig eingesetzt wird und auch für die Entwicklung von Desktop-Anwendungen – siehe Gnome 3, WinRT, Chrome Apps oder FirefoxOS.
Das Oracle JDK/OpenJDK bzw. das JRE[ 135 ](Bei anderen Java SE-Implementierungen muss das nicht zwingend gegeben sein.) bringt ab Java 6 neben dem Java-Compiler eine JavaScript-Engine und ein Kommandozeilentool mit, das Skript ausführt. So lässt sich JavaScript als alternative Programmiersprache einsetzen – die Sprache ist also auswechselbar und steht versinnbildlicht auf den Bibliotheken und der Laufzeitumgebung. Auch lässt sich die JavaScript-Umgebung in eigene Java-Programme einbetten und integrieren.
Die Java-Distribution liefert eine JavaScript-Engine aus, die sich allerdings von Java 6 auf Java 8 verändert hat:
Java 6, Java 7: Rhino (https://developer.mozilla.org/en-US/docs/Rhino). Die JavaScript-Engine – in Java programmiert – kommt von Mozilla und basiert auf einer Implementierung von Netscape, die ihren Browser damals komplett in Java schreiben wollten. Die Browser-Implementierung »Javagator« wurde zwar eingestellt, doch die JavaScript-Umgebung lebte weiter, und Sun lizenzierte die Technologie und bettete sie (minimal verändert) in das JDK ein.
Ab Java 8: Nashorn (http://openjdk.java.net/projects/nashorn/, http://openjdk.java.net/jeps/174). Oracle entwickelte von Grund auf eine ganz neue JavaScript-Engine, die den Code ohne Interpretation direkt in Bytecode übersetzt und eine exzellente Performance und Kompatibilität mit ECMAScript-262 Edition 5.1 zeigt. Obwohl die JavaScript Edition 6 noch keine Rolle spielt, unterstützt Nashorn einige Spracherweiterungen, etwa Abkürzungen für Lambda-Ausdrücke. Da Nashorn nur JavaScript selbst unterstützt, Rhino aber noch einige Mozilla-Bibliotheken, kommt es zu Inkompatibilitäten, falls Nashorn unreines JavaScript ausführen soll.
18.3.2Kommandozeilenprogramme jrunscript und jjs
Java bringt zwei Werkzeuge im bin-Verzeichnis zum Ausführen von Skripten mit:
jrunscript: Führt ein Skript mit einer JSR-223-kompatiblen Skript-Engine aus. Standardmäßig ist das JavaScript, weil keine andere Skript-Engine vorinstalliert ist. Der Schalter -I bestimmt alternative Skriptsprachen. Zur weiteren Dokumentation der Optionen siehe http://docs.oracle.com/javase/8/docs/technotes/tools/windows/jrunscript.html.
jjs: Führt immer JavaScript-Programme mit Nashorn aus. Siehe auch http://docs.oracle.com/javase/8/docs/technotes/tools/windows/jjs.html für weitere Optionen. Interessant für JavaFX-Anwendungen ist der Schalter -fx, um verkürzte JavaFX-Anwendungen in JavaScript schreiben zu können.
Während also jrunscript generisch für alle Skriptsprachen ist, ist jjs exklusiv für Nashorn und bietet bei Angabe eines Log-Levels die Möglichkeit, den abstrakten Syntaxbaum anzuzeigen und vieles mehr.
[»]Hinweis
Ohne Argumente gehen beide in einen interaktiven Modus:
jjs> print("Hallo Nashorn")
Hallo Nashorn
jjs> 12+3
15
jjs>
18.3.3javax.script-API
Die Standardbibliothek bietet über das Paket javax.script eine API, um beliebige Skriptsprachen wie JavaScript, Groovy oder Jython anzusprechen. Diese API wurde im JSR-223, »Scripting for the Java Platform«, definiert.
Die im bin-Verzeichnis stehenden Kommandozeilenprogramme jrunscript und jjs ermöglichen das Setzen von Variablen, das Ausführen von Skriptcode und das Auswerten von Ergebnissen. Eine Skriptsprache kann auf den vollen Umfang der Java-API zurückgreifen, definiert aber üblicherweise auch eigene Bibliotheken.
[zB]Beispiel
Führe ein simples Skript über das Kommandozeilenprogramm aus:
Eine Skript-Engine führt die Skripte aus, indem sie das Parsen, Interpretieren und Verwalten der Objekte übernimmt. Nashorn übernimmt JavaScript, doch es gibt eine große Anzahl weiterer Skriptsprachen; die Webseite http://java-source.net/open-source/scripting-languages führt mehrere auf.
18.3.4JavaScript-Programme ausführen
Bevor wir uns mit der Java-Seite beschäftigen, wollen wir ein kleines JavaScript-Programm schreiben. Es liest aus einer Variablen im Kontext, gibt etwas auf der Konsole aus und deklariert und initialisiert eine neue Variable:
Listing 18.12com/tutego/insel/script/tutego.js
f.setSize( 500, 100 )
f.defaultCloseOperation = javax.swing.JFrame.EXIT_ON_CLOSE
f.title = "Hallo " + name + "."
f.visible = true
today = new Date()
println( today )
month = today.getMonth() + 1
Damit ein Java-Programm das Skript laden und ausführen kann, sind nur zwei Klassen nötig. Der ScriptEngineManager verschafft uns Zugang zu einer gewünschten Skript-Engine. Erfragt wird eine Skript-Engine über einen Namen (wie »JavaScript«) oder über eine Dateiendung, die die Skriptdateien üblicherweise haben (wie »js«).
Listing 18.13com/tutego/insel/script/JavaScriptDemo.java, main() Teil 1
Die Methode getEngineByName(String) liefert ein ScriptEngine-Objekt, dessen eval(…)-Methoden die Interpretation starten:
Listing 18.14com/tutego/insel/script/JavaScriptDemo.java, main() Teil 2
engine.eval( new InputStreamReader(
ScriptDemo.class.getResourceAsStream( "tutego.js" ) ) );
System.out.println( engine.get( "month" ) );
Ausgeführt stellt das Programm ein Swing-Fenster dar und gibt Folgendes auf der Konsole aus:
Sun Jul 08 2007 15:25:12 GMT+0200 (CEST)
7.0
18.3.6Alternative Sprachen für die JVM
Die hochoptimierte JVM und die umfangreichen Java-Bibliotheken lassen sich mittlerweile durch alternative Programmiersprachen nutzen. Auf der einen Seite existieren klassische Interpreter und Compiler für existierende Sprachen wie Ruby, Prolog, LISP, BASIC, Python, die bestmöglich auf die Java-Umgebung portiert werden. Auf der anderen Seite sind es ganz neue Programmiersprachen (wie schon das genannte Groovy), die sich als echte Alternative zur Programmiersprache Java etablieren. Skriptsprachen werden oft über die JSR 223, »Scripting for the Java Platform«, eine standardisierte API, angesprochen.
Dieser Abschnitt gibt einen kleinen Überblick über aktuelle Programmiersprachen auf der JVM. Im ersten Teil geht es um existierende Programmiersprachen, die auf die JVM gebracht werden.
JRuby
JRuby (http://jruby.org/) ist die Java-Version der dynamisch getypten Programmiersprache Ruby (http://www.ruby-lang.org/). Ruby wird in einem Atemzug mit dem Web-Framework Ruby on Rails genannt, einem Framework für Webapplikationen, welches dank JRuby auch auf jedem Tomcat- und Java Application-Server läuft.
Jython
Die beliebte Programmiersprache Python (http://www.python.org/) bringt Jython (http://jython.org/) auf die Java-Plattform. Auch Jython übersetzt Python-Programme in Java-Bytecode und erlaubt relativ schnelle Ausführungszeiten. Jython 2.5 implementiert Python auf 2.5, für Python 2.7 gibt es eine Jython Beta, doch hat sich (C‐)Python mit Version 3.4 auch schon weiterentwickelt. Auch sonst gibt es Unterschiede, etwa bei den eingebauten (nativen) Funktionen. Auf der Basis von Eclipse gibt es die Entwicklungsumgebung PyDev (http://pydev.org/).
Quercus
Quercus (http://quercus.caucho.com/) ist eine Implementierung der Programmiersprache PHP, entwickelt von Caucho Technology. Mit Quercus lassen sich viele beliebte PHP-Projekte in einer Java-Umgebung ausführen. In der Java-Welt werden zwar nicht alle PHP-Funktionen unterstützt, aber dafür gibt es dort keine Speicherüberläufe oder Sicherheitsprobleme.
Clojure
Die Programmiersprache Clojure (http://clojure.org/) ist ein LISP-Dialekt und fällt so in die Kategorie der funktionalen Programmiersprachen. Der Compiler erzeugt direkten Bytecode. Für die Kommandozeile gibt es ein kleines Tool (Read-Eval-Print-Loop, REPL), mit dem jeder erste Versuche von der Kommandozeile aus machen kann. Seit einiger Zeit gibt es auch Umsetzungen für .NET und JavaScript.
LuaJava und Juaj
Eine Umsetzung der Programmiersprache Lua (http://www.lua.org/) für die JVM ist LuaJava (http://www.keplerproject.org/luajava/) bzw. Juaj (http://sourceforge.net/projects/luaj/). Die aus Brasilien stammende dynamisch getypte Programmiersprache Lua zählt zu den performantesten interpretierten Skriptsprachen. Sie ist in erster Linie als eingebettete Programmiersprache zur Applikationssteuerung entworfen worden; prominente Nutzer sind Sim City, World of Warcraft, Adobe Photoshop Lightroom, SciTE, mehr unter http://www.lua.org/uses.html.
Prolog
GNU Prolog for Java (http://www.gnu.org/software/gnuprologjava/) implementiert einen ISO-standardisierten PROLOG-Interpreter. GNU Prolog kann sehr einfach in eigene Java-Programme eingebettet werden.
Die Wikipedia-Seite https://en.wikipedia.org/wiki/List_of_JVM_languages führt weitere Programmiersprachen für die JVM auf. Allerdings sind viele der gelisteten Sprachen für sehr spezielle Anwendungsfälle entworfen, experimentell oder werden nicht mehr gepflegt.
Die genannten Implementierungen bringen eine bekannte Sprache auf die Java-Umgebung, sodass zum Beispiel Code zwischen Plattformen ausgetauscht werden kann. Es gibt auch komplette Neuentwicklungen für neue Programmiersprachen.
Groovy
Groovy bietet eine starke Syntax mit Closures, Listen/Mengen, regulären Ausdrücken, einer dynamischen und statischen Typisierung und vielem mehr. Moderne IDEs wie Eclipse oder NetBeans unterstützen Groovy durch Plugins (http://groovy.codehaus.org/Eclipse+Plugin, http://groovy.codehaus.org/NetBeans+Plugin). Der Groovy-Compiler erzeugt für die Groovy-Klassen den typischen Bytecode, sodass normale Java-Klassen problemlos Groovy-Klassen nutzen können – oder umgekehrt.
Scala
Scala ist eine funktionale objektorientierte Programmiersprache, die in der Java-Community große Zustimmung findet. Plugins für diverse Entwicklungsumgebungen stehen ebenfalls bereit. Auch für die .NET-Plattform gibt es eine Implementierung. Besonders zeichnet Scala ein durchdachtes Typsystem aus. Für viele Entwickler ist es »Java 2.0«.
In den letzten Jahren sind vermehrt neue JVM-Programmiersprachen aufgetaucht, sie sind vom Sprachdesign her auf jeden Fall interessant, finden aber bisher kaum großen Einsatz. Zu diesen zählen etwa Fantom (http://fantom.org/), Ceylon (http://ceylon-lang.org/) oder Gosu (http://gosu-lang.org/).
Geschichte
Als Java noch unter der Sonne stand, stellte Sun zentrale Entwickler ein, um die Weiterentwicklung von Skriptsprachen unter der JVM zu unterstützen. Darunter etwa im März 2008 Frank Wierzbicki, Hauptentwickler von Jython. Doch schon nach 1,5 Jahren verließ er Sun wieder.[ 136 ](http://fwierzbicki.blogspot.de/2009/11/leaving-sun.html) Das gleiche Spiel mit den Entwicklern von JRuby, Charles Nutter und Thomas Enebo, die 2006 zu Sun gingen und 2009 das Unternehmen in der Oracle- Akquisitionsphase verließen.[ 137 ](http://news.idg.no/cw/art.cfm?id=C0D2078D-1A64-6A71-CE889FFB617BA47D) NetBeans bot einst den besten (J)Ruby-Editor, doch entfernte die Version NetBeans 7.0 die Unterstützung komplett.[ 138 ](https://netbeans.org/features/ruby/index.html) Umgekehrt wird der C++-Editor immer ausgefeilter.
18.3.7Von den Schwierigkeiten, dynamische Programmiersprachen auf die JVM zu bringen *
Seit Java vor über 10 Jahren auf dem Markt erschien, hat sich vieles geändert (und vieles ist auch gleich geblieben). Auffällig ist eine starke Zunahme von Skriptsprachen – spielten sie vor 10 Jahren kaum eine Rolle, sind sie heute unübersehbar. Möglicherweise ist ein Grund dafür, dass vor 10–20 Jahren nicht compilierte Sprachen einfach nicht die nötige Performance brachten, während heute auch durch leistungsfähige Maschinen und intelligente Ausführung die Leistung einfach da ist.
Skriptsprachen auf der JVM
Eine unübersehbare Tatsache ist, dass viele Skriptsprachen heute in einer virtuellen Maschine laufen. Eine besondere Rolle nehmen dabei die Java-Plattform (mit der JVM) und die .NET-Plattform mit CLR (einer virtuellen Maschine für .NET) ein. Zwei Trends zeichnen sich ab: Zum einen werden existierende Skriptsprachen auf die JVM/CLR übertragen und zum anderen Sprachen explizit für die virtuellen Maschinen entworfen:
Skriptsprache | Für JVM | Für CLR |
---|---|---|
Python | Jython* | IronPython |
Ruby | JRuby** | IronRuby |
Lua | Jill, Kahlua | LuaCLR |
JavaScript | Rhino/Nashorn | IronJS |
PHP | Quercus*** | |
Tcl | Jacl | |
Groovy**** | ||
JavaFX | ||
Boo | ||
*) http://www.jython.org/ |
Tabelle 18.4Skriptsprachen auf der JVM (Java) und CLR (.NET)
Rhino[ 139 ](http://developer.mozilla.org/en-US/docs/Rhino) ist als JavaScript-Engine in Java 6 und Java 7 integriert, in Java 8 wurde Rhino von Nashorn abgelöst.
Umsetzung der Skriptsprachen auf der JVM
Eine Skriptsprache, die explizit für eine VM entworfen wurde, berücksichtigt natürlich Einschränkungen der JVM. Existierende Programmiersprachen sind eine ganz andere Herausforderung, da sie Sprach- und Laufzeitkonstrukte bieten können, die auf der JVM vielleicht nicht unterstützt werden. Das ist ein Problem, und drei Strategien zur Lösung bieten sich an:
Ignorieren der Eigenschaften: Python ermöglicht Mehrfachvererbung, Java nicht. Daher kann das Verhalten von Python unter Java nicht perfekt nachgebildet werden, obwohl Jython sein Bestes gibt. Weiterhin kann Python Interrupts auffangen, aber so etwas gibt es unter Java nicht, also auch nicht in Jython. (Das ist aber eher eine Bibliotheks- und weniger eine Spracheigenschaft.)
Nachbilden der Eigenschaften: In Sprachen wie JavaScript, Python oder Ruby gibt es das so genannte Duck Typing. Erst zur Laufzeit wird das Vorhandensein von Methoden geprüft, und die Typen müssen nicht im Quellcode stehen. In JavaScript ist zum Beispiel function add(x, y){return x + y;} oder function isEmpty(s){return s == null || s.length() == 0;} erlaubt, und erst später beim Aufruf stellt sich heraus, was x und y überhaupt ist und ob ein Plus-Operator/eine length()-Methode definiert ist. Deklarationen und Aufrufe dieser Art sind nur mit vielen Tricks auf der JVM umzusetzen.
Ändern der JVM zur Unterstützung der Eigenschaften: Die Spracheigenschaften zu ignorieren ist natürlich keine schöne Sache. Glücklicherweise kommt das selten vor, denn die JVM macht vieles mit. Ruby erlaubt etwa Methodennamen wie ==, <, >, +, und 1.+2 ist gültig. Die Bezeichner sind in Java zwar nicht möglich, aber die JVM hat grundsätzlich keine Probleme mit den Bezeichnern. Es ist interessant zu sehen, wie unterschiedlich Java als Sprache und die JVM sind, denn vieles in Java gibt es in der JVM gar nicht. Aufzählungstypen zum Beispiel setzt der Compiler als einfache Klassen um. Oder dass eine Klasse ohne einen vom Entwickler explizit geschriebenen Konstruktor automatisch einen Standard-Konstruktor bekommt, ist nur etwas, das der Compiler generiert. Oder ein super() als ersten Aufruf im Konstruktor – alles das sind Compilereigenschaften, von Generics ganz zu schweigen.
Wenn die Besonderheiten einer Skriptsprache über Tricks und intelligentes Übersetzen in Bytecode realisiert werden können, stellt sich die Frage, ob an der JVM überhaupt Änderungen nötig sind. Wie bei vielen Diskussionen kommt dann ein Argument auf, das Änderungen oft rechtfertigt: Performance. Wenn eine Änderung in der JVM die Abarbeitung bestimmter Konstrukte massiv beschleunigt, ist das ein Grund für die Änderung. Und somit sind wir beim neuen Bytecode invokedynamic angelangt.
Umsetzung von Duck Typing
Reine Java-Programme benötigen den Bytecode invokedynamic nicht (obwohl der Java-Compiler in Java 8 Closures so umsetzt). Er ist einzig und allein in die JVM eingeführt worden, um Methodenaufrufe der Skriptsprachen auf der JVM zu beschleunigen. Die bisherigen Bytecodes von Methodenaufrufen tragen alle nötigen Typinformationen, so wie es bei Java üblich ist. Nehmen wir:
Der Compiler setzt den Methodenaufruf seit eh und je mit dem Befehl invokevirtual um, wobei der Compiler für die Laufzeit alle Typinformationen in den Bytecode setzt:
Der Empfänger ist PrintStream, und seine Methode ist println.
Der Parametertyp ist ein int.
Die »Rückgabe« ist void.
Exakt diese Typen müssen vom Compiler in Bytecode gegossen werden, sonst kann die JVM den Aufruf nicht durchführen.
Skriptsprachen hingegen sind da lascher. Nehmen wir noch einmal unser Beispiel aus JavaScript:
Zur Laufzeit ist klar, mit welchem Typ s die Methode aufgerufen wird, aber eben nicht zur Compilezeit, in der der Bytecode erstellt werden muss. Was soll der Compiler also machen, wenn er Bytecode erstellen soll, aber ganz offensichtlich der Rückgabe- und der Parametertyp fehlen? Natürlich könnten sie über einen Trick zu Object ergänzt werden. Spielen wir kurz die Konsequenzen durch, wenn der Java-Compiler folgende Umsetzung wählt:
Das sieht gut aus. Aber jetzt wird es haarig bei length(), das keine Methode von Object ist. Das Problem kann ein Compiler auf zwei Arten lösen. Zunächst kann der Aufruf von length() an das unbekannte Objekt s über Reflection gelöst werden. Da Reflection-Aufrufe aber nicht so performant sind wie richtige Methodenaufrufe, ist diese Lösung nicht so effizient. Eine zweite Lösung besteht in der Einführung einer Schnittstelle, die genau die fehlenden Operationen deklariert:
Object isEmpty( I s ) { return s == null || s.length() == 0; }
Ja, das geht – irgendwie. Das ist performanter als Reflection, aber auf diese Weise kommt eine riesige Anzahl neuer Schnittstellen bzw. Klassen in die JVM, deren Behandlung recht speicherintensiv ist. Außerdem können die Deklarationen von Methoden in Skriptsprachen auch außerhalb von Klassen vorkommen, sodass für Java ein Fake-Methoden-Empfänger erzeugt werden müsste.
invokedynamic und Hilfsklassen
Um die sonderbare Methodendeklaration in Java performant umzusetzen, hat der JSR-292, »Supporting Dynamically Typed Languages on the Java Platform«, den neuen Bytecode invokedynamic definiert. Bei invokedynamic sind viel weniger Typinformationen nötig als bei den anderen vier existierenden Bytecodes für Methodenaufrufe. Generiert wird der Bytecode zum Beispiel von Skriptsprachen, wenn Typinformationen fehlen. Die Compiler der Skriptsprachen nutzen Bytecode-Bibliotheken wie ASM (http://asm.ow2.org/) und umgehen den Java-Compiler zur Erstellung der Klassendateien.
Der neue Bytecode ist die eine Seite. Aber wenn die Typinformationen fehlen, insbesondere der wichtige Empfänger (wie PrintStream bei println(…)), wie kommt die JVM zur wirklichen Implementierung? Etwa bei unserem Beispiel von isEmpty(s), in dem es einen invokedynamic-Aufruf von s.length() gibt. Der Aufruf muss ja irgendwo landen.
Zwei Dinge sind hier zusätzlich nötig: Das erste ist, dass es neben dem Aufruf von invokedynamic noch eine zusätzliche Information im Bytecode gibt, nämlich von einer Bootstrap-Methode. Findet die JVM zum ersten Mal ein invokedynamic, dann weiß sie nicht, an wen der Aufruf geht, und wendet sich an die Bootstrap-Methode. Zur passenden Auswahl der Zielmethode übergibt die JVM an die Bootstrap-Methode Informationen über den Aufrufer und den Namen, sodass alle wichtigen Informationen zur Auswahl des Ziels vorhanden sind. Hier sind wir nun in der zweiten Hälfte, denn zusätzlich zum neuen Bytecode gibt es ein neues Paket java.lang.invoke. Die Bootstrap-Methode spezifiziert mit der API des neuen Pakets die Zielmethode und gibt diese an die JVM weiter. Wenn das einmal geschehen ist, ist der Aufruf gebunden, und die JVM ruft beim dynamischen Aufruf direkt die vom Bootstrap gelieferte Methode auf. Der genaue Ablauf bei den invokedynamic-Aufrufen dokumentiert das Paket.
Generiert wird er von dynamischen Skriptsprachen, um die Aufrufe schnell von der JVM ausführen zu lassen. Nur Bytecode-Bauer werden mit invokedynamic in Kontakt kommen, »08-15«-Entwickler nie. Der neue Bytecode muss von der JVM natürlich unterstützt werden, sonst ist die Umsetzung zwecklos.