Rubine für Java

Erste Schritte mit JRuby 1.0

Eric Schmieders

Ruby ist eine recht moderne Sprache, die sich rasant weiterentwickelt und durch JRuby nun endlich Einzug in die Java-Welt erfährt. Anlässlich des ersten Major Releases von JRuby 1.0 wird in diesem Artikel die Technologie im Rahmen einer Beispielapplikation verwendet, die das Potenzial von Scriptsprachen verdeutlichen soll.

Das neue Major Release JRuby 1.0 ist sehr ambitioniert und peilt ein hoch gestecktes Ziel an: Auf Anhieb sollte nichts Geringeres als die Kompabilität zu Ruby 1.8.5 erzielt werden. Dazu mussten unter anderem die Built-in Ruby-Libraries in Java implementiert werden, was sicher eines der umfangreicheren Subprojekte war. Das endgültige Release, auf dem dieser Artikel basiert, erfolgte am 12 Juni. Ein paar zusätzliche Informationen über Ruby gibt der Kasten „Hintergründe zu Ruby“. Im Folgenden gehen wir auf die Interoperabilität von Java und Ruby ein, die anhand der Beispielapplikation demonstriert werden soll. Vorab aber erfolgt eine kurze Übersicht über die benötigten Tools. Für die Entwicklung der Beispielapplikation JRubyChestDemo.java wurde Java 1.6 verwendet. Eine Java-Installation, die Generics unterstützt, ist allerdings ausreichend. Des Weiteren wird JRuby 1.0 und die Eclipse 3.3 benötigt. Alternativ zu Eclipse kann natürlich auch NetBeans oder sogar ein schlichter Texteditor eingesetzt werden.

Hintergründe zu Ruby

Ruby wurde von dem Japaner Yukihiro Matsumoto entwickelt und erstmals im Februar 1993 veröffentlicht. Weite Bekanntheit erlangte die Sprache erst durch „Ruby on Rails“. Als Framework dient es zur raschen Entwicklung von Multi-Tier-Applikationen im Web-Bereich.

Ruby ist eine objektorientierte Sprache. Sie ist als „Multiparadigmen-Sprache“ konzipiert. Das heißt, dass Ruby-Programme beispielsweise imperativ implementiert werden können. Damit ändert sich aber nichts an der Tatsache, dass Ruby nur Objekte kennt. Selbst Zahlen wie ‚5‘ sind in der Sprache als Objekte präsent.

Ruby wird während der Laufzeit von einem Interpreter geparsed und dann ausgeführt. An dieser Stelle kommt JRuby ins Spiel. JRuby ist ein Laufzeit Interpreter, der den Ruby-Source in Java-Objekte überführt. Das macht sich die Beispielapplikation zunutze, die in diesem Artikel entwickelt wird.

Nun zur Beispielapplikation. Sie können die Applikation ausführen, ohne den Source in ein Projekt zu laden. Wechseln Sie per Shell in das root-Verzeichnis, das im Baum unten den Platzhalter <Eclipse Projekt Verzeichnis> trägt. Mit der Kommando-Zeile java -cp „.bin;<JRuby-root>libjruby.jar“ de.javamagazin.JrubyChestDemo starten Sie die Anwendung. Ersetzen Sie <JRuby-root> durch das Installationsverzeichnis von JRuby. Die Classpathangabe (-cp) müssen Sie machen, sonst wird die JRuby-Laufzeitumgebung nicht gefunden. Verwenden Sie ein Linux-oder Unix-System, ersetzen Sie außerdem das Semikolon in der Pfadangabe durch einen Doppelpunkt. Alternativ können Sie den Source der Applikation in ein Projekt einer gängigen IDE, beispielsweise Eclipse, importieren. Dazu legen Sie nach der Installation der genannten Tools ein neues Eclipse-Projekt an. Den Installationshinweis zur Software finden Sie auf der jeweiligen Herstellerseite. In das neu erstellte Eclipse-Projekt importieren Sie die .java-Dateien. Ziel des Imports ist ein Source-Verzeichnis (Bsp. „src“). Die .rb-Datei speichern Sie in ein selbst erstelltes Script-Verzeichnis, das den Namen „script“ tragen muss. Verwenden Sie eine andere IDE als Eclipse, dann verfahren sie äquivalent.

Es entsteht folgende Verzeichnisstruktur:


|   .classpath
|   .project
|   
+---bin
|   ---de
|       ---javamagazin
|               Chest.class
|               JRubyChestDemo.class
|               
+---script
|       Chest.rb
|       
---src
    ---de
        ---javamagazin
                Chest.java
                JRubyChestDemo.java

Bitte denken Sie daran, dass die Dateien Chest.java und JRubyChestDemo.java im Unterverzeichnis „de/javamagazin“ liegen müssen, damit der Paketname korrekt aufgelöst werden kann. Als Nächstes ist es notwendig, dass die JRuby-Bibliotheken dem Projekt bekannt gemacht werden, was wie üblich durch Anpassung des Buildpath geschieht. Fügen Sie bei den Project Properties unter Java Build Path alle .jar-Dateien aus dem JRuby-lib-Verzeichnis hinzu. Ansonsten kann der JRuby Interpreter nicht gefunden werden. Nun zur Beispielapplikation JRubyChestDemo.java. Die Klasse kann als Standalone gestartet werden. Kern der kleinen Demo ist eine Truhe (Chest), die durch ein Ruby-Script mit Daten gefüllt wird. Nichts besonderes, kann man spontan meinen, denn erst beim genaueren Hinschauen erkennt man den Clou an der ganzen Geschichte: Im Ruby-Script wird eine Chest-Klasse definiert, die im Java-Programm verwendet wird. Ja, wie geht denn das? Ganz einfach, denn die Ruby-Klasse RubyChest erbt von der Java-Klasse Chest! Und es ist schon ein erstaunliches Feature des JRuby Interpreter, die zwei Welten „Java“ und „Ruby“ so butterweich miteinander zu verschmelzen. Die Entwickler von JRuby kommentieren diesen Mechanismus mit „subclassing java.* is magic“. Ein bisschen der „Magie“ verliert das Subclassing allerdings, wenn man sich in den Hinterkopf zurückruft, dass JRuby ja immerhin in Java implementiert ist und daher der Weg zu Java wohl nicht allzu steinig sein kann. Denn bekanntermaßen bietet Java selber, z.B. durch das Reflection-API, viele Möglichkeiten, auf die Eigenschaften von Klassen und Objekten zu zugreifen. Nichtsdestotrotz ergibt das bequeme Subclassing zwischen Ruby und Java mithilfe des Ruby 1.0 Interpreters eine Menge Design-Möglichkeiten für Entwickler. Diese sollen hier auszugsweise demonstriert werden. Bevor wir die Klassen im Einzelnen beleuchten, verschaffen wir uns zunächst einen Überblick über die beteiligten Komponenten.

Abb. 1: Klassenübersicht zur JRubyChestDemo.java

Die Klasse Chest ist eine abstrakte Superklasse. Ihre Bedeutung für das Projekt ist ambivalent. Erstens erbt sie von java.util.Stack. Durch den Zusatz <String> im extends wird per Generics eine Typsicherheit erzwungen. Damit erfüllt sie die Kernfunktionalität einer Truhe, verschiedene Dinge (repräsentiert durch String-Objekte) aufnehmen zu können. Das wird im Source durch die Methoden add oder auch push realisiert. Beide Methoden stammen aus der Superklasse Stack, bzw. deren Superklasse Vector. Die Methoden werden von der Ruby-Klasse RubyChest genutzt. Entscheidend ist in diesem Zusammenhang folgende Zeile

abstract public void addAll();

Die addAll()-Methode wird von RubyChest überlagert und dazu verwendet, „Dinge“ in die Truhe zu „legen“. Zweitens ist sie die Klasse, die den dedizierten Zugriff auf RubyChest von der Java-Seite aus ermöglicht. Dazu wird ein Typecast während der Laufzeit ausgeführt. Dazu aber später mehr. Drittens verfügt sie über die Funktion, ihren Inhalt in einen String zusammenzufassen und anderen Klassen bereitzustellen – in diesem Fall JRubyChestDemo.java. Das wird durch die Methode public String getPrint() ermöglicht. Da Chest.java für Java-Entwickler keine Überraschungen parat haben sollte, betrachten wir jetzt das Ruby-Script genauer, denn die zehn Zeilen Code haben es in sich.

Listing 1
Ruby Chest
01: require 'java'
02: class RubyChest 

In der ersten Zeile steht die require-Anweisung, die JRuby instruiert das Java-Modul zu laden. Sie ist äquivalent zur Java-import-Anweisung zu verstehen. Zeile zwei deklariert die neue Klasse RubyChest. Sie erbt von der Java-Klasse de.javamagazin.Chest, was durch das „kleiner als“-Zeichen (<) realisiert wird. In der nächsten Zeile wird die Klassenvariable content deklariert. Das doppelte „@„-Symbol weist sie als Klassenvaribale aus. Wie der aufmerksame Leser richtig vermuten wird, sind die Werte [’sunglasses‘,’towel‘,’Java Magazin‘] die „Dinge“, die der Truhe hinzugefügt werden sollen. Die Werte sind durch Kommata getrennt und in eckigen Klammern gefasst, was äquivalent zu anderen Sprachen (beispielsweise Prolog – übrigens auch eine Scriptsprache) als Array-Definition zu interpretieren ist. Das Typisieren von Variablen erfolgt wie bei PHP implizit. So müssen wir auch hier nicht @@content ausdrücklich als Array typisieren.

In der fünften Zeile wird die Methode addAll() definiert. Im Gegensatz zu Java werden Anweisungsblöcke nicht durch geschweifte Klammern kenntlich gemacht. In der Regel startet ein Block mit einem Ausdruck wie def und wird mit end beendet. In den Zeilen sechs bis acht wird über die Werte des content-Arrays iteriert. Die Variable item ist ein Pointer auf den jeweils aktuellen Wert des Durchlaufs. Im Herzen der Iteration, in Zeile sieben, wird das referenzierte item der RubyChest-Instanz hinzugefügt. Das Hinzufügen erfolgt durch den Aufruf von add(item), der nicht aus der Klasse Stack stammt, sondern wiederum aus deren Superklasse java.util.Vector. Hier wird demonstriert, dass nicht nur die Methoden der unmittelbaren Superklasse verfügbar sind, sondern auch die Methoden ihrer Superklasse. Damit der Zusammenhang deutlich wird, hier noch einmal eine kleine Zusammenfassung: RubyChest erbt von de.javamagazin.Chest. Die Ruby-Script-Klasse RubyChest kann die Methoden ihrer Java-Superklassen aufrufen. In der Iteration werden der RubyChest-Instanz die Werte aus der Klassenvariable content hinzugefügt. Spätestens nach der kleinen Zusammenfassung stellt man sich wahrscheinlich die Frage, wo die Instanziierung von RubyChest stattfindet? Um diese Frage zu beantworten, müssen wir uns die Klasse de.javamagazin.JRubyChestDemo etwas genauer anschauen.

Wenn wir einen Blick auf die UML-Übersichtsgrafik werfen, ist unschwer zu erkennen, dass die Klasse JRubyChestDemo die JRuby-Bibliotheken verwendet. Diese wiederum benutzen das RubyChest-Script. Diese abstrakte Beschreibung müssen wir mit etwas Leben füllen und schauen uns dazu den Quelltext der Klasse im Detail an. Die Klasse erbt von der Superklasse java.lang.Thread. Die run()-Methode wird überlagert und implizit durch den Aufruf in der main-Methode new JRubyChestDemo().start(); gestartet. Damit wird ein neuer Thread erzeugt. Für die Beispielapplikation hat das keinen unmittelbaren Nutzen. Sinnvoll wird dieser Mechanismus erst dann, wenn ein System wie die Beispielapplikation in ein anderes Programm integriert wird. Dann nämlich kann das Hauptprogramm seine Dienste verrichten, während der Lademechanismus der Beispielapplikation das Ruby-Script zyklisch auf Veränderungen überprüft. (Die drei aufeinander folgenden Punkte in den Zeilen zehn, dreizehn und vierzehn deuten an, dass die Textformatierung einen Zeilenbruch erzwingt.)

Listing 2
run-Methode aus Chest.java
01:  public void run() {
02:    long lastModified = 0;
03:    while (true) {
04:      File rubyFile = new File(JRUBYFILEABSOLUTEPATH);
05:      if (rubyFile.lastModified() != lastModified) {
06:        lastModified = rubyFile.lastModified();
07:        try {
08:          Chest chest = getCurrentChest();
09:          System.out.println("nCurrent content of 10:
          ...the chest:"+chest.getPrint());
11:        } catch (Exception e) {
12:          System.out.println("Following problem 13:
                             ...occured while creating the chest-
14:          ...object:");
15:          e.printStackTrace();
16:        }
17:      }
18:      try {
19:        Thread.sleep(SLEEP);
20:      } catch (InterruptedException e) {
21:        e.printStackTrace();
22:      }
23:    }
24:  }

In Zeile zwei wird eine Variable definiert, die die letzte Modifikationszeit am RubyChest-Script in Millisekunden speichert. In der dritten Zeile wird eine Endlosschleife gestartet, die die Kernfunktionalität der gesamten Applikation abbildet. In Zeile vier wird ein File-Objekt erzeugt, dass den JRubyScript-File kapselt. Das ist notwendig, denn in der nächsten Zeile wird durch die Vergleichsoperation überprüft, ob das Script geändert wurde. Wurde es geändert, dann soll es neu geladen werden. Darauf folgend soll ein neues Chest-Objekt erzeugt und der neue Truhen-Inhalt ausgegeben werden. Das geschieht in den Zeilen sieben bis 16. Damit der Thread nicht das gesamte System lahm legt, wird er in Zeile 19 für 1000 Millisekunden schlafen gelegt.

Wozu dient das Ganze? Generell hilft ein Ruby-Script, einen kompletten Kompilierungszyklus zu vermeiden. Das Script wird im Plain text gespeichert. Aber wieso will man einen Kompilierungszyklus vermeiden? In komplexen Projekten kann es durch den Einsatz einer umfangreichen Tool Chain zu langen Kompilierungszeiten kommen. Beispielsweise umfasst das Kompilat für einen J2EE-Application-Server neben den binary-Datein auch die mit XDoclet generierten EJB-Descriptoren. Selbst beim automatisierten Ablauf der Tool Chain durch den Einsatz von Ant-Scripten dauert der Deploy-Prozess samt Komprimieren und Kopieren ein paar Minuten – in Abhängigkeit von der Projektgröße. Wird das Projekt einmal am Tag kompiliert, ist das nicht weiter gravierend. Aber gerade in bestimmten Projektphasen, wie beim Debugging, ist häufiges Kompilieren notwendig. Da wird aus ein paar Minuten in Summe schnell eine kleine Ewigkeit.

Der im zweiten Listing skizzierte Mechanismus setzt noch eins drauf. Neben der Kompilierungszeit kann man sogar auf einen Neustart des Programms verzichten. Während der Programmlaufzeit wird das Script neu geladen und das betroffene Programmfragment automatisch aktualisiert. In der Software-Entwicklung nennt man dieses Prozedere Hot Deployment, was einen zusätzlichen „Geschwindigkeitsvorteil“ bei der Entwicklung der Software bedeutet. Das Hot Deployment ist kein Feature, das sich nur auf Scriptsprachen anwenden lässt. Bei Applikationsservern ist ein Hot Deployment für verschiedene Dateien (archivierte Java-Dateien oder auch .xml-Dateien) die Regel. Weiter unten wird gezeigt, wie man diesen Mechanismus im Rahmen unserer kleinen Demo testen kann. Vorab aber noch die Methode, die das RubyChest-Objekt bereitstellt. Listing 3 zeigt die Methode, die das neue RubyChest-Objekt generiert. Die Methode wird in Zeile acht des vorhergehenden Listings aufgerufen.

Listing 3
Generierung des JRuby-Objekts
01:  private Chest getCurrentChest() throws Exception {
02:    getRubyRuntime().evalScript(new BufferedReader(new 
03:                                 ...FileReader(JRUBYFILEABSOLUTEPATH)),JRUBYFILE);
04:    Object o = getRubyRuntime().evalScript("RubyChest.new()");
05:    o = org.jruby.javasupport.JavaEmbedUtils.
06:    ...rubyToJava(getRubyRuntime(),(org.jruby.runtime.builtin.IRubyObject) o, 07: Chest.class);
08:    Chest chest = (Chest) o;
09:    chest.addAll();
10:    
11:    return chest;
12:  }

In Zeile zwei wird die JRuby-Laufzeitumgebung angewiesen, das RubyScript zu parsen und auszuführen. In Zeile 4 wird ein kleines, zusätzliches Script gestartet. Mit dem Ruby-Source RubyChest.new() wird ein neues RubyChest-Objekt erzeugt. Die Variable o verweist auf das neue Objekt. In Zeilen fünf bis acht wird das erzeugt Objekt transformiert und letztlich nach de.javamagazin.Chest geparst.

In Zeile neun kommt dann noch einmal etwas „magic“ zum Einsatz. Dort wird nämlich die Methode addAll() aufgerufen. Wie wir gesehen haben, stammt diese Methode ursprünglich aus der Java-Superklasse und wird in RubyChest überlagert. Durch die Instantiierung von RubyChest innerhalb von JRuby und der Überführung in ein Java-Objekt kann an dieser Stelle der Aufruf addAll() erfolgen. Er fügt der RubyChest-Instanz die Objekte aus @@content hinzu. Die Antwort auf die Frage, wo die Instantiierung stattfindet, ist also: Das Instanziieren findet auf der Ruby-Seite statt. Da aber das Instantiieren von der integrierten JRuby-Laufzeit durchgeführt wird, kann das erzeugte RubyChest-Objekt von der Java-Seite aus genutzt werden. Die Möglichkeit, das Objekt sinnvoll einzusetzen, erfolgt aber erst durch einen Cast zur Superklasse. Nur durch den Cast können die implementierten Funktionen von RubyChest adressiert und ausgeführt werden.
Zum Testen des dynamischen Lademechanismus starten Sie die Applikation. Das Programm erzeugt die Ausgabe:

Starting...

Current content of the chest:
sunglasses
towel
Java Magazin

Ändern Sie das Ruby-Script, aber stoppen Sie während dessen die Programmausführung nicht. Ergänzen Sie die Zeile @@content = [’sunglasses‘,’towel‘,’Java Magazin‘] mit den Worten sun cream. Mit einem Komma trennen Sie den Begriff vom vorhergehenden Wert des Arrays. @@content = [’sunglasses‘,’towel‘,’Java Magazin‘,’sun cream‘].

Speichern Sie jetzt die Datei ab. Die JRubyChestDemo-Applikation läuft derweil weiter. Nach dem Speichern erscheint die neue Ausgabe:

Current content of the chest:
sunglasses
towel
Java Magazin
sun cream

Die Demoapplikation hat den Source automatisch erneut geladen und ein neues Objekt generiert. Die Ausgabe erfolgt durch Aufruf der getPrint()-Methode des Chest-Objekts. Sie können den Ruby-Source beliebig erweitern. Er wird nach Abspeichern der Script-Datei stets neu in das Sytem eingebunden. Dabei stellt sich die generelle Frage, wieviel Logik auf die Ruby- oder auf die Java-Seite verteilt werden soll. Diese Frage soll im Anschluss kurz diskutiert werden. Für das Verlegen der Applikationslogik auf die Ruby-Seite spricht ein schnelles Entwickeln der Software, was durch Ruby als Scriptsprache ermöglicht wird. Wie bei Scriptsprachen üblich können Kompilierungszyklen vermieden werden.

Ruby ist zudem eine sehr unkomplizierte Sprache, was einen Einstieg in die Welt der Programmierung vereinfacht. Anfänger brauchen sich nicht um Typisierungen zu kümmern und können anfangs sogar auf das Definieren von Klassen verzichten. Ruby eignet sich also auch dafür, bestimmte Entwicklungsarbeiten von Fachfremden durchführen zu lassen. Ruby kann beispielsweise zur Lokalisierung einer Applikation verwendet werden. Mit anderen Worten kann die Programmsprache einer Applikation („Englisch“, „Deutsch“ und so weiter) in Ruby geschrieben werden. Die Sprachanpassung wird dadurch sauber von der restlichen Applikation gekapselt.

Gegen das Implementieren von Programmlogik auf der Ruby-Seite spricht die Wartbarkeit des Ruby-Sourcecodes. Beispielsweise werden beim Refactoring der Java-Superklasse Chest mögliche Fehler erst beim Ausführen des erbenden Ruby-Scripts erkannt. Um solche syntaktischen Inkonsistenzen vor Programmausführung aufzudecken, wäre die Erweiterung der IDE durch Plug-ins sinnvoll, die aber derzeit auf dem Markt noch nicht existieren. Bislang führen solche Source-Änderungen wie gehabt zu unbequemen Fehlern, die zwangsläufig erst während der Laufzeit auftreten können.

Man darf bei der Integration von JRuby in den Source einer komplexen Java-Applikation außerdem nicht vergessen, dass Ruby immerhin eine komplette Programmiersprache ist. Der Systemdesigner muss sich also beim Softwaredesign darüber im Klaren sein, dass er dem RubyScript-Entwickler viel Potenzial mit an die Hand gibt. Es ist denkbar, dass die Sprach-Lokalisierung von Externen durchgeführt wird – von Mitarbeitern einer anderen Firma etwa oder von engagierten Usern aus dem Web. Dadurch kann sich die Notwendigkeit ergeben, das Classloading der Ruby-Objekte mit Constraints zu versehen. So kann der Softwaredesigner einem eventuellen Missbrauch einen Riegel vorschieben.

Ein weiterer Nachteil ist ein (wenn auch geringer) Geschwindigkeitsverlust. Der Ruby-Source wird von JRuby geparst und in Java-Objekte umgewandelt, die ihrerseits in einer JVM laufen. Dieser Overhead ist nicht zu übersehen . Zwar lässt sich natürlich die JVM in Hinblick auf die Interpretation von Ruby optimieren, aber das ist Zukunftsmusik.

Das Auflisten der Pros und Contras erfolgt hier nur exemplarisch. Die Argumentationskette lässt sich beliebig lang ausdehnen. Letztlich gibt es keine Faustregel an der man sich beim Planen der Applikationslogik orientieren kann. Das „richtige“ Maß der Verteilung wird allein durch die Applikation und durch ihren Anwendungskontext vorgegeben.

Eric Schmieders, M.A., kam 1998 zum ersten Mal mit Java in Kontakt und hat sich seitdem eingehend mit den drei verfügbaren Editionen sowie mit angrenzenden Technologien (z.B. XML und diversen Skriptsprachen) beschäftigt. Derzeit befasst er sich bei einem international tätigen Dienstleistungsunternehmen mit der Java-basierten Entwicklung FlexRay-konformer Testsysteme. Außerdem ist er als Autor und als freiberuflicher Berater in den Bereichen Client-Server-Anwendungen (Java EE) und Firmenkommunikation tätig.

Zu dem Artikel können Sie hier den Quellcode als zip-Datei downloaden.

Geschrieben von
Eric Schmieders
Kommentare

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht.