© Enkel/Shutterstock.com
Polyglotte Softwareentwicklung mit Clojure und Java

Ziemlich beste Freunde


Clojure liebt Java und die JVM! Clojure-Programme werden zur Laufzeit zu Java-Bytecode kompiliert (Clojure ist kein Interpreter) und können somit JIT-optimiert werden. Anstatt die Java-Datentypen hinter sprachspezifischen Wrappern zu verstecken, nutzt Clojure direkt Javas Datentypen, wie z. B. java.lang.String und java.lang.Boolean. Clojures Collection-Datentypen (Listen, Vektoren, Maps, Sets) implementieren das jeweils zugehörige java. util-Interface. Mit Clojure kannst du direkt Java-Interfaces implementieren, Arrays nutzen, von Klassen ableiten und sogar Annotationen verwenden. A perfect match.

Clojure [1] ist eine LISP-artige, funktionale Programmiersprache, die auf der JVM läuft. Mit Hilfe von ClojureScript [2] kann Clojure-Code nach JavaScript transpiliert werden, und somit läuft Clojure-Code auch im Browser und Node.js. Clojure ist also eine Full-Stack-Programmiersprache, die sich sowohl für das Backend als auch für Single-Page-Anwendungen (z. B. React/re-frame [3]) eignet. Clojure hat als Programmiersprache viele nützliche Eigenschaften, u. a.:

  • Immutability-by-Default

  • starke Concurrency-Primitive

  • optionales Type-Checking

  • zugängliche Interoperabilität mit der Hostplattform (JVM/Java, JavaScript)

  • Unterstützung eines interaktiven, agilen Entwicklungsvorgehens

Doch selbst wenn du Clojure für private oder kommerzielle Zwecke einsetzen möchtest, wirst du häufig nicht auf der grünen Wiese anfangen: Dein Projekt/Produkt wird schon in Java implementiert sein, und i. d. R. wirst du nicht die Gelegenheit erhalten, eine bestehende Java-Anwendung from scratch neu in Clojure zu schreiben: zu teuer, zu viel Risiko, zu wenig Nutzen.

Mit Clojure bist du jedoch in der Lage, Teile einer bestehenden Java-Anwendung in Clojure zu reimplementieren, neue Features vollständig in Clojure zu bauen und mit der bestehenden Java-Anwendung zu integrieren. Es ist keine Big-Bang-Umstellung nötig. Stattdessen hast du mit Clojure einen Migrationspfad, der es dir ermöglicht, jene Dinge mit Clojure umzusetzen, die für dich in der jeweiligen Situation den größten Nutzen bringen. Somit kannst du auch die Risiken und Kosten steuern, die mit der Einführung einer neuen Programmiersprache in den (polyglotten) Softwareentwicklungsprozess einhergehen.

In diesem Artikel werde ich nicht so sehr auf Clojure als Programmiersprache eingehen. Stattdessen möchte ich anhand von Codebeispielen konkret zeigen, welche Möglichkeiten es gibt, Clojure- und Java-Programme miteinander zu integrieren.

Getting started

Um die folgenden Beispiele selbst auszuprobieren, benötigst du neben einem JDK >= 1.8 nur clojure-1.8.0.jar und die Quellen aus meinem Git-Repository. Ich verwende im Folgenden eine Windows-Cmd-Shell.

c:\>git clone https://github.com/henrik42/ziemlich-beste-freunde.git c:\>cd ziemlich-beste-freunde

Als Erstes kompilierst du die Java-Quellen und führst eine der Klassen aus, um sicherzustellen, dass alles richtig eingerichtet ist. Die Notation ;;=> in den Beispielen bedeutet, dass dies die Ausgabe auf STDOUT ist.

javac -cp lib\* -d bin src\java_mag\*.java java -cp bin java_mag.Beispiel_1 ;;=> Hallo, Welt!

Nun lädst du clojure-1.8.0.jar nach .\lib\ und prüfst, ob du Clojure aufrufen kannst:

wget -P lib ^ https://repo1.maven.org/maven2/org/clojure/clojure/1.8.0/clojure-1.8.0.jar java -cp lib\* clojure.main -e "(println ""Hello, world!"")" ;;=> Hello, world!

Statische Java-Methoden ausführen

Um Clojure-Code auszuführen, musst du ihn laden. Während des Ladevorgangs wird der Clojure-Code kompiliert und ausgeführt. Es gibt mehrere Möglichkeiten, Clojure-Code zu laden, von denen ich einige vorstellen werde.

Anhand des vorangegangenen Hello-World-Beispiels kannst du erkennen, wie Clojure-Programme als Kommandozeilenargument übergeben und geladen werden können. Das nutzt du, um direkt Java-Code (hier die statische Methode java_mag.Beispiel_1.foo(String)) (Listing 1) auszuführen:

Listing 1: Beispiel_1.java

package java_mag; public class Beispiel_1 { public static String foo(String text) { return "*" + text + "*"; } public static void main(String[] args) { System.out.println("Hallo, Welt!"); } }
java -cp bin;lib\* clojure.main ^-e "(println (java_mag.Beispiel_1/foo ""bar""))" ;;=> *bar*

Java-Instanzen erzeugen und verwenden

Anstatt den Clojure-Code direkt auf der Kommandozeile anzugeben, nutzt du diesmal den Quelltext in Datei beispiel_2.clj (Listing 2). Das folgende Beispiel zeigt, wie du von Clojure aus auf Java-Konstanten (java_mag.Beispiel_2/HELLO), Konstruktoren (java_mag.Beispiel_2.) und Methoden (.foo) (Listing 3) zugreifst:

java -cp bin;lib\* clojure.main -i clj\beispiel_2.clj ;;=> Hello, world!

In Clojure steht das Verb (also das, was getan werden soll) in einem Klammerausdruck immer vorn. Dahinter folgen jene Dinge, auf die sich das Verb bezieht. Somit bedeutet (.foo x "world!"): Führe die Methode foo auf dem Object x aus, übergebe dabei als Argument "world!" und liefere den Rückgabewert als Ergebnis des Klammerausdrucks.

Listing 2: beispiel_2.clj

(let [x (java_mag.Beispiel_2. java_mag.Beispiel_2/HELLO)] (println (.foo x "world!")))

Listing 3: Beispiel_2.java

package java_mag; public class Beispiel_2 { public final static String HELLO = "Hello, "; String text; public Beispiel_2(String text) { this.text = text; } public String foo(String arg) { return text + arg; } }

Java-Interfaces implementieren

In Listing 4 wird mit Hilfe von (reify) on the fly eine Klasse generiert, die das Interface java_mag.Beispiel_3.Foo (Listing 5) implementiert. Zusätzlich wird eine Instanz dieser Klasse konstruiert und als Ergebnis der (Factory-)Funktion (make-foo) geliefert.

Listing 4: beispiel_3.clj

(import (java_mag Beispiel_3 Beispiel_3$Foo)) (defn make-foo [x] (reify Beispiel_3$Foo (bar [_] x)))

Der Bytecode dieser dynamischen Klasse wird nicht in eine Datei geschrieben, sondern zur Laufzeit im Heap als Bytearray erzeugt und direkt an einen Class Loader bzw. die JVM übergeben, die diesen Bytecode als Klassendefinition lädt. Natürlich findet dieser Kompilierschritt nur einmalig beim Laden des Clojure-Codes statt und nicht jedes Mal, wenn die Funktion (make-foo) ausgeführt wird. Das Argument x der Funktion (make-foo) verhält sich wie eine final-Instanzvariable in Java, nur erfolgt die Bindung an die Instanz in diesem Fall über eine Closure [4].

Mit dem folgenden Aufruf wird erst die Datei beispiel_3.clj geladen und ausgeführt. Die Ausführung von (defn) definiert eine Funktion, die auch eine dynamische Klasse ist, und bindet diese Funktion an den Namen make-foo, sodass du die Funktion anschließend über diesen Namen ...

Neugierig geworden? Wir haben diese Angebote für dich:

Angebote für Gewinner-Teams

Wir bieten Lizenz-Lösungen für Teams jeder Größe: Finden Sie heraus, welche Lösung am besten zu Ihnen passt.

Das Library-Modell:
IP-Zugang

Das Company-Modell:
Domain-Zugang