Groovy: Statische Integration

Als statische Integration bezeichnen wir hier die Kombination von kompilierten Java- und Groovy-Programmteilen; dabei werden keine Groovy-Skripte oder -Klassen zur Laufzeit übersetzt. Statische Integration bedeutet aber nicht, dass die dynamischen Fähigkeiten von Groovy außer Kraft gesetzt werden. Dies werden Sie weiter unten bestätigt finden, wenn wir auch mit den Mitteln von Java versuchen, die dynamischen Möglichkeiten der Sprache Groovy zu nutzen.

Eine Groovy-Klasse konventionell einbinden

Bearbeiten

Wir üben die statische Integration einer Groovy-Klasse in ein Java-Programm wieder einmal an einem sehr einfachen Beispiel, an dem sich das Prinzip besonders gut darstellen lässt. Die im folgenden Beispiel dargestellte Groovy-Quelldatei definierte eine Klasse namens EineGroovyKlasse.

// Groovy
class EineGroovyKlasse {
  String eineProperty
  def eineMethode() { println 42 }
}

Wir schreiben die Datei mit einem normalen Texteditor und speichern sie unter EineGroovyKlasse.groovy ab. Die Klasse hat eine Property namens eineProperty, und wir wissen, dass Groovy dazu zwei Accessor-Methoden getEineProperty() und setEineProperty() generiert. Wir ziehen es vor, die Property zu typisieren und nicht einfach nur mit def zu deklarieren, denn andernfalls müsste bei jedem lesenden Java-Zugriff ein Typecast durchgeführt werden. Die Methode eineMethode() hat keine Parameter und kann wie jede andere Methode eines Java-Objekts verwendet werden.

Ein kurzes Java-Programm instanziiert die Klasse, setzt die Property, liest sie aus und ruft einmal die Methode auf. Wir speichern es als EinJavaProgramm.java ab.

// Java
public class EinJavaProgramm {
  public static void main(String[] args) {
    EineGroovyKlasse eg = new EineGroovyKlasse();
    eg.setEineProperty("Beispiel");
    System.out.println(eg.getEineProperty());
    eg.eineMethode();
  }
}

Übersetzen Sie beide Klassen mit dem Groovy- bzw. dem Java-Compiler und rufen Sie es anschließend die Java-Klasse ganz normal auf. Wie gesagt: Dabei dürfen Sie nicht vergessen, die Groovy-Bibliotheken in den Klassenpfad aufzunehmen.

> groovyc EineGroovyKlasse.groovy
> javac EinJavaProgramm.java
> java -cp %GROOVY_HOME%\embeddable\groovy-all-1.1.jar;. EinJavaProgramm
Beispiel
42

Insofern also eigentlich nichts Besonderes. Allerdings ist zu beachten, dass auf diese Weise nur die tatsächlich in die Klasse hineinkompilierten Felder und Methoden zur Verfügung stehen. Dies sind die explizit definierten Methoden und Felder, die hinzugenerierten Getter- und Setter-Methoden sowie die im Interface GroovyObject definierten Methoden.

Auf dynamische Methoden und Properties zugreifen

Bearbeiten

Die auf das jeweilige Objekt anwendbaren vordefinierten Methoden und dynamisch hinzugefügten Methoden können Sie allerdings nutzen, indem Sie die bei allen Groovy-Objekten vorhandenen Methoden invokeMethod(), getProperty() und setProperty() anwenden.

So lässt sich der Aufruf einer Methode getProperties() bei einer Instanz von EineGroovyKlasse, die in einem Groovy-Programm immer verfügbar ist, in Java nicht kompilieren:

// Java
Map properties = eg.getProperties(); // nicht kompilierbar

Sie können diese Methode aber dynamisch aufrufen, müssen das Ergebnis allerdings immer mit cast auf den erwarteten Typ anpassen:

// Java
Map properties = (Map)invokeMethod("getProperties",null);

Ähnlich können Sie mit den Properties des Objekts verfahren. Die beiden folgenden Java-Zeilen setzen die Property eineProperty und lesen sie aus.

// Java
eg.setProperty("eineProperty","ein Text");
String propertyInhalt = (String)eg.getProperty("eineProperty");

Allerdings stehen die Methoden invokeMethod(), getProperty() und setProperty() für den dynamischen Zugriff auf Methoden und Properties nur bei Objekten zur Verfügung, die das Interfaces GroovyObject implementieren. Nun kann es aber durchaus einmal vorkommen, dass Sie von Java aus auf Methoden oder Properties eines Nicht-Groovy-Objekts zugreifen möchten. In diesem Fall müssen Sie auf die in Groovy definierte Metaklasse zugreifen. Beispielsweise gibt es eine vordefinierte Methode namens reverse() für Strings, die einen String mit den Zeichen des Strings in umgekehrter Reihenfolge liefert. Und auch für Java-Klassen ist die virtuelle Property properties verfügbar. Wenn Sie diese Dinge aus Java nutzen möchten, können Sie auf die entsprechende Metaklasse ausweichen:

// Java
import groovy.lang.*;
...

String text = "abcdefg";
MetaClass meta = GroovySystem.getMetaClassRegistry()
   .getMetaClass(text.getClass());
String textVerkehrt = meta.invokeMethod(text,"reverse",null);
Map properties = (Map)meta.getProperty(text,"properties");

Ein Wrapper für dynamische Klassen

Bearbeiten

Alle diese dynamischen Zugriffe auf Groovy-Methoden und -Properties sind etwas umständlich und vor allem architektonisch unschön, weil Sie in Ihrem Java-Code erhebliche Abhängigkeiten von der Groovy-Umgebung bekommen. Völlig verhindern können Sie dies natürlich nicht, wenn Sie die speziellen Möglichkeiten von Groovy in Java-Programmen nutzen möchten. Sinnvoll ist es aber, die Abhängigkeit von Groovy in einer Klasse zu kapseln, die den Zugriff etwas vereinfacht und ohne intime Kenntnisse der Groovy-eigenen Mechanismen verwendet werden kann. Folgendes Beispiel zeigt die Klasse DynamicObject, die genau dies leistet. Sie setzt eine Java-Version von mindestens 5.0 voraus, da wir variable Argumentlisten und generische Methoden nutzen.

import groovy.lang.*;
import org.codehaus.groovy.runtime.*;
import org.codehaus.groovy.runtime.typehandling.DefaultTypeTransformation;

public class DynamicObject {
  static MetaClassRegistry registry = InvokerHelper.getInstance()
           .getMetaRegistry();
  Object theObject; // Das Groovy- oder Java-Objekt
  boolean isGroovy; // Flag für Groovy-Objekt

  /**
   * Konstruktor übernimmt das gekapselte Objekt und prüft,
   * ob es sich um ein Groovy-Objekt handelt.
   */
  public DynamicObject(Object obj) {
    theObject = obj;
    isGroovy = obj instanceof GroovyObject;
  }

  /**
   * Untypisierter dynamischer Aufruf einer Methode.
   * @param method Name der Methode
   * @param args Argumente für den Methodenaufruf
   * @return Ergebnis als Object
   */
  public Object invoke(String method, Object... args) {
    if (isGroovy) {
      return ((GroovyObject)theObject).invokeMethod(method, args);
    } else {
      return getMeta().invokeMethod(theObject, method, args);
    }
  }

  /**
   * Typisierter dynamischer Aufruf einer Methode.
   * @param type Erwarteter Typ des Ergebnisses
   * @param method Name der Methode
   * @param args Argumente für den Methodenaufruf
   * @return Ergebnis als Objekt des angegebenen Typs
   */
  public <T> T invoke(Class<T> type, String method, Object... args) {
    return cast(invoke(method,args),type);
  }

  /**
   * Untypisierte Abfrage einer Property.
   * @param property Name der Property
   * @return Wert der Property als Object
   */
  public Object get(String property) {
    if (isGroovy) {
      return ((GroovyObject)theObject).getProperty(property);
    } else {
      return getMeta().getProperty(theObject, property);
    }
  }
   
  /**
   * Typisierte Abfrage einer Property.
   * @param type Erwarteter Typ des Ergebnisses
   * @param property Name der Property
   * @return Wert der Property als Object
   */
  public <T> T get(Class<T> type, String property) {
    return cast(get(property), type);
  }
  
  /**
   * Setzt eine Property.
   * @param property Name der Property
   * @param value Zu setzender Wert
   */
  public void set(String property, Object value) {
    if (isGroovy) {
      ((GroovyObject)theObject).setProperty(property, value);
    } else {
      getMeta().setProperty(theObject, property, value);
    }
  }
   
  // Ermittelt die Metaklasse zum aktuellen Objekt.
  // Nur benötigt, wenn es kein Groovy-Objekt ist.
  private MetaClass getMeta() {
    return registry.getMetaClass(theObject.getClass());
  }
  
  // Typabbildung auf den angegebenen Zietyp.
  // Nutzt die Groovy-Mechanismen zur Typanpassung.
  private <T> T cast(Object obj, Class<T> type) {
    Object res = DefaultTypeTransformation.castToType(obj,type);
    return type.cast(res);
  }
}


Mit DynamicObject können die dynamischen Zugriffe auf EineGroovyKlasse nun folgendermaßen aussehen:

// Java
EineGroovyKlasse eg = new eineGroovyKlasse();

// DynamicObject-Instanz erzeugen.
DynamicObject dyn = new DynamicObject(eg);

// Dynamischer Aufruf einer Methode.
Map properties = dyn.invoke(Map.class,"getProperties");

// Dynamisches Setzen und Abfragen einer Property.
dyn.set("eineProperty","ein Text");

// Dynamisches Auslesen einer Property mit Typanpassung.
String inhalt = dyn.get(String.class,"eineProperty");

In derselben Weise können Sie nun auch dynamische Methoden und Properties eines Java-Objekts verwenden.

// Java
...
String text = "abcdefg";
DynamicObject dynText = new DynamicObject(text);
String textVerkehrt = dynText.invoke("reverse");
Map properties = dynText.get(Map.class,"properties");

Wir könnten sogar noch einen Schritt weiter gehen und in DynamicObject diejenigen vordefinierten Methoden, die für jedes Objekt verfügbar sind, schon gewissermaßen fest verdrahten. Dadurch würden wir etwas mehr Typsicherheit zur Compile-Zeit gewinnen, die es im dynamischen Groovy nicht geben kann, die wir aber in Java-Programmen nicht ohne Grund schätzen. Als Beispiel könnte es eine zusätzliche Methode iterator() geben, die, wie in Groovy üblich, für jedes beliebige Objekt einen Iterator liefert. Damit implementieren wir auch gleich noch das Interface java.lang.Iterable.

// Java
public class DynamicObject implements Iterable {
  ...
  public Iterator<T> iterator(Class<T> elementType) {
    return invoke(elementType,"iterator");
  }
}

Das Elegante dabei ist, dass wir den Iterator auch gleich in for-Schleifen im Java-5.0-Stil einsetzen könnten:

// Java
String s = "abcde";
for (Object c : new DynamicObject(s)) { . . . }

Natürlich sollte der Iterator auch noch typisiert werden; wie Sie dies machen, ist aber eher ein Java- als ein Groovy-Problem und soll daher an dieser Stelle nicht weiter behandelt werden.

TODO: Aufruf spezieller Groovy-Klassen, z.B. Closures.

Architekturfragen

Bearbeiten

Wenn Sie Anwendungen schreiben, die teils aus Java- und teils aus Groovy-Klassen bestehen, können Sie sehr leicht in ein schwer auflösbares Gestrüpp wechselseitiger Abhängigkeiten geraten, das sich zwar mit Hilfe des Joint Compiler immer noch kompilieren lässt, aber auch Architektursicht inakzeptabel ist. Ein einfaches Beispiel zeigt das Problem: Angenommen, Sie haben drei Klassen K1, K2 und K3. K1 benötigt K2 und K2 benötigt K3 (siehe UML-Diagramm in folgener Abbildung). Wenn nun die Klassen K1 und K3 in Java programmiert sind, K2 aber in Groovy, haben Sie eine solche wechselseitige Abhängigkeit.

 
Wechselseitige Abhängigkeit zwischen Groovy- und Java-Klassen

In solchen Situationen empfiehlt es sich, die Anwendung in verschiedene Konfigurationseinheiten aufzuteilen, die getrennt kompiliert werden können, und die Klassen durch Interfaces zu entkoppeln.

  • Legen Sie eine Konfigurationseinheit A mit zwei in Java geschriebenen Interfaces I2 und I3 an. Die Interfaces müssen von den Klassen K1 bzw. K2 entsprechend implementiert werden.
  • Eine zweite Konfigurationseinheit B enthält die Java-Klassen. K1 wird so geändert, dass sie nicht mehr direkt auf K2, sondern stattdessen auf das von K2 implementierte Interface I2 zugreift, und K3 implementiert jetzt das Interface I3.
  • Die dritte Konfigurationseinheit C beinhaltet die Groovy-Klassen. Die Klasse K2 wird entsprechend angepasst, dass sie das Interface I2 implementiert und selbst auf das Interface I3 anstelle von K3 zugreift.

Damit dies funktioniert, können Sie die Klassen K2 und K3 nicht mehr über Konstruktoren instanziieren, sondern müssen dafür eine "neutrale" Factory verwenden. Folgende Abbildung zeigt diese Struktur der Anwendung.

 
Auflösung der Abhängigkeit durch getrenntes Kompilieren

Die Java- und Groovy-Klassen sind nun nicht mehr direkt voneinander abhängig, sondern über die Interfaces in Konfigurationseinheit A verknüpft, und die gesamte Anwendung lässt sich problemlos in der Reihenfolge A - B - C kompilieren.

Wir wollen auf die Detailfragen, die mit einer solchen Architektur verbunden sind, nicht näher eingehen, da sie nicht spezifisch für die Groovy-Einbindung sind. Denken Sie aber daran, sich rechtzeitig mit den Problemen wechselseitiger Abhängigkeiten auseinanderzusetzen, wenn Sie gemischte Java-Groovy-Anwendungen schreiben wollen.

Tipp: Um Anwendungen zu schreiben, die aus sorgfältig entflochtenen Komponenten bestehen, bietet es sich an, einen der zahlreichen verfügbaren Container zu verwenden, der nach dem Inversion-of-control-Prinzip arbeitet. Bekannte Beispiele hierfür sind das Spring-Framework und der Pico-Container.
Aktuell: Der Groovy-Compiler kann inzwischen auch Java-Quellen übersetzen. Damit lässt sich das hier beschriebene Problem lösen. Siehe http://blackdragsview.blogspot.com/2007/07/joint-compilation-in-groovy.html.