Eine Einführung

Dependency Management mit Apache Ivy

Xavier Hanin und Jan Matèrne

Wenn die eigene Software bestehende Module wiederverwenden kann, ist dies schön, da das Rad nicht erneut erfunden werden muss – aber es folgen andere Probleme. Wenn die eigene Software von Komponenten abhängt, die ihrerseits Abhängigkeiten aufweisen, wird es schnell komplex. Dann wird eine Werkzeugunterstützung wichtig und Apache Ivy unterstützt bei dieser Verwaltung.

In diesem Artikel werden wir zeigen, warum Dependency Management wichtig ist und wie man mit Ivy die damit verbundenen Probleme löst. Ivy ist ein flexibler Dependency Manager unter Open Source Lizenz und gerade dabei, den Inkubationsprozess der Apache Software Foundation zu durchlaufen. Wir werden weiterhin zeigen, wie Ivy installiert und in einfachen Szenarien verwendet wird, um im Folgenden über das mächtige und flexible Konfigurationskonzept zu sprechen.

Warum sollte ich mich überhaupt mit Dependency Management beschäftigen?

Am Anfang des Software-Engineerings war die Wiederverwendung von Software lediglich Utopie. Seit Einführung der objektorientierten Programmierung und der immer stärkeren Akzeptanz von Open Source beginnen Entwickler mehr und mehr, existierende Bibliotheken wiederzuverwenden. Haben Sie schon mal etwas von Hibernate, Apache Log4J oder dem Spring Framework gehört? Ich bin sicher, Sie haben es, da nur noch wenige dieser Tage das Rad neu erfinden. Entwickler ziehen es vor, bestehende und solide Bibliotheken wiederzuverwenden und konzentrieren sich auf ihr eigentliches Business.

Schön wäre es, wenn die Wiederverwendung einer einfachen Bibliothek so leicht wäre wie „lade sie herunter und lege sie in den Klassenpfad“. Viele haben sich bestimmt schon die Frage gestellt: Was muss ich alles in meinen Klassenpfad legen, um Hibernate zum Laufen zu bekommen? Ganz einfach: nur die ganzen JARs aus dem lib-Verzeichnis von Hibernate in den eigenen Klassenpfad übernehmen und fertig. Gut – wenn einem die Größe der eigenen Anwendung egal ist. Aber was ist, wenn die andere Bibliothek ihrerseits andere Bibliotheken verwendet? Und dann noch welche, die man selber nutzt? Was ist zu tun? Einfach die neueste Version nutzten? Aber warum habe ich eine bestimmte Version? Weil Hibernate diese haben möchte? Oder weil diese lediglich für ein optionales Cache Management benötigt wird, was in unserer Anwendung ohnehin nie genutzt wird?

Wie man sich vorstellen kann, ist die Wiederverwendung von Bibliotheken kompliziert und die Verwaltung von diesen Abhängigkeiten nicht einfach und kann zu Kopfschmerzen und schlaflosen Nächten führen. Einige werden vielleicht anführen, dass dies doch eher selten auftritt, da das Architekturteam bereits ein aufeinander abgestimmtes und getestetes Set von Bibliotheken freigegeben hat, welches von allen genutzt wird.

Das Problem ist, dass Software-Reuse nicht auf externe Bibliotheken beschränkt ist. Sehr häufig werden komplexe Systeme dergestalt entworfen und realisiert, dass das System in kleinere Bestandteile – Module –  zerlegt wird, getreu nach dem Motto „Teile und Herrsche“. Wenn man nun anfängt, diese Module isoliert zu betrachten, wird das Dependency Management bereits schnell zum Albtraum: Was passiert, wird ein common-Modul eine neue Version einer Bibliothek oder gar eine ganz andere Bibliothek benötigt? Wie sollen die Module zusammen in ihren jeweilig aktuellsten Versionen integriert werden? Oder wie werden alle Module in ihrem neuesten Release und eine mit der aktuellsten Version integriert? Wie kommt man zu Vorversionen zurück, weil ein Modul selbst andere Abhängigkeiten besitzt? Und was, wenn dies alles erst am Tag der Release-Erstellung auftritt, weil alle davon ausgegangen sind, dass alles gut läuft, die Integration aber dennoch fehlschlägt? Und wie soll das weitergehen, wenn man für dieses Dependency Management einen halben Tag braucht, aber ein Integrationsbuild einmal oder zweimal die Woche vorgesehen sind? Oder es soll gar kontinuierlich integriert werden (Continouus Integration)?

Weil diese Probleme nicht leicht zu lösen sind, benötigt man ein Werkzeug. Und hier kann Ivy helfen. Ivy ist ein mächtiges, stabiles und doch flexibles Dependency Management Werkzeug unter Open-Source-Lizenz. Es ist von vorneherein so entworfen worden, dass es an die jeweiligen Bedürfnisse und Umgebungen anzupassen ist und damit nicht eine bestimmte Umgebung oder Vorgehensweise vorschreibt. Ivy ist sehr gut geeignet für das Dependency Management bei Java Projekten, aber es ist nicht auf Java-Projekte beschränkt. Es kann genauso gut auch andere Abhängigkeiten verwalten. Ivy ist sehr gut in Apache Ant integriert, kann aber auch standalone von der Kommandozeile oder in einer eigenen Anwendung verwendet werden. Es gibt viele Möglichkeiten – dieser Artikel wird sich jedoch auf eine beschränken: das Dependency Management bei Java Projekten mit Apache Ivy und Apache Ant.

Erster Kontakt mit Ivy

Um mit Ivy (in der Verbindung mit Ant) anzufangen, benötigen wir folgende installierte Software:

Wir überprüfen die Installation mit folgenden Anweisungen:

$ java -version
java version "1.6.0_01"
Java(TM) SE Runtime Environment (build 1.6.0_01-b06)
Java HotSpot(TM) Client VM (build 1.6.0_01-b06, mixed mode, sharing)

$ ant -version
Apache Ant version 1.7.0 compiled on December 13 2006

Gut. Nachdem die Voraussetzungen geprüft sind, können wir Ivy installieren. Die Installation von Ivy ist sehr einfach: nur die Binär-Distribution von der Homepage herunterladen, das ZIP-Archiv irgendwohin entpacken und das ivy.jar ist da (ivy-2.0.0-alpha-1-incubating.jar im aktuellen Release). Danach muss nur noch Ant zur Verfügung gestellt werden, beispielsweise indem es in Ants lib-Verzeichnis kopiert oder per lib-Parameter eingebunden wird.

Aber es geht noch einfacher, indem die Schritte Download und Bekanntmachen im Buildfile selber eingetragen werden. Dies ist möglich, da zum einen Ivy als alles enthaltende Jar-Datei erhältlich ist und Ants <taskdef> einen Suchpfad annimmt.

Hier ist ein kleines Buildfile, welches diese automatische Installation von Ivy durchführt:

Dies funktioniert zusammen mit der Propertiesdatei ivy.properties:

ivy.version=2.0.0-alpha-1-incubating
ivy.jar.url=http://people.apache.org/~xavier/ivy/${ivy.version}/ivy.jar
ivy.jar.dir=${basedir}/ivy
ivy.jar.file=${ivy.jar.dir}/ivy.jar

Mit diesen Angaben lädt Ant die Propertiesdatei ivy.properties. In dieser sind die Angaben hinterlegt, was wohin geladen werden soll. Im Anschluss lädt es die ivy.jar aus dem Internet, erzeugt einen <path> mit dieser Jar-Datei (und allen anderen Dateien, die wir in das spezifizierte Verzeichnis legen, was für optionale Abhängigkeiten nützlich ist). Schlussendlich lädt es die Ivy-Tasks, wie sie in der AntLib definiert sind.

Vielleicht der wichtigste Teil des Buildfiles ist die Namespace-Deklaration am Anfang:

xmlns:ivy="antlib:org.apache.ivy.ant"

Diese ist auch dann Voraussetzung, wenn man Ivy manuell installieren möchte, um die Ivy-Tasks an den Ivy-Namespace zu koppeln. Mit diesem setup-Target können wir den ersten Schritt mit Ivy wagen. Wir lassen Ivy ein Modul mit seinen (transitiven) Abhängigkeiten herunterladen. Dazu fügen wir folgendes Target unserem Buildfile hinzu:

Testen wir nun, ob dieses Target richtig funktioniert.

$ ant retrieve-deps

Wenn alles gut gegangen ist, sollte nun ein lib-Verzeichnis angelegt worden sein, das in etwa so aussieht:

Abb. 1: lib-Verzeichnis

Wie man sehen kann, hat Ivy das struts2-core-Modul im Maven2 Repository gefunden und es mit all seinen selbst benötigten Bibliotheken heruntergeladen. Wie das? Wir haben die ivy:retrieve-Task mit den Parametern zur Identifizierung des Moduls, welches wir haben möchten, aufgerufen: die Organisation, den Modulnamen und die Revision. Da wir das Maven2 Repository nutzen (was die Voreinstellung ist, wenn man sonst nichts angibt), entsprechen diese den Angaben des Repositorys. Welche Module im Maven2 Repository vorhanden sind, kann auf dessen Webseite mithilfe der Suche herausgefunden werden. Das Einzige, was man wissen muss, ist, dass die Ivy-Organisation der Maven-groupId entspricht. Für den Rest wird dasselbe Vokabular verwendet. Neben den Angaben zur Modulidentifikation haben wir noch zwei weitere Parameter angegeben: inline=true wird verwendet, um zu spezifizieren, dass wir unsere Abhängigkeit auf struts2-core im Buildfile selber (eben „inline“) und nicht in einer externen Konfigurationsdatei speichern. Da die Inline-Spezifikation nicht der Standardfall ist, ist dieses Attribut in unserem Fall zu setzten. Der andere Parameter ist conf. Dieser spezifiziert, welche Modul-Konfiguration wir haben möchten. Was Modul-Konfigurationen sind und wie nützlich und mächtig sie sind, werden wir später sehen. Für diesen Augenblick ist conf ein guter Anfangswert.

Weiter geht’s: Einführung in Ivy´s Konfigurationsdateien

Nun da wir wissen, wie wir mit Ivy ein Modul und dessen abhängige Komponenten bekommen, wollen wir alle unsere benötigten Bibliotheken mit Ivy besorgen. Mithilfe der <ivy:retrieve>-Tasks und der Inline-Konfiguration haben wir eine Abhängigkeit aufgelöst. Dieses Verfahren würde auch für mehrere Bibliotheken funktionieren, allerdings skaliert es nicht wie gewünscht: Ivy wird dann angewiesen jede Bibliothek einzeln zu laden und schlimmer – Ivy wird die Möglichkeit genommen, den Überblick über alle Abhängigkeiten zu behalten und kann damit nicht mehr Konflikte erkennen und gegebenenfalls auflösen. Stellen Sie sich vor, wir hätten Abhängigkeiten zu zwei Modulen, die jeweils von commons-logging abhängen – aber davon unterschiedliche Versionen benötigen. Wenn wir jede (unserer) Abhängigkeiten einzeln laden würden, würden wir zwei Versionen von commons-logging erhalten. Nicht wirklich wünschenswert, oder?

Um nun alle Abhängigkeiten auf einmal aufzulösen (und auch aus anderen Gründen wie zum Beispiel Separation of Concerns: die Abhängigkeiten sind nun losgelöst vom Buildfile), sollten die Abhängigkeiten in einer eigenen Datei, dem Moduldeskriptor oder einfach Ivy-File, beschrieben werden.

Hier ist ein Beispiel für ein Ivy-File (in der Regel mit dem Namen ivy.xml):

Der Anfang der Datei ist leicht zu verstehen: wir beschreiben die Abhängigkeiten unserer Anwendung und deren eigener Bezeichnung (Organisation: org.acme.foo und Name: bar). Die eigene Bezeichnung wird sinnvoll, wenn man später die Anwendung selber in einem Repository zur Verfügung stellen möchte, sodass andere darauf verweisen können. Wir haben keine Revisionsnummer für unsere Anwendung angegeben (das Version-Attribut des <ivy-module>-Tags ist lediglich die Spezifikationsnummer des Dateiaufbaus), da unsere Anwendung sich noch in der Entwicklung befindet.

Lassen wir den Konfigurationsabschnitt mal für einen Augenblick beiseite und wenden uns den spezifizierten Abhängigkeiten zu. Sie sollten recht bekannt aussehen, da sie der Inline-Spezifikation ähnlich sehen. Hier haben wir zwei Abhängigkeiten deklariert, eine auf struts2-core und eine auf junit.

Nun sehen wir uns mal diese Abhängigkeitsbeschreibung an. Da fragen wir uns, was dieses conf-Attribut zu sagen hat. Erinnern Sie sich an die Inline-Deklaration? Das conf-Attribut wird für das Modul-Konfigurations-Mapping verwendet. Ivy verfügt mit der Modulkonfiguration über ein flexibles Konzept, das uns erlaubt, verschiedene Einsatzszenarien für ein Modul und unterschiedliche Abhängigkeiten für jedes Szenario zu definieren. In unserem Beispiel, genauer in dem <configurations>-Abschnitt, definieren wir zwei Szenarien (oder configurations um in der Ivy-Terminologie zu bleiben): eine runtime– und eine test-Konfiguration. Was wir dann noch brauchen, ist die Definition der Abhängigkeiten für diese verschiedenen Konfigurationen. Aber wir können auch noch mehr. Denn auch die Module, zu denen wir Abhängigkeiten spezifizieren, können über verschiedenen Konfigurationen verfügen. Daher können wir im conf-Attribut der <dependency>-Elemente auch deren Konfiguration angeben: Für die erste Abhängigkeit (struts2-core) ist das Mapping runtime->default. Das heißt, dass unsere runtime-Konfiguration von der default-Konfiguration von struts2-core abhängt. Der Pfeil verbindet also unsere Konfiguration mit der des Moduls. Entsprechend hängt unsere test-Konfiguration von der default-Konfiguration von junit ab.

Das ist alles. Im Grunde genommen war dies schon das Konfigurationsmapping (auch wenn Ivy einige Abkürzungen und mächtige Konstrukte im Bereich des Mappings beinhaltet). Schön einfach und doch sehr mächtig und flexibel, da man so viele Konfigurationen speichern kann, wie man möchte und mit den Namen, die man verwenden möchte. Auch wenn die default-Konfiguration eine spezielle Bedeutung hat, ist es letztendlich auch nur ein Name für Ivy wie runtime oder test. Sie behalten die Kontrolle darüber, was wann verwendet werden soll und können verschiedene komplexe Einsatzszenarien abdecken.
So, da wir nun unsere Abhängigkeiten in einem Ivy-File spezifiziert haben, interessiert es uns brennend, wie Ant nun Ivy dazu veranlassen soll, diese aufzulösen. Doch das ist einfach. Wenn nicht sogar einfacher als die Inline-Deklaration:

Wie zu Beginn nutzten wir die <ivy:retrieve>-Task, diesmal jedoch anstelle der Inline-Spezifikation mit einer Referenz auf das Ivy-File. Das file-Attribut könnte hier auch weggelassen werden, da ivy.xml der Default-Wert ist. Das pattern-Attribut ist ebenfalls optional. Es erlaubt uns das Ablageschema für die heruntergeladenen Bibliotheken zu spezifizieren. Dazu nutzten wir einen Patternmechanismus. Die Werte innerhalb der Klammern werden von Ivy durch die entsprechenden Werte ersetzt. (Eine Liste der zur Verfügung stehenden Pattern ist unter „Pattern“ hier zu finden.)  Dies erlaubt eine flexible und einfache Konfiguration zur Dateiablage. In diesem Fall landen unsere Bibliotheken alle in einem lib-Verzeichnis und darunter in einem Verzeichnis mit dem Namen der Konfiguration und der Revisionsnummer im Namen. Wenn wir die Revisionsnummer nicht im Namen haben wollen, brauchen wir lediglich das entsprechende Token weglassen.

Im Anschluss nutzten wir <ivy:report>, um einen Report über die Auflösung der Abhängigkeiten zu erzeugen. Dieser detaillierte HTML-Report hilft dabei zu verstehen, wie die einzelnen JAR-Dateien zusammenhängen. (Ivy kann auch einen Graphen erzeugen. Mehr dazu in der Ivy-Dokumentation.)

Abb. 2: Geladene Abhängigkeiten
Schlussbeispiel

In Beispiel 4 ist dann noch einmal alles zusammengefasst. Es beinhaltet zwei Java Quellen, ein Buildfile mit Propertydatei, einmal die Ivy-Properties und ein Ivy-File.
Die fachliche Javaklasse HelloWorld nutzt das Apache Log4J um eine Meldung auszugeben. Die Klasse HelloWorldTest testet diese mittels JUnit. Entsprechend definiert das Ivy-File zwei Konfigurationen: runtime mit einer Abhängigkeit zu Log4J und test mit einer zu JUnit. Der gesamte Buildprozess läuft dann folgendermaßen ab:

  • Lösche generierte Dateien
  • Lade ivy.jar herunter und registriere die Ivy-Tasks
  • Nutzte Ivy, um die benötigten Bibliotheken herunterzuladen
  • Kompiliere die Quellen
  • Starte die Klasse
  • Führe die Tests aus
  • Erzeuge die JavaDoc
Fazit

Wir haben gesehen, wie einfach Ivy installiert werden kann und wie Abhängigkeiten gegen ein öffentliches Maven2 Repository aufgelöst werden können. Wir haben das Konzept der Modulkonfiguration kennengelernt ebenso wie das flexible Konfigurationsmapping. Um mehr über Ivy zu lernen und wie auch mit anderen Arten von Repositories, Versionierungsschemata oder Konfliktlösungsstrategien gearbeitet werden kann, kann auf der Ivy Webseite und der Dokumentation entnommen werden. Natürlich sind auch Fragen an die Mailingliste willkommen.

Xavier Hanin ist unabhängiger Java-Consultant und der ursprüngliche Autor von Ivy. Er ist Mitglied des JSR 277 „Java Module System“ und aktiver Entwickler in zahlreichen OpenSource Projekten, darunter wicket-contrib-push, xooki und xoocode.org.

Jan Matèrne betreut als Softwareentwickler im Rechenzentrum des Landes Nordrhein-Westfalen die Entwicklungsumgebungen für Java. Er ist seit einigen Jahren im PMC von Apache Ant und ist Mitentwickler von args4j.

Zu dem Artikel können Sie Beispiele downloaden:

Geschrieben von
Xavier Hanin und Jan Matèrne
Kommentare

Schreibe einen Kommentar

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