Groovy: Dynamische Objekte

Aus Wikibooks
Zur Navigation springen Zur Suche springen

Kategorien | ↑ Dynamisches Programmieren | → Das Metaobjekt-Protokoll

Zunächst müssen wir begrifflich zwischen Groovy- und Java-Objekten unterscheiden. Groovy-Objekte sind alle Objekte, deren Klassen das Interface groovy.lang.GroovyObject implementieren. Das ist bei allen Objekten der Fall, deren Klassen in der Sprache Groovy geschriebenen sind, aber natürlich können auch in Java geschriebene Klassen dieses Interface implementieren; genau dies ist bei einer Reihe der Klassen der Groovy-Laufzeitbibliothek der Fall, die damit auch zu Groovy-Objekten werden. Mit Hilfe der Klasse groovy.lang.GroovyObjectSupport ist es auch für Sie kein Problem, eine solche Klasse in Java zu schreiben. Java-Objekte sind alle übrigen Objekte, deren Klassen in Java (oder einer anderen Programmiersprache, die Java-Bytecode erzeugt) geschrieben sind und nicht GroovyObject implementieren.

Eine Groovy-Klasse von innen[Bearbeiten]

Als Beispiel schnitzen wir uns eine kleine Groovy-Klasse, die an Primitivität kaum zu überbieten ist, für unsere Untersuchungen aber vollständig ausreicht.

class MeineKlasse {
  def inhalt
}

Die Klasse hat eine untypisierte Property namens inhalt, und wir wissen, dass Groovy von selbst zwei Zugriffsmethoden hinzugeneriert, nämlich getInhalt() und setInhalt(). Wenn Groovy die Klasse übersetzt, sieht sie in Java formuliert ungefähr so aus:[1]

// Java-Äquivalent der Klasse MeineKlasse
import groovy.lang.*;
import org.codehaus.groovy.runtime.InvokerHelper;

public class MeineKlasse implements GroovyObject {
  // Property "inhalt"
  private Object inhalt;
  // Referenz auf Metaklasse
  transient MetaClass metaClass;
  // Zeitstempel
  public static Long __timeStamp = new Long(1170538070965L);
  // Getter für die Property "inhalt"
  public Object getInhalt() {
    return inhalt;
  }
  // Setter für die Property "inhalt".
  public void setInhalt(Object value) {
    inhalt = value;
  }
  // Ruft Methoden dieses Objekts auf.</nowiki>
  public Object invokeMethod(String method, Object arguments) {
    return getMetaClass().invokeMethod(this, method, arguments);
  }
  // Liest eine Property dieses Objekts aus.
  public Object getProperty(String property) {
    return getMetaClass().getProperty(this, property);
  }
  // Setzt eine Property dieses Objekts.
  public void setProperty(String property, Object value) {
    getMetaClass().setProperty(this, property, value);
  }
  // Liefert die diesem Objekt zugeordnete Metaklasse
  public MetaClass getMetaClass() {
    MetaClass meta = metaClass;
      if (meta==null) {
      meta = InvokerHelper.getMetaClass(this);
    }
    return meta;
  }
  // Setzt Metaklasse für dieses Objekt.
  public void setMetaClass(MetaClass value) {
    metaClass = value;
  }
}

Sie erkennen die konventionellen Getter- und Setter-Methoden für die Property inhalt. Diese werden ganz normal aufgerufen, wenn wir beispielsweise ein Objekt dieser Klasse in einem Java-Programm verwenden. Daher lassen sich Groovy-Objekte auch problemlos im Java-Kontext verwenden. Der ganze Rest ist aber genau das, was ein Objekt zu einem Groovy-Objekt macht. Es implementiert das Interface GroovyObject und muss daher auch die darin definierten Methoden anbieten.

Die Groovy-Laufzeitbibliothek bietet auch eine Klasse groovy.lang.GroovyObjectSupport, die standardmäßige Implementierungen der GroovyObject-Methoden enthält. Die Klasse MeineKlasse kann aber nicht einfach von GroovyObjectSupport abgeleitet werden, da wir dann nicht mehr die Möglichkeit hätten, eigene Ableitungshierarchien aufzubauen. Somit müssen diese Methoden vom Groovy-Compiler hinzugeneriert werden.

  • Die Methode invokeMethod() ist dran, wenn eine Instanzmethode des Objekts aus demselben oder einem anderen Groovy-Objekt heraus aufgerufen werden soll; dies normalerweise aber nur, wenn keine Methode mit der passenden Signatur.[2] Sie leitet den Aufruf an eine Metaklasse weiter, die entweder durch eine eigene Referenz oder, wenn diese nicht gesetzt ist, über die Groovy-interne Klasse InvokerHelper ermittelt wird.
  • Die Methoden getProperty() und setProperty() kommen immer dann ins Spiel, wenn lesend oder schreibend auf eine nicht-statische Property des Objekts zugegriffen werden soll, z.B. über einen Ausdruck in der Property-Notation wie mk.inhalt aus einem anderen Namensraum heraus.
  • Die beiden Zugriffsmethoden getMetaClass() und setMetaClass() ermöglichen es, die dem Objekt zugeordnete Metaklasse auszulesen oder zu setzen.

Die Metaklasse[Bearbeiten]

Die generierte Klasse enthält eine Referenz auf eine Groovy-Metaklasse, mit der alle zusätzlich generierten Methoden zu tun haben. Die Metaklasse ist gewissermaßen der Dreh- und Angelpunkt der dynamischen Objektorientierung in Groovy, die Schaltstelle, über die alle Methodenaufrufe und Property-Referenzen geleitet werden.

Die Metaklasse ist eine Instanz einer Klasse, die von der abstrakten Klasse groovy.lang.MetaClass abgeleitet ist und über alle Informationen bezüglich einer bestimmten Java- oder Groovy-Klasse verfügt, die Groovy benötigt, um seine dynamischen Fähigkeiten ausspielen zu können. Der Zusammenhang zwischen einer Klasse und der korrespondierenden Metaklassen-Instanz wird durch ein Registratur-Objekt (groovy.lang.MetaClassRegistry) hergestellt. Im Normalfall verwendet Groovy für jede in einem Programm auftretende Klasse genau eine Instanz der Klasse groovy.lang.MetaClassImpl; es ist aber möglich, für einzelne Klassen Instanzen anderer Ableitungen von MetaClass zu registrieren und damit das Verhalten dieser Klassen wesentlich zu verändern.

Die auf Klassen bezogene Zuordnung von Metaklassen kann auch auf Instanzebene überschrieben werden. Wie Sie im obigen Listing sehen, hat jedes Groovy-Objekt eine eigene Referenz auf die Metaklasse, die mit Hilfe von Getter- und Setter-Methoden gelesen und geschrieben werden kann. Sobald einem einzelnen Objekt eine eigene Metaklassen-Instanz zugeordnet ist, verwendet es diese und nicht mehr die Metaklassen-Instanz, die über die Registratur seiner Klasse zugeordnet ist.

Beispiele für die Möglichkeiten, die sich aus dem Austausch der Metaklasse auf Klassen- oder Instanzebene ergeben, finden Sie in Das Metaobjekt-Protokoll.

Aufruf von Methoden einer Groovy-Klasse[Bearbeiten]

Was geschieht, wenn wir aus einem Groovy-Programm heraus eine Methode der Groovy-Klasse aufrufen? Als Beispiel nehmen wir das folgende Code-Stück, das die toString()-Methode aufruft.

groovy> mk = new MeineKlasse()
groovy> println mk.toString()
MeineKlasse@123456

Dabei interessieren wir uns eigentlich nur für den Ausdruck mk.toString(), der den Aufruf enthält. Er wird vom Groovy-Compiler in Byte-Code übersetzt, der etwa dem folgenden Java-Ausschnitt entspricht:

// Java-Äquvalent für mk.toString()
org.codehaus.groovy.runtime.InvokerHelper.invokeMethod(mk,"toString",null);

Es wird also gar nicht direkt die Methode toString() der MeineKlasse-Instanz aufgerufen, sondern der Aufruf wird an eine statische Methode der internen Framework-Klasse InvokerHelper delegiert. Dieser Methode werden eine Referenz auf das Objekt, bei dem die Methode aufgerufen werden soll, der Name der Methode sowie, falls vorhanden, die Aufrufargumente als Array von Objekten mitgegeben (da wir keine Argumente haben, ist es hier einfach null).

InvokerHelper gehört zu einer Reihe von Klassen im Package org.codehaus.groovy.runtime, deren Schnittstellen nicht standardisiert sind und die hauptsächlich dazu dienen, die Menge des vom Compiler zu generierenden Codes zu minimieren. Was da im Einzelnen vor sich geht, soll uns nicht weiter interessieren. Wichtig ist aber, dass im Endeffekt folgendes geschieht:

  1. Ist das Ziel des Aufrufs eine Klasse und keine Instanz, so wird der Aufruf als statischer Methodenaufruf an die für diese Klasse registrierte Metaklasse weiterdelegiert.
  2. Ist es eine Instanz einer Java-Klasse (die nicht GroovyObject implementiert), so wird der Aufruf ebenfalls an die für diese Java-Klasse registrierte Metaklasse weiterdelegiert.
  3. Ist es ein Groovy-Objekt und implementiert die Klasse nicht GroovyInterceptable, so wird mit getMetaClass() die zutreffende Metaklassen-Instanz ermittelt und der Aufruf gleich an diese weitergeleitet.
  4. Kann die Metaklassen-Instanz den Aufruf nicht durchführen, weil bei dem Zielobjekt nichts mit passender Signatur zu finden ist, so wird direkt invokeMethod() beim Zielobjekt aufgerufen.

Hier haben wir es mit dem dritten Fall zu tun: da es sich bei mk um ein Groovy-Objekt handelt, landet der Aufruf erst einmal beim der Methode invokeMethod() dieses Objekts. Diese Methode leitet nun, wie wir oben sehen können, den Aufruf ihrerseits weiter and die Metaklassen-Instanz, die dem Objekt zugeordnet ist und die schließlich dafür sorgt, dass die Methode toString() per Reflection aufgerufen wird. Die folgende Abbildung zeigt ein vereinfachtes UML-Kollaborationsdiagramm, dass die wesentlichsten Schritte der Delegationskette für diesen Fall verdeutlicht.

Methodenaufruf als Kollaborationsdiagramm

Eh der Aufruf von toString() endlich bei dieser Methode angelangt ist, hat er also schon einen beträchtlichen Weg hinter sich. Es wird klar, dass Groovy-Programme niemals so performant sein können wie Äquivalente Java-Programme. Dafür bieten sie verschiedene Möglichkeiten, in das Geschehen einzugreifen; und mit diesen wollen wir uns im Folgenden beschäftigen.

Vorgegaukelte Methoden[Bearbeiten]

Die vom Compiler generierten Methoden sind immer als freundliches Angebot zu verstehen, das anzunehmen Sie nicht gezwungen sind. Wir prüfen dies, indem wir unsere simple Groovy-Klasse mit einer zusätzlichen invokeMethod()-Methode versehen. Sie soll nichts weiter tun, als Laut zu geben, sobald sie aufgerufen wird.

class MeineKlasse {
  def inhalt
  def invokeMethod (String name, Object args) {
    println "invokeMethod aufgerufen: $name $args"
  }
}

Nun versuchen wir verschiedene Methodenaufrufe:

groovy> mk = new MeineKlasse()
groovy> println mk.toString()
groovy> println mk.toString("Hallo")
groovy> println mk.tunix()
MeineKlasse@3bc1a1
invokeMethod aufgerufen: toString {"Hallo"}
null
invokeMethod aufgerufen: tunix {}
null

Ohne unsere selbstgemachte invokeMethod()-Methode würden wir schon bei mk.toString("Hallo") einen Laufzeitfehler bekommen, denn folgendes würde passieren: Die Laufzeitumgebung delegiert den Methodenaufruf gleich am Anfang an die zu MeineKlasse gehörende Metaklasse weiter und, da diese keine passende Methode findet, wendet sie sich dann als letzte Rettung an invokeMethod() in der Zielklasse. In der Originalversion delegiert invokeMethod() erneut an die Metaklasse, was natürlich immer noch nicht zum Ziel führen kann und schließlich die Runtime-Exception auslöst.

Unsere eigene Implementierung von invokeMethod() spart sich die Weiterleitung an die Metaklasse ‒ diese muss ja immer zu einem Fehler führen, denn invokeMethod() wird schließlich nur aufgerufen, nachdem die Metaklasse schon einmal den Methodenaufruf erfolglos probiert hat. Stattdessen gibt sie eine kleine Meldung aus. An der Ausgabe des Skripts sehen Sie zudem, dass der erste toString()-Aufruf auch korrekt ausgeführt wird. Nur anstelle der beiden anderen Aufrufe, wobei der eine ein überzähliges Argument hat und der zweite eine überhaupt nicht existierende Methode anspricht, tritt unser invokeMethod() in Aktion und zeigt an, dass es aufgerufen wurde. Da die Methode kein Ergebnis zurückliefert, gibt println() in beiden Fällen null aus.

Der typische Anwendungsfall für eigene invokeMethod()-Implementierungen sind Klassen, die sich die Syntax von Methodenaufrufen zunutze machen, um etwas ganz anderes zu bewirken. In den Groovy:Link und {{:Groovy: Link | XML} haben Sie am Beispiel der Builder bereits gesehen, welche Möglichkeiten dies eröffnet.

Interzeptoren[Bearbeiten]

Der oben dargestellte Ablauf eines Methodenaufrufs im Invoker sieht etwas anders aus, wenn die Klasse des Zielobjekts das Interface groovy.lang.GroovyInterceptable implementiert. Dies ist ein Marker-Interface, das selbst keine Methoden definiert, aber der Groovy-Laufzeitumgebung signalisiert, dass alle Methodenaufrufe an die eigene invokeMethod()-Methode geleitet werden sollen und nicht zuerst an die Metaklasse. Folgende Abbildung stellt auch diesen Ablauf grafisch dar.

Methodenaufruf bei einem Inteceptable-Objekt

Um das zu zeigen, bauen wir MeineKlasse noch etwas um, sodass sie GroovyInterceptable implementiert. Dabei ändern wir auch die println()-Anweisung in invokeMethod() so, dass direkt System.out angesprungen wird, um eine Endlosschleife zu vermeiden.

class MeineKlasse implements GroovyInterceptable {
  def inhalt
  def invokeMethod (String name, Object args) {
    System.out.println "invokeMethod aufgerufen: $name $args"
  } 
}

Das Ergebnis der interaktiven Skriptzeilen mit den drei Methodenaufrufen sieht nun anders aus.

groovy> mk = new MeineKlasse()
groovy> println mk.toString()
groovy> println mk.toString("Hallo")
groovy> println mk.tunix()
invokeMethod aufgerufen: toString {}
null
invokeMethod aufgerufen: toString {"Hallo"}
null
invokeMethod aufgerufen: tunix {}
null

In allen drei Fällen ist unser invokeMethod() aufgerufen worden, und das eigentliche toString() wurde offenbar überhaupt nicht erreicht.

Das Interface GroovyInterceptable ist natürlich nicht dazu gedacht, alle Methodenaufrufe abzublocken. Vielmehr ermöglicht es dem Programmierer, Interzeptoren zu bauen, das sind Klassen, die jeden Methodenaufruf abfangen, um irgendeine Querschnittsaktion auszuführen. Typische Anwendungsfälle dafür sind Protokollierung und Zugriffskontrollen. An einem simplen Beispiel wollen wir zeigen, wie dies im Prinzip funktionieren kann.

Wir definieren uns eine Klasse namens ProtokollObjekt. Sie ist als Basisklasse für beliebige andere Klassen gedacht, deren Methodenaufrufe automatisch protokolliert werden sollen. Wir definieren sie aber nicht ausdrücklich als abstract, da sie keine abstrakten Methoden enthält und prinzipiell instanziierbar ist.

import java.util.logging.*
class ProtokollObjekt implements GroovyInterceptable {
  def invokeMethod (String name, Object args) {
    Class klasse = metaClass.invokeMethod(this,"getClass")
    String klassenname = klasse.name
    Logger logger = Logger.getLogger (klassenname)
    logger.info "Aufruf $klassenname.$name $args"
    try {
      def ergebnis = metaClass.invokeMethod (this,name,args)
      logger.info "Ergebnis $klassenname.$name: $ergebnis"
      return ergebnis
    } catch (Throwable thr) {
      logger.info "Exception in $klassenname $name: $thr.message"
      throw thr
    }
  }
}

Diese Basis-Implementierung von invokeMethod() lässt sich einen Logger für das aktuelle Objekt geben, und protokolliert darin den Aufruf und die Rückkehr aus der gewünschten Methode. Wenn eine Exception auftritt, wird diese abgefangen und auch entsprechend protokolliert, bevor sie erneut ausgelöst wird. Wir müssen in dieser Methode sorgfältig darauf achten, dass keine eigenen Instanzmethoden aufgerufen werden, da es sonst unausweichlich zu einer Endlosschleife kommt. Aus diesem Grund können wir uns auch den aktuellen Klassennamen nicht direkt über getClass() holen, sondern müssen damit die Metaklasse beauftragen.

Damit das Protokollobjekt zeigen kann, was es kann, entfernen wir invokeMethod() aus MeineKlasse und leiten die Klasse jetzt von ProtokollObjekt ab.

class MeineKlasse extends ProtokollObjekt {
  def Inhalt
}

Wie das Ganze zusammen läuft, probieren wir wieder interaktiv aus.

groovy> mk = new MeineKlasse()
groovy> println mk.toString()
groovy> println mk.toString("Hallo")
06.02.2007 20:30:47 sun.reflect.NativeMethodAccessorImpl invoke0
INFO: Aufruf MeineKlasse.toString {}
06.02.2007 20:30:47 sun.reflect.NativeMethodAccessorImpl invoke0
INFO: Ergebnis MeineKlasse.toString: MeineKlasse@1c80b01
MeineKlasse@1c80b01
06.02.2007 20:30:47 sun.reflect.NativeMethodAccessorImpl invoke0
INFO: Aufruf MeineKlasse.toString {"Hallo"}
06.02.2007 20:30:47 sun.reflect.NativeMethodAccessorImpl invoke0
INFO: Exception in MeineKlasse toString: No signature of method: MeineKlasse.toString() is applicable for argument types: 

(java.lang.String) values: {"Hallo"}

06.02.2007 20:30:47 sun.reflect.NativeMethodAccessorImpl invoke0
INFO: Aufruf MeineKlasse.toString {"Hallo"}
06.02.2007 20:30:47 sun.reflect.NativeMethodAccessorImpl invoke0
INFO: Exception in MeineKlasse toString: No signature of method: MeineKlasse.toString() is applicable for argument types: 

(java.lang.String) values: {"Hallo"}

Caught: groovy.lang.MissingMethodException: No signature of method: MeineKlasse.toString() is applicable for argument types: 

(java.lang.String) values: {"Hallo"}

    at ProtokollObjekt.invokeMethod(LoggerBeispiel.groovy:10)
    at LoggerBeispiel.run(LoggerBeispiel.groovy:31)
    at LoggerBeispiel.main(LoggerBeispiel.groovy)

Zunächst erzeugen wir ein Objekt von MeineKlasse und nehmen dann zwei Methodenaufrufe vor. Der erste wird ausgeführt, der zweite jedoch nicht, weil die Methode mit einem Argument nicht definiert ist. Sie sehen, dass die Aufrufe und Ergebnisse bzw. Fehler brav protokolliert werden. Der fehlerhafte Aufruf erfolgt sogar zweimal; das liegt daran, dass der Grovy-Invoker mehrere Versuche unternimmt, eine passende Methode zu finden.

Info: Im Ernstfall würden Sie die Meldungen des ProtokollObjekt lieber mit logger.fine() als mit logger.info() ausführen. Es empfiehlt sich aber nicht, mit logger.finer() oder den vordefinierten Logger-Methoden entering(), exiting() und throwing() zu arbeiten. Andernfalls bekommen Sie das Problem, dass Sie den Log-Handler auf die Stufe FINER setzen müssen und gleich eine Fülle von Meldungen zu sehen bekommen, die Groovy beim Aufruf jeder Methode generiert.</nowiki>

An dem ProtokollObjekt können Sie gut erkennen, wie Sie mit Hilfe der invokeMethod()-Methode und dem Interface GroovyInterceptable Querschnittsfunktionalitäten implementieren können. Allerdings gibt es hier zwei kleine Probleme, die wir Ihnen nicht vorenthalten wollen: Erstens scheitert der Ansatz, sobald Sie eine Klasse von einer anderen Klasse erben lassen wollen, beispielsweise einer Standard-Java-Klasse, die nicht vom ProtokollObjekt abgeleitet ist, da Groovy genau wie Java keine Mehrfachvererbung kennt. Zweitens können Sie keine Aufrufe von statischen Methoden und Konstruktoren abfangen, denn da haben Sie keine Instanz in der Hand, deren invokeMethod() aufgerufen werden könnte. Weiter unten werden Sie im Abschnitt ##Meta-Objekt-Protokoll## sehen, wie eine Lösung aussehen kann, die diese Nachteile nicht hat, dafür aber weitaus undurchsichtiger ist.

Dynamische Properties[Bearbeiten]

Beim Übersetzen einer Groovy-Klasse erzeugt der Compiler auch zwei Methoden getProperty() und setProperty(), mit denen Properties des Objekts anhand ihres Namens abgerufen werden können. Genau wie invokeMethod() leiten sie die Aufrufe an die Metaklasse des Objekts weiter, die im Normalfall versucht, die korrespondierenden Getter oder Setter ausfindig zu machen. Im Unterschied zu invokeMethod() werden die Property-Methoden aber von der Laufzeitumgebung immer aufgerufen und nicht nur, wenn das Marker-Interface gesetzt ist. Sie können diese Methoden ähnlich einsetzen wie die optionalen Methoden get() und set(), die wir im Kapitel Objekte behandelt haben, um mit dynamischen Properties zu arbeiten. Da getProperty() und setProperty() aber Vorrang vor der standardmäßigen Behandlung von Properties haben, brauchen Sie hier keine Sorge zu haben, dass es Namenskonflikte mit zufällig vorhandenen Gettern oder Settern gleichen Namens gibt.

Die beiden generischen Property-Zugriffsmethoden sind besonders praktisch, wenn Sie eine Wrapper-Klasse zum Kapseln einer vorhandenen Java-API bauen möchten, die nicht den Regeln für JavaBeans folgt, also keine standardmäßigen Getter und Setter hat, oder die dynamische Elemente hat aber keine Map implementiert.

Ein Beispiel für eine solche API findet man in JDBC. Das Interface java.sql.ResultSet wird von den Herstellern von JDBC-Treibern mit eigenen Klassen implementiert, die Ergebnismengen zu SQL-Abfragen liefern. Dieses Interface definiert eine Vielzahl von Methoden zum Navigieren von Zeile zu Zeile und für den Zugriff auf die Spalten, die recht umständlich zu verwenden sind. Unsere simple Klasse ResultSetWrapper hält eine Referenz auf ein ResultSet-Objekt und leitet einfach alle lesenden Property-Zugriffe an diese weiter.

import java.sql.*
class ResultSetWrapper {
  ResultSet data // Aktuelles ResultSet
  // Konstruktor übernimmt einen Resultset
  def ResultSetWrapper(ResultSet resultSet) {
    data = resultSet
  }
  // Properties sind Spalten des ResultSet
  Object getProperty(String property) {
    data.getObject(property);
  }
}

Die Klasse definiert einen Konstruktor, dem eine ResultSet-Instanz übergeben werden muss, und implementiert die Methode GroovyObject-Methode getProperty() derart, dass alle Aufrufe an die getObject()-Methode des ResultSet weitergeleitet werden. Diese Methode ist intelligent genug, alle Datenbankfelder in äquivalente Java-Objekte umzuwandeln, so dass die Ergebnisse der Property-Abfragen automatisch immer den richtigen Typ haben.

Angenommen, wir hätten einen ResultSet namens resultSet aus einer Datenbankabfrage mit den Spalten vorname und zuname, dann würde wir ihn normalerweise folgendermaßen ausgeben:

while (resultSet.next()) {
  println "${resultSet.getObject('vorname')} ${resultSet.getObject('zuname')}"
}

Mit unserer Wrapper-Klasse ist dies schon mal etwas übersichtlicher, da wir auf die Tabellenspalten wie auf Properties zugreifen können.

data = new ResultSetWrapper(resultSet)
while (data.next() {
  println "$data.vorname $data.zuname"
} 

Dieses Beispiel mag noch nicht allzu beeindruckend sein; im folgenden Abschnitt wollen wir jedoch zeigen, wie Sie mit wenigen Programmzeilen eine komplette Kapsel erstellen können, die einen bequemen Zugriff nach Groovy-Art auf alle Möglichkeiten des ResultSet bietet.

Projekt: ein kompletter Wrapper für ResultSet[Bearbeiten]

Wir wollen die JDBC-Ergebnismenge hier als Beispiel für eine ältere Java-API betrachten, die sich nicht an die in Java inzwischen verbreiteten Muster hält. Sie ist nur relativ umständlich zu verwenden und man muss sich erst mal orientieren, welche Methoden für welchen Zweck vorgesehen sind. Dazu bauen wir eine Groovy-Klasse, die folgende Eigenschaften hat:

  • Sie kapselt die Klasse ResultSet vollständig, macht also deren Funktionalität komplett verfügbar, ohne dass ein Durchgriff des Anwenders auf das gekapselte Objekt erforderlich ist.
  • Sie ermöglicht den lesenden Zugriff auf die Spalten einer Abfrage wie auf Properties (dies kennen Sie schon aus dem vorigen Abschnitt).
  • Sie implementiert das Interface java.util.Iterator, so dass sie in for-Schleifen durchlaufen werden kann.
  • Sie verfügt über eine Schleifenmethode eachRow(), die für jede Ergebniszeile die übergebene Closure aufruft.

Da die Groovy-Laufzeitbibliothek bereits eine umfangreiche und ausgefeilte Lösung für den Zugriff auf Datenbanken enthält (siehe Datenbanken), ist es nicht besonders sinnvoll, eine solche Lösung selbst zu programmieren. Das Beispiel soll Ihnen in erster Linie zeigen, welche Möglichkeiten Groovy zum Kapseln solcher vorhandenen APIs zur Verfügung stellt und wie Sie dabei vorgehen können.

Die in folgendem Beispiel erweiterte Version der Klasse ResultSetWrapper enthält bereits alles, was zur Lösung der gestellten Aufgabe erforderlich ist. Die Erläuterung folgt sogleich.

import java.sql.*

class ResultSetWrapper implements Iterator {
 
    private ResultSet data    // Aktuelles ResultSet
 
    // Konstruktor übernimmt einen Resultset
    def ResultSetWrapper(ResultSet resultSet) {
        data = resultSet
    }

    // Properties sind Spalten des ResultSet
    Object getProperty(String property) {
        data.getObject(property)
    }

    // Setzen von Properties nicht implementiert.
    void setProperty(String property, Object value) {
        throw new UnsupportedOperationException()
    }
 
    // Implementierung von next() aus Iterator
    // Schaltet zur nächsten Zeile
    def next() {
        if (data.next()) return this
        throw new NoSuchElementException()
    }

    // Implementierung von hasNext() aus Iterator
    // Prüft, ob es weitere Zeilen gibt
    boolean hasNext() {
        return ! data.isLast()
    }

    // Keine Implementierung von remove() aus Iterator
    void remove() {
        throw new UnsupportedOperationException()
    }

    // Methode zum Durchlaufen des ResultSet mit einer
    // Closure
    void eachRow (Closure closure) {
        if (! data.isBeforeFirst()) {
            data.beforeFirst()
        }
        while (data.next()) {
            closure(this)
        }
    }

    // Leitet alle Aufrufe von undefinierten Methoden
    // an das ResultSet weiter.
    def invokeMethod (String methodName, Object args) {
        return data."$methodName"(*args)
    }
}

Die Methode getProperty() kennen Sie schon von oben, sie lässt sich vom ResultSet den Wert geben, dessen Spaltenname dem Property-Namen entspricht. Das Pendant zum Schreiben setProperty() ist nur der Vollständigkeit wegen implementiert und liefert eine Exception. Wir verzichten darauf, die standardmäßige GroovyObject-Methode getProperties() so zu überschreiben, dass diese alle Properties liefert, die auch die getProperty()-Methode findet. Das ist vielleicht nicht ganz konsistent, aber wir befinden uns damit im Einklang mit der Groovy-Standardbibliothek, in der diese Übereinstimmung ebenfalls nicht immer gegeben ist. ##Na, ob das eine gute Rechtfertigung ist? ;-)##

Die drei Methoden next(), hasNext() und remove() sind zur Implementierung des Iterator-Interface erforderlich. Sie bedienen sich der äquivalenten, aber anders definierten Methoden des ResultSet. Allerdings wird remove() für unsere Zwecke nicht benötigt und löst daher vorschriftsmäßig die UnsupportedOperationException aus. Die Implementierung von next() ist etwas unkonventionell, da die Methode einfach das ResultSet auf die nächste Zeile schaltet und den ResultSetWrapper selbst zurückgibt. Dies reicht aber aus, um for-Schleifen über den ResultSetWrapper zu ermöglichen.

Eine zweite Möglichkeit zum Durchlaufen der Daten bietet die Methode eachRow() der man eine Closure übergibt, die für jede Ergebniszeile einmal aufgerufen wird. Als Argument erhält die Closure auch hier wieder den ResultSetWrapper selbst, dessen Spaltenwerte sich ja wie Properties abfragen lassen.

Schließlich ist noch die Methode invokeMethod() zu erwähnen, mit der wir die Anforderung erfüllen, dass das ResultSet vollständig gekapselt werden soll. Wie wir weiter oben gezeigt haben, leitet diese Methode alle Methodenaufrufe, die ResultSetWrapper, zu denen es im ResultSetWrapper selbst keine passende Methode gibt, an den gekapselten ResultSet weiter. Und schon steht die gesamte Funktionalität des ResultSet auch dem Benutzer des ResultSetWrapper zur Verfügung, ohne auf das ResultSet selbst durchgreifen zu müssen. Daher können wir auch die Variable data hier ruhigen Gewissens auf private stellen ‒ sie wird von außen nicht benötigt.[3]

Um das Ganze zu testen benötigen wir eine Datenbank, denn nur diese kann uns einen ResultSet liefern. Das ist nicht schwierig, wenn Sie sich einfach das aktuelle HSQLDB-Datenbanksystem von http://hsqldb.org/ herunterladen und daraus die Bibliotheksdatei hsqldb.jar in das Verzeichnis ~/.groovy/lib unterhalb Ihres Home-Verzeichnisses platzieren, so dass sie für Groovy im Klassenpfad verfügbar ist. Unter Windows XP lautet der vollständige Pfad also beispielsweise C:\dokumente und einstellungen\krause\.groovy\lib\hsqldb.jar).

Mit ein paar Zeilen Code öffnen wir eine Datenbankverbindung, legen eine neue In-Memory-Datenbank an, definieren in ihr eine neue Tabelle an und füllen diese mit ein paar Daten. Dann lesen wir diese Daten einmal in einer for-Schleife und einmal über die eachRow()-Methode aus. Beispiel ## zeigt dies anhand eines Testskripts.

import java.sql.*

// Datenbanktreiber laden (unter Java 6.0 nicht mehr nötig)
Class.forName 'org.hsqldb.jdbcDriver'
// Verbindung herstellen 
conn = DriverManager.getConnection('jdbc:hsqldb:mem:aname', 'sa', );
stmt = conn.createStatement()

// Zwei Closures zur Vereinfachung von Abfragen
execute = { stmt.execute it }     // Einfacher Befehl
query = { stmt.executeQuery it}   // Abfrage mit Ergebnis

// Tabelle anlegen und mit zwei Zeilen füllen
execute('CREATE TABLE daten (name VARCHAR, geboren DATE, punkte INTEGER)')
execute("INSERT INTO daten values ('Klaus Meier','1972-12-01',100)")
execute("INSERT INTO daten values ('Otto Müller','1966-02-28',99)")

// Erste Testschleife
println 'Test eachRow()'
rsw = new ResultSetWrapper(query('SELECT * FROM daten'))
rsw.eachRow { println "$it.name - $it.geboren - $it.punkte" }
rsw.close()

// Zweite Testschleife
println 'Test for-Schleife'
rsw = new ResultSetWrapper(query('SELECT * FROM daten'))
for (r in rsw) {
    println r.getString('name')+', '+r.getDate('geboren')+
               ', '+r.getInt('punkte')
}
rsw.close()

// Datenbankverbindung schließen
conn.close()

Wenn Sie dieses Skript im selben Verzeichnis wie ResultSetWrapper.groovy ablegen und es dann aufrufen, sollte das Ergebnis so aussehen:

> groovy ResultSetWrapper.groovy
Test eachRow()
Klaus Meier - 1972-12-01 - 100
Otto Müller - 1966-02-28 - 99
Test for-Schleife
Klaus Meier, 1972-12-01, 100
Otto Müller, 1966-02-28, 99

Beachten Sie die Aufrufe der Originalmethoden des ResultSet an unserer Klasse: getString(), getDate(), getInt() und rsw.close(). Allesamt sind sie nicht im ResultSetWrapper definiert. Dies ist auch nicht nötig, denn invokeMethod() leitet ja alle Methodenaufrufe, die das ResultSetWrapper-Objekt nicht bedienen kann, an das gekapselte Objekt weiter. Und so funktionieren die Methoden genau so, als würde man sie am ResultSet direkt aufrufen.

Die Klasse Proxy[Bearbeiten]

Um solche Konstellationen noch etwas zu vereinfachen, bietet Groovy in seiner Laufzeitbibliothek eine Klasse namens groovy.util.Proxy an, in der die Weiterleitung der Methodenaufruf an ein anderes Objekt schon eingebaut ist und von dem solche Klassen wie das obige ResultSetWrapper bequem abgeleitet werden können. Das folgende Klasse ResultSetWrapperProxy macht exakt dasselbe wie der obige ResultSetWrapper, ist aber auf Basis der Proxy-Klasse implementiert.

import java.sql.* import groovy.util.Proxy as GroovyProxy

class ResultSetWrapperProxy extends GroovyProxy {

    // Konstruktor übernimmt einen Resultset uns setzt
    // ihn als adaptee des Proxy.
    def ResultSetWrapperProxy(ResultSet resultSet) {
        setAdaptee(resultSet)
    }

    // Properties sind Spalten des ResultSet
    Object getProperty(String property) {
        getAdaptee().getObject(property);
    }

    // Setzen von Properties nicht implementiert.
    void setProperty(String property, Object value) {
        throw new UnsupportedOperationException()
    }

    // Implementierung von next() aus Iterator
    // Schaltet zur nächsten Zeile
    def next() {
        if (getAdaptee().next()) return this
        throw new NoSuchElementException()
    }

    // Implementierung von hasNext() aus Iterator
    // Prüft, ob es weitere Zeilen gibt
    boolean hasNext() {
        return ! getAdaptee().isLast()
    }

    // Keine Implementierung von remove() aus Iterator
    void remove() {
        throw new UnsupportedOperationException()
    }

    // Methode zum Durchlaufen des ResultSet mit einer
    // Closure
    void eachRow (Closure closure) {
        if (! getAdaptee().isBeforeFirst()) {
            getAdaptee().beforeFirst()
        }
        while (getAdaptee().next()) {
            closure(this)
        }
    }
}

Statt des Feldes data benutzen wir hier eine in Proxy definierte Property namens adaptee als Referenz auf den ResultSet, an den die Methodenaufrufe automatisch weitergeleitet werden. Und die eigene Implementierung von invokeMethod() können wir einfach weglassen, weil deren Aufgabe vom Proxy übernommen wird. Wir müssen hier ein wenig aufpassen, dass wir adaptee nicht in der Property-Notation ansprechen, sondern immer ordentlich über die Getter und Setter, denn die Property-Zugriffe werden ja alle als Feld-Anfragen zum ResultSet geleitet, was hier natürlich nicht beabsichtigt ist.

Sie können diese Klasse ebenfalls mit dem Skript ResultSetWrapperTest.groovy ausprobieren, Sie müssen nur an einer Stelle ResultSetWrapper durch ResultSetWrapperProxy ersetzen. Das Ergebnis ist genau dasselbe.

Zwei Anmerkungen noch zur Implementierung der Klasse ResultSetWrapperProxy. Erstens ist die import-Anweisung für groovy.util.Proxy mit dem Alias GroovyProxy versehen. Eigentlich ist der ausdrückliche Import der Klassen im Package groovy.util nicht erforderlich, da es aber einen Namenskonflikt mit der ebenfalls implizit importierten Klasse java.net.Proxy geben kann, müssen wir angeben, was wir meinen. Zweitens fällt Ihnen vielleicht auf, dass die implements-Klausel für Iterator fehlt, die wir oben beim ResultSetWrapper noch angegeben haben. Tatsächlich hätten wir sie oben auch nicht benötigt, denn für die for-Schleife in Groovy ist allein wichtig, dass die Methoden next() und hasNext() vorhanden sind, nicht aber die formale Implementierung des Interface. Ein Beispiel dafür, dass Interfaces für Groovy eine weit weniger wichtige Rolle spielen als für Java.

Kategorien | ↑ Dynamisches Programmieren | → Das Metaobjekt-Protokoll


  1. Wenn Sie den kompilierten Code mit einem Java-Decompiler rückübersetzen, sieht das Ergebnis um einiges komplizierter aus, entspricht aber in der Funktionalität etwa dem hier wiedergegebenen Java-Listing.
  2. Als Signatur bezeichnet man die Kombination aus Methodennamen und Parametertypen, die eine Methode eindeutig bezeichnet.
  3. Ehrlicherweise müssen wir zugeben, dass die Funktionalität des ResultSet doch nicht vollständig verfügbar ist: die Methode next() wird vom ResultSetWrapper durch eine eigene Methode mit anderer Semantik verdeckt. In diesem Fall lässt es sich verschmerzen, da das verdeckte next() nicht mehr benötigt wird; das Beispiel erinnert uns aber daran, dass wir bei solchen Wrapper-Konstruktionen wie hier auf möglicherweise verdeckte Member des gekapselten Objekts achten müssen.