In fast jedem Java-Projekt wird Apache Ant als Build-Tool eingesetzt, mit dem diverse komplexe Aktivitäten im Zusammenhang mit der Software-Entwicklung automatisiert werden können. Ant umfasst eine Vielzahl vordefinierter, als Task bezeichnete Aktionen, die mit Hilfe von XML-Dateien konfiguriert und zu Targets gruppiert werden, zwischen denen wiederum Abhängigkeiten bestehen können. Weitere Tasks kann man mit recht übersichtlichem Aufwand selbst erstellen; Hersteller von Tools, deren Integration in Build-Prozesse sinnvoll ist, liefern sie häufig gleich fix und fertig mit.

Da Groovy als Skriptsprache sich, wie gesagt, besonders gut dazu eignet, automatisierte Prozesse im Java-Umfeld zu steuern, ist Ant bereits in der Groovy-Laufzeitbibliothek enthalten. Dazu gibt es einige fertige Tasks zur Erweiterung von Ant und eine Klasse namens AntBuilder, mit der sich Build-Prozesse ausgesprochen elegant steuern lassen.

Um diesen Abschnitt verstehen zu können, sollten Sie sich etwas mit Ant auskennen. Ausführliche (englischsprachige) Informationen erhalten Sie auf den Projektseiten von Ant unter http://ant.apache.org/manual. Oder lesen Sie beispielsweise das Buch Ant ‒ kurz & gut von Stefan Edlich und Jörg Staudemeyer (O'Reilly-Verlag).

Ant-Tasks für Groovy

Bearbeiten

Klar, dass Sie Ant auch in größeren Groovy-Projekten einsetzen möchten (für einfache Skripte, die Sie schreiben und gleich ausführen, gibt es natürlich weniger Sinn). Um Ihnen dies zu ermöglichen, stellt Groovy einige dafür erforderliche Tasks zur Verfügung.

Um diese in einem Build-Skript einsetzen zu können, müssen Sie sie zunächst deklarieren. Dies können Sie beispielsweise für den groovy-Task mit dem folgenden Stück XML-Code tun, den Sie vor der erstmaligen Verwendung dieses Tasks einfügen sollten.

<path id="GROOVYPATH"
  location="${ENV.GROOVY_HOME}/embeddable/groovy-all-1.1.jar" />
<taskdef name="groovy"
  classname="org.codehaus.groovy.ant.Groovy"
  classpathref="GROOVYPATH"/>

Den neu definierten Task können Sie nun innerhalb beliebiger Targets dazu verwenden, ein Groovy-Skript direkt auszuführen. Beispielsweise so:

<target name="rungroovy">
  <groovy src="BeispielSkript.groovy"/>
</target>

Im Folgenden wollen wir Ihnen die von Groovy mitgelieferten Ant-Tasks mit ihren Attributen vorstellen, soweit sie in üblichen Build-Prozessen üblicherweise Anwendung finden. (Es gibt noch einige weitere Tasks und Attribute, die aber eher für Spezialprobleme im Rahmen der Groovy-Entwicklung verwendet werden.

Der Task <groovy>

Bearbeiten

Führt das angegebene Skript direkt aus. Zu dem Task gibt es die folgenden Attribute:

append
Boolescher Wert; legt fest, ob die Standardausgabe an die mit dem Attribut output benannte Datei angefügt wird oder sie ob die Datei ersetzen soll soll. Nur sinnvoll, wenn das Attribut output gesetzt ist. Der Vorgabewert ist false, d.h. wenn nichts angegeben ist, wird die Datei überschrieben.
classpath
Klassenpfad, der beim Laden des Skripts sowie der von ihm verwendeten Skripts und Klassen verwendet werden soll.
classpathref
Name eines Klassenpfades, der an anderer Stelle des Build-Skripts definiert worden ist.
output
Name einer Datei, in die alle Standardausgaben geschrieben werden sollen. Wenn dieses Attribut nicht gesetzt ist, erscheinen die Ausgaben im Ant-Protokoll.
src
Name der auszuführenden Groovy-Quelldatei. Muss angegeben sein, wenn sich das Skript nicht als Text im Rumpf des Tags befindet.
stacktrace
Boolescher Wert, der angibt, ob bei Übersetzungsfehlern ein vollständiger Stacktrace ausgegeben werden soll. Der Vorgabewert ist false.

Das Skript kann entweder durch einen Dateinamen im Attribut src angegeben sein, oder sich als Klartext zwischen dem Anfangs- und dem End-Tag befinden.

Anstelle des srcdir-Attributes können auch ein oder mehrere Pfade für die Quelldateien als innere <src>-Tags angegeben werden. Dasselbe gilt für den Klassenpfad, der in Form von inneren <classpath>-Tags spezifiziert werden kann.

In einem durch diesen Task ausgeführten Skript stehen einige spezielle Skriptvariablen zur Verfügung:

  • ant verweist auf einen frisch instanziierten AntBuilder so dass Sie hier Groovy-gesteuerte Ant-Tasks direkt aufrufen können (siehe weiter unten).
  • project beinhaltet das aktuelle Ant-Projekt als Objekt.
  • properties ist eine Map mit den Ant-Properties des aktuellen Projekts.
  • target ist das aktuelle Ant-Target als Objekt.
  • task verweist auf die Instanz des Groovy-Tasks.

Der Task <groovyc>

Bearbeiten

Übersetzt Groovy-Quelldateien in Java-Klassen analog zum Ant-Task javac. Dabei wird eine Quelldatei nur dann übersetzt, wenn es nicht schon eine entsprechende Klassendatei neueren Datums gibt. Der Task groovyc kennt diese Attribute:

classpath
Klassenpfad, der beim Kompilieren verwendet werden soll.
classpathref
Name eines Klassenpfades, der an anderer Stelle des Build-Skripts definiert worden ist.
destdir
Zielverzeichnis für die zu erzeugenden .class-Dateien. Wenn nicht angegeben, wird das aktuelle Verzeichnis verwendet.
encoding
Kodierung der Quelldateien. Wenn nicht angegeben, wir die Standardkodierung der virtuellen Maschine verwendet.
failonerror
Boolescher Wert, der angibt, ob der Build-Prozess abbrechen soll, wenn beim Kompilieren eines Quellprogramms ein Fehler auftritt. Vorgabewert ist true.
listfiles
Boolescher Wert, der angibt, ob die Namen der zu übersetzenden Dateien im Protokoll ausgegeben werden soll. Vorgabewert ist false.
proceed
Das Gegenteil von failonerror. Es sollte nur eines der Attribute proceed und failonerror angegeben sein, andernfalls ist das Ergebnis undefiniert.
srcdir
Wurzelverzeichnis der zu kompilierenden Quelldateien. Muss angegeben sein, wenn es keine inneren <src>-Elemente gibt.
stacktrace
Boolescher Wert, der angibt, ob bei Compiler-Fehlern ein vollständiger Stacktrace ausgegeben werden soll. Vorgabewert ist false.
verbose
Boolescher Wert, der angibt, ob der Compiler Einzelheiten zum Fortschritt seiner Tätigkeit melden soll. Vorgabewert ist false.

Anstelle des srcdir-Attributes können auch ein oder mehrere Pfade für die Quelldateien als innere <src>-Tags angegeben werden. Dasselbe gilt für den Klassenpfad, der in Form

Der Task <groovydoc>

Bearbeiten

Dieser Task erzeugt eine Quellcode-Dokumentation im Stil von JavaDoc aus den in den Programmen enthaltenen Dokumentationskommentaren. Zu diesem Task gibt es die folgenden Attribute:

destdir
Legt das Zielverzeichnis an, in dem die HTML-Seiten für die Dokumentation abgelegt werden sollen.
packagenames
Komma-separierte Liste mit den Namen der zu verarbeitenden Packages. Wenn an der Stelle des letzten Pfadelements eines Package-Namens eine Stern angegeben ist (z.B. groovy.util.*), so werden alle unterliegenden Packages verarbeitet. Wenn das Attribut nicht gesetzt ist, werden alle Packages durchlaufen.
private
Boolescher Wert; legt fest, ob auch private Klassen und Klassen-Member berücksichtigt werden sollen. Vorgabe ist false.
sourcepath
Wurzelverzeichnis für die Quelldateien. Muss angegeben sein.
windowtitle
Legt den Titel der zu generierenden HTML-Dateien fest.

Ant mit Groovy statt XML

Bearbeiten

So perfekt Ant mit seinem enormen Funktionsumfang und seiner Flexibilität als Mittel für Builds und sonstige automatisierte Prozesse auch ist, so gibt es doch ein gravierendes Problem: Die Build-Skripte werden in XML formuliert, einer Sprache, die sich zwar hervorragend für statische Konfigurationsdaten eignet. Automatisierte Prozesse sind aber nun einmal dynamisch, und da wird es mit XML schnell kompliziert und unübersichtlich, wenn komplexere Abläufe gesteuert werden müssen. Die Build-Skripte verfügen zwar über Sprachmittel, mit denen sich Abhängigkeiten und Verzweigungen formulieren lassen, sobald es aber über sehr einfache Ablaufstrukturen hinausgeht, gerät man rasch an die Grenzen des Möglichen oder zumindest des Nachvollziehbaren. In der Praxis gibt es daher häufig eine Mischung aus Ant- und Shell-Skripten zur Steuerung von Build-Vorgängen, um die Vielfalt der Möglichkeiten von Ant mit einer verständlichen Ablauflogik zu verbinden. Dies hat aber wieder den Nachteil, dass zwei ganz unterschiedliche Sprachmittel beherrscht werden müssen, und dass die resultierenden Skripte in der Regel nicht mehr plattformneutral sind.

Für dieses Problem bietet Groovy eine äußerst elegante Lösung an, bei der prozedurale und deklarative Elemente harmonisch miteinander verknüpft werden können. Sie kennen bereits die verschiedenen Builder in Groovy, mit deren Hilfe sich in quasi-deklaratorischer Weise hierarchische Strukturen aufbauen lassen, seien es XML-Dokumente, Objektbäume oder die Elemente einer Bedienoberfläche. Einen solchen Builder gibt es auch für Ant; er ermöglicht Ihnen, die Aufrufe von Ant-Tasks in normale Groovy-Skripte einzubetten. Das funktioniert nach folgendem Schema:

ant = new AntBuilder()
ant.echo "Dies ist Ant"

In der ersten Zeile erzeugen wir eine Instanz des AntBuilder, und in der zweiten führen wir einen Ant-Task namens <echo> aus, der den angegeben String in das Ant-Protokoll schreibt. Geben Sie die beiden Zeilen ohne jede weitere Vorbereitung beispielsweise in eine groovysh ein, und Sie sehen, dass tatsächlich die Ausgabe von Ant erscheint:

[echo] Dies ist Ant.

Nun ist die Wirklichkeit natürlich komplizierter. Wie wir in Groovy einen etwas komplexeren Build-Prozess steuern, wollen wir an einem realitätsnahen Lehrbuchbeispiel eines Ant-Skripts zeigen. Es dient dazu, in einem gemischten Java- und Groovy-Projekt folgende Aufgaben auszuführen, für die es jeweils ein Target gibt:

  • Target clean: Alle Zielverzeichnisse mit ihren Inhalten löschen.
  • Target buildjava: Alle Java-Quelldateien in ein Verzeichnis classes kompilieren.
  • Target buildall: Alle Groovy-Quelldateien in das classes-Verzeichnis kompilieren. Setzt voraus, dass zuvor buildjava gelaufen ist, da die Groovy-Klassen von den Java-Klassen abhängig sind.
  • Target runjava: Führt das Programm aus, indem es die aus einer Groovy-Quelldatei kompilierte Main-Klasse aufruft. Setzt buildall voraus.
  • Target rungroovy: Führt das Programm aus, indem es die Main-Klasse als Groovy-Skript startet. Setzt nur buildjava voraus, denn die Groovy-Klassen müssen nicht übersetzt sein.
  • Target dist: Erstellt aus allen kompilierten Klassen eine JAR-Datei und kopiert es zusammen mit allen übrigen benötigten Java-Bibliotheken in ein Verzeichnis dist. Setzt clean und buildall voraus.

Folgendes Beispiel zeigt das entsprechende Build-Skript.

<?xml version="1.0"?>
<project name="BeispielProjekt" basedir="." default="buildall">

  <property environment="ENV"/>
  <property name="JAVA_SOURCE" value="src"/>
  <property name="GROOVY_SOURCE" value="src-groovy"/>
  <property name="LIB" value="lib"/>
  <property name="BUILD" value="classes"/>
  <property name="DIST" value="dist"/> 
  <property name="APPNAME" value="beispiel"/>
  <property name="MAINCLASS" value="GroovyKlasse"/>
  <property name="VERSION" value="0.1-beta-6"/>

  <path id="MAINPATH">
    <fileset dir="${LIB}">
      <include name="*.jar"/>
    </fileset>
    <pathelement path="${BUILD}"/>
  </path>

  <path id="GROOVYPATH"
    location="${ENV.GROOVY_HOME}/embeddable/groovy-all-1.1.jar" />

  <taskdef name="groovyc"
    classname="org.codehaus.groovy.ant.Groovyc"
    classpathref="GROOVYPATH" />
  <taskdef name="groovy"
    classname="org.codehaus.groovy.ant.Groovy"
    classpathref="GROOVYPATH"/>

  <target name="clean">
    <delete dir="${BUILD}"/>
    <delete dir="${DIST}"/>
  </target>

  <target name="buildjava">
    <mkdir dir="${BUILD}"/>
    <javac destdir="${BUILD}" debug="true" failonerror="true">
      <src path="${JAVA_SOURCE}"/>
      <classpath refid="MAINPATH"/>
    </javac>
  </target>

  <target name="buildall" depends="buildjava">
    <groovyc destdir="${BUILD}" srcdir="${GROOVY_SOURCE}" classpathref="MAINPATH"/>
  </target>

  <target name="runjava" depends="buildall">
    <java classname="${MAINCLASS}" classpathref="MAINPATH"/>
  </target>

  <target name="rungroovy" depends="builjava">
    <groovy src="${GROOVY_SOURCE}/${MAINCLASS}.groovy" classpathref="MAINPATH"/>
  </target>

  <target name="dist" depends="clean, buildall">
    <mkdir dir="${DIST}"/>
    <jar basedir="${BUILD}" destfile="${DIST}/${APPNAME}-${VERSION}.jar" />
    <copy todir="${DIST}">
      <fileset dir="${LIB}" includes="**/*.jar" />
    </copy>
  </target> 

</project>

Am Anfang definieren wir eine Reihe von Ant-Properties, die ungefähr dieselbe Bedeutung haben wie Variablen in einem Programm. Sie definieren die benötigten Klassenpfade und die beiden Tasks zum Kompilieren und Ausführen von Groovy-Programmen. Dann kommen die einzelnen Targets wie oben beschrieben.

Sie können das Build-Skript als build.xml speichern, und sofern Ant richtig installiert ist, genügt ein Aufruf von ant oder ant targetname, um entweder das angegebene Target oder das Default-Target buildall auszuführen.

Wenn wir nun dasselbe mit Groovy machen wollen, müssen wir folgendermaßen vorgehen:

  • Alle Ant-Properties in Groovy-Variablen verwandeln.
  • Die Targets zu Methoden des Groovy-Skripts umformen. Wo Abhängigkeiten bestehen, müssen die vorausgesetzten Targets als Methoden aufgerufen werden.
  • Aus allen Task-Aufrufen Methodenaufrufe einer AntBuilder-Instanz machen. Dabei werden die XML-Attribute zu benannten Methodenparametern. Wenn es eingebettete Elemente gibt, müsse diese mit geschweiften Klammern zu einer Closure verbunden werden.
  • Überall, wo Sie in dem Ant-Skript Attributwerte als Strings angegeben sind, können Sie stattdessen Groovy-Ausdrücke passenden Typs einfügen (z.B. Groovy: debug:true anstelle von XML debug="true").
  • Dann benötigen Sie noch etwas Logik, die dafür sorgt, dass das richtige Target aufgerufen wird.

Das Ergebnis sieht aus, wie in folgendem Beispiel dargestellt.

ant = new AntBuilder()

// Einige Variablen definieren
BASEDIR = '.' // entspricht basedir-Attribut
JAVA_SOURCE = "$BASEDIR/src"
GROOVY_SOURCE = "$BASEDIR/src-groovy"
LIB = "$BASEDIR/lib"
BUILD = "$BASEDIR/classes"
DIST = "$BASEDIR/dist"
APPNAME = "beispiel"
MAINCLASS = "GroovyKlasse"
VERSION = "0.1-beta-6"
DEFAULT_TARGET = "buildall"

// Methode zum ermitteln von Umgebungsvariablen
def env(name) { System.getenv(name) }

// Klassenpfad aus Build-Verzeichnis und Bibliotheken
MAINPATH = ant.path {
  fileset (dir:LIB) {
    include (name:'*.jar')
  }
  pathelement(path:BUILD)
}

// Taskdefinitionen für Groovy
ant.taskdef (name:'groovyc',classname:'org.codehaus.groovy.ant.Groovyc')
ant.taskdef (name:'groovy',classname:'org.codehaus.groovy.ant.Groovy')

// Clean löscht alle Zielverzeichnisse
def clean() {
  println "clean:"
  ant.delete(dir:BUILD)
  ant.delete(dir:DIST)
}

// Buildjava kompiliert alle Java-Quellen
def buildjava() {
  println "buildjava:"
  ant.mkdir(dir:BUILD)
  ant.javac(destdir:BUILD,debug:true,failonerror:true) {
    src(path:JAVA_SOURCE)
    classpath(path:MAINPATH)
  }
}

// Buildall kompiliert alle Quellen
def buildall() {
  buildjava()
  println "buildall:"
  ant.groovyc (destdir:BUILD,srcdir:GROOVY_SOURCE,classpath:MAINPATH)
}

// Runjava führt die Anwendung als Java-Executable aus
def runjava() {
  buildall()
  println "runjava:"
  ant.java (classname:MAINCLASS,classpath:MAINPATH)
}

// Rungroovy führt die Anwendung als Skript aus
def rungroovy() {
  buildjava()
  println "rungroovy:"
  ant.groovy (src:"$GROOVY_SOURCE/${MAINCLASS}.groovy",classpath:MAINPATH)
}

// Dist erzeugt JAR und kopiert alle Bibliotheken in Zielverzeichnis
def dist() {
  clean()
  build()
  println "dist:"
  ant.mkdir(dir:DIST)
  ant.jar(destfile:"$DIST/${APPNAME}-${VERSION}.jar",basedir:BUILD)
}

// Skript-Routine
// Wenn kein Target angegeben ist, nehmen wir das DEFAULT_TARGET
if (!args) args = [DEFAULT_TARGET]
// Alle als Argumente angegebenen Targets der Reihe nach aufrufen
args.each { target -> "$target"() }

Das Groovy-Programm ist fast eine Eins-zu-eins-Übertragung des Ant-Skripts, das Sie anhand des Kommentars leicht nachvollziehen können sollten. Auch die Task-Definitionen für die beiden Groovy-Tasks <groovy> und <groovyc> finden Sie wieder. Sie sind etwas kompakter, da wir, weil wir uns bereits in einem Groovy-Skript befinden, nicht noch einmal den Klassenpfad von Groovy deklarieren müssen. Am Anfang einiger Target-Methoden finden Sie die Aufrufe der Target-Methoden, die jeweils vorausgesetzt werden. Außerdem steht dort immer eine println()-Anweisung, die den Namen des Targets genau in derselben Weise ausgibt wie Ant es tut.

Am Ende des Skripts gibt es noch genau zwei Zeilen Ausführungslogik. Sie sorgt dafür, dass das Default-Target gesetzt wird, wenn kein Target explizit als Aufrufparameter genannt ist, und ruft dann alle Aufrufparameter als parameterlose Methoden auf. Denken Sie daran, dass sich bei diesem Beispiel die tools.jar-Datei aus dem JDK im Klassenpfad befinden muss, da sonst der Java-Compiler nicht gefunden wird.

Sie sehen schon an diesem Beispiel, dass das Groovy-Skript deutlich übersichtlicher ist als das äquivalente Ant-Skript, vor allem wenn Ihnen als Groovy- (oder zumindest Java-) Programmierer die Handhabung von Ant-Skripten weniger vertraut ist. Dieser Unterschied wird aber noch wesentlich drastischer, wenn Sie komplexe Abläufe oder sogar Schleifen abbilden müssen.

Die Tatsache, dass Sie in Groovy das ganze Ant-Paket mit seinen Standard-Tasks immer automatisch dabei haben, hat noch einen netten Nebeneffekt: Sie können es auch an Stellen nutzen, die mit Build-Vorgängen überhaupt nicht zu tun haben. Denken Sie daran, was Sie zu tun haben, um den Inhalt eines Verzeichnisses mit allen Unterverzeichnissen zu löschen. Mit Groovy und seinen vordefinierten Methoden für die Klasse File ist die Aufgabe zwar ohnehin übersichtlich, aber zusammen mit Ant wird es geradezu grotesk simpel:

new AntBuilder().delete(dir:'C:/build/temp')

Fertig.

Der Vollständigkeit halber seien auch zwei Nachteile des AntBuilder genannt. Einerseits gibt es auch hier ‒ wie auch im Vergleich mit Java ‒ die Schwierigkeit, dass es noch keine gleichwertige Toolunterstützung gibt. Wenn Ihre Entwicklungsumgebung ein Werkzeug zum Bearbeiten von Ant-Skripten zur Verfügung stellt, das Sie mit sofortiger Syntaxprüfung, automatischen Ergänzungen und Auswahllisten für mögliche Eingaben verwöhnt, müssen Sie mit Groovy an dieser Stelle etwas Verzicht üben. Am Ende werden Sie aber belohnt mit kompakten, verständlicheren und dadurch leichter zu wartenden Build-Skripten.

Eine spezielle Fähigkeit von Ant-Skripten lässt sich mit dem AntBuilder von Groovy nicht ohne weiteres abbilden: die Behandlung von Abhängigkeiten. Ant ordnet alle auszuführenden Targets mit ihren Abhängigkeiten so an, dass jedes Target nur einmal aufgerufen werden muss, und zwar bevor irgendein anderes Target an die Reihe kommt, das von ihm abhängig ist. Wir haben in unserem obigen Beispiel die Abhängigkeiten einfach durch verschachtelte Methodenaufrufe abgebildet. Dies kann zu Mehrfach-Aufrufen desselben Targets und unter bestimmten Bedingungen zu inkonsistenten Ergebnissen führen. In solchen Fällen müssen Sie sich noch etwas einfallen lassen, um die korrekte Reihenfolge der Abarbeitung von Targets zu garantieren.

Hilfreich könnte in diesem Zusammenhang ein derzeit noch im Aufbau befindliches Groovy-Modul namens Gant sein, das getrennt erhältlich ist. Auf dem AntBuilder aufbauend unterstützt es auch Build-Skripte mit weiter gehenden Anforderungen. Weitere Informationen dazu finden Sie unter http://groovy.codehaus.org/Gant.