Softwareprojekte sind immer eine Herausforderung. Je größer Projekte werden, desto mehr bewahrheitet sich diese alte Weisheit. Ein Themenkomplex wird dabei immer auf besonders dramatische Weise unterschätzt: Buildmanagement und Packaging in Java Projekten. Nachdem Apache Ant diesen Bereich Jahrelang versuchte in geregelte Bahnen zu lenken etabliert sich seit ein paar Jahren auch Apache Maven als zunehmend interessante Alternative.
Hervorgegangen ist Maven aus dem Apache Jakarta Turbine Projekt. Es wurde mit dem Ziel aus der Taufe gehoben, das komplexe und verstreute, Ant basierte Buildmanagement der verschiedenen Subprojekte zu reorganisieren und zu vereinfachen. Hauptsächliches Ziel dabei war es, gemeinsame Bibliotheken effektiv wiederzuverwenden und einen standardisierten Weg zu haben um ein stabiles Turbine Projekt erstellen zu können. Vor diesem Hintergrund wurden bereits in 2002 etliche Ant Buildskripte von allen Projekten durch einen konsistenten Maven Build ersetzt. Damit ist auch klar, was "Maven" als Wort bedeutet. Es ist ein Wort aus dem Jiddischen, was soviel bedeutet, wie "Sammler von Wissen".
Mit diesem vergleichsweise hohen Anspruch tritt Maven auch an um Java Projekte besser zu machen. Dabei geht es primär um einen einfachen, standardisierten Buildprozess. Ergänzend dazu stellen eine Unmenge von Erweiterungen Informationen über die jeweiligen Projekte bereit und sorgen damit für die notwendige Transparenz über alle Subprojekte hinweg. Quasi nebenbei bringt Maven damit auch eine anständige Sammlung von Richtlinien und Regelungen im Sinne von "Best Practices" mit. Dazu gehören vor allem die saubere Integration von Unittests und das Bauen von verschiedenen Archivtypen (bspw. *.war, *.jar, etc.). In Summe ist Maven also das bessere Ant. Vor allem vor dem Hintergrund, dass es sich nicht nur um ein weiteres Build-Werkzeug handelt, sondern vor allem deshalb, weil es sich um einen standardisierten Buildprozess handelt. Oberstes Ziel ist und war dabei immer, dass das Wissen um den eigentlichen Build und Packaging Prozess nicht mehr nur bei ein paar Spezialisten verbleibt, sondern dass alle beteiligten Entwickler in der Lage sind, die Beschreibungen für den Prozess zu lesen und zu verändern. Damit können alle an derartigen Projekten beteiligten Entwickler ohne Probleme von einem ins nächste Projekt wandern, ohne sich erst orientieren zu müssen. Neben dem eigentlichen Build gilt dieses mit Maven auch für die Modultests und die Dokumentation. Quasi nebenbei erlaubt die konsequente Modulstrategie auch die Wiederverwendung von einzelnen Komponenten bzw. Modulen.
<proxy>
<id>optional</id>
<active>true</active>
<protocol>http</protocol>
<host>proxy.company.de</host>
<port>8084</port>
<nonProxyHosts>localhost</nonProxyHosts>
</proxy>
Die notwendigen Bibliotheken werden dann unter Zuhilfenahme des Proxys aus dem Netz geladen. Zum Glück ist dies nicht für jedes Ausführen von Maven notwendig. Einmal geladen, werden abhängige Bibliotheken von Maven auf dem Entwicklerrechner vorgehalten. Dies geschieht im Home-Verzeichnis des jeweiligen Nutzers unter ~\.m2\repository.
Was auf den ersten Blick nach dem Anlegen des neuen Moduls noch recht überschaubar aussieht, kann allerdings mit wachsenden Abhängigkeiten und längerer Maven Verwendung durchaus mächtig wachsen. Ein gut 500MB großes Repository ist keine Seltenheit. Es empfiehlt sich, in der settings.xml den Pfad zum <localRepository> anzupassen und ein Laufwerk zu verwenden, welches mehr Luft hat, als beispielsweise die typische C:\ Partition eines Windowsrechners.
Wer bei mehr als einem oder deutlich komplexeren maven-basierten Projekt mitarbeitet kann auch über die Verwendung von sogenannten <profiles> nachdenken. Dieser Konfigurationsabschnitt in der settings.xml ermöglicht die komplett separate Definition von verschiedenen Build-Prozess-Profilen. Mit diesem Mechanismus kann man dann nicht nur für unterschiedliche Umgebungen (Test, Produktion, et cetera) sondern auch für verschiedene Projekte ein jeweils einheitliches Profil definieren.
Auf der Ebene "src" finden sich die beiden Verzeichnisse "main" und "test". In "main" werden alle Java Sourcen hinterlegt. In "Test" gehören die relevanten Junit Testfälle. Grundsätzlich kann man mit diesem ersten Modul bereits anfangen zu entwickeln. Vor dem Start sollte man allerdings noch einen Blick in die pom.xml -Datei werfen. In dieser werden alle projektspezifischen Einstellungen von Maven verwaltet. In diesem ersten Beispiel ist sie noch recht übersichtlich.
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>de.heise.ix</groupId>
<artifactId>maven-beispiel</artifactId>
<packaging>jar</packaging>
<version>1.0-SNAPSHOT</version>
<name>maven-beispiel</name>
<url>http://maven.apache.org</url>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>3.8.1</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
Neben den gemachten Angaben zur Version und zum eigentlichen Modul (artifact) finden sich in der pom.xml vor allem die weiteren Abhängigkeiten zu anderen Bibliotheken oder Modulen. Im Falle unseres Beispiels ist das eine Abhängigkeit (Dependency) zum Projekt Junit in der Version 3.8.1.
Neben den Abhängigkeiten ist vor allem die Art des Moduls wichtig. Diese wird über das Tag <packaging> gesetzt. Im Falle des Beispiels steht dort "jar" als Typ. Weiterhin sind noch maven-plugin, ejb, war, ear, rar, par und pom möglich. Dabei handelt es sich jeweils um eine spezielle Art des Packaging, was von maven durchgeführt wird. Beim Typ "war" wird beispielsweise eine JEE Webanwendung gebaut. Besonders ist der Typ "pom". Er definiert ein übergreifendes Konfigurationsmodul. Dies ist wichtig, will man ein Projekt aus mehreren Submodulen aufbauen. Erst wenn man in dem Beispiel den Typ "pom" gesetzt hat, kann man dies auch tun. Wechselt man in den erstellen Ordner "maven-beispiel" und führt den folgenden Befehlt aus:
mvn archetype:create -DgroupId=de.heise.ix -DartifactId=modul1
Wird ein neues Modul-Projekt unterhalb des maven-beispiels angelegt. In der übergreifenden pom.xml findet sich dann auch der neue, zusätzliche Eintrag:
<modules>
<module>modul1</module>
</modules>
Analog dazu lassen sich nun beliebig strukturierte und hierarchisch aufgebaute Projekt- und Modulstrukturen umsetzen. Im Sinne der Modulfindung eignet sich ein Blick auf die Komponenten eines Systems. Es hat sich herausgestellt, dass grade die Komponenten, welche fachliche Anforderungen gesamthaft behandeln geeignete Kandidaten für separate Module sind. Im Sinne des Softwaredesigns kann man hier tatsächlich auch von Softwarekomponenten reden. Klassisch sind diese bereits durch geeignete Packagestrukturen vorgegeben. Findet man bereits zum Projektstart einen entsprechenden Modulschnitt, dann hat man Abhängigkeiten zwischen Komponenten per Deklaration von Anfang an im Griff. Hat man nun mehr als ein weiteres Modul hinzugefügt und will man beispielsweise aus modul1 auf modul2 zugreifen, dann ist dies nicht ohne weiteres Möglich. Nachdem beide Module ihre Sourcen und auch die Kompilate in verschiedenen Verzeichnissen haben, muss man Maven zuerst mitteilen, wie die Abhängigkeiten zwischen den Modulen aussehen. Dazu wird in dem Modul, welches ein oder mehrere andere Module verwenden will eine entsprechende Abhängigkeit in die pom.xml eingetragen. Will unser modul1 bspw. auf das modul2 zugreifen, dann muss in der pom.xml von modul1 folgendes eingetragen werden:
<dependency> <groupId>de.heise.ix</groupId> <artifactId>modul2</artifactId> <version>1.0-SNAPSHOT</version> </dependency>Dies führt dazu, dass die Abhängigkeit bei allen Aktionen auf modul1 entsprechend berücksichtigt wird. Muss modul1 beispielsweise kompiliert werden, dann wird zuerst modul2 kompiliert und dessen Kompilate können dann in modul1 verwendet werden. Gleiches gilt natürlich auch für die weitere Vererbung von Bibliotheken. D.h. wenn modul2 eine weitere Bibliothek verwendet, dann kommt diese über die Abhängigkeit auch in das modul1. Nicht unerwähnt bleiben sollte die Tatsache, dass man neben Testfällen und den Java Sourcen auch noch beliebige andere Ressourcen in einem Modul verpacken kann. Dazu erstellt man einfach unterhalb von "main" noch ein weiteres Verzeichnis "resources". Alle Konfigurationen oder ähnliche Dinge, welche noch benötigt werden können hier hinterlegt werden. Beim Einpacken durch Maven kommen die ebenfalls mit ins Artefakt. Was nun noch fehlt ist eine Integration in eine integrierte Entwicklungsumgebung (IDE). Auch dies unterstützt Maven selbstverständlich. Mit dem Kommando mvn eclipse:eclipse generiert Maven die für Eclipse notwendigen .classpath und .project Dateien in die Module hinein. Dem Import in eine Eclipse Workarea steht damit nichts mehr im Wege. Neben Eclipse werden auch IntelliJ, NetBeans und sogar der IBM Rational Application Developer (RAD) unterstützt. Durch die Konfiguration des entsprechenden Plugins kann man eine nahtlose Integration in die jeweilige Entwicklungsumgebung erreichen. Das folgende Beispiel zeigt, wie man das Ausgabeverzeichnis des Eclipse Build auf einen beliebigen Wert setzt:
<build>
[...]
<pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-eclipse-plugin</artifactId>
<configuration>
<outputDirectory>target-eclipse</outputDirectory>
</configuration>
</plugin>
</plugins>
</pluginManagement>
[...]
</build>
Abschließend bleibt nur noch die Frage, wie man denn mit Maven seine Sourcen compiled. Dazu gibt es ebenfalls ein Maven Komando. Mit mvn compile rennt Maven los und kompiliert alle Sourcen von allen Modulen in den zugehörigen "target" Ordner. Aufgeräumt wird mit mvn clean. Dabei werden alle generierten Dateien wieder gelöscht.
Ein letztes Kommando soll den bunten Reigen abrunden. Mit mvn install wird das generierte und verpackte Artefakt in das lokale Repository geschoben. Damit steht es anderen Projekten unter seiner Id und Gruppenzugehörigkeit, wie definiert zur Verfügung.
<dependency> <groupId>com.sun.facelets</groupId> <artifactId>jsf-facelets</artifactId> <version>1.1.14</version> </dependency>Erlaubt man dies in jedem beliebigen Modul zu jeder Zeit, dann überlebt man grad in größeren Teams immer wieder Überraschungen. Auch wenn die pom.xml Dateien zusammen mit den Sourcen in das Versionsverwaltungssystem gehören kann es schon einmal vorkommen, dass die letzte generierte Eclipse Integration (.classpath) nicht mehr zu den in den pom.xml konfigurierten Abhängigkeiten passt. Direktes Ergebnis hieraus ist es, dass zwar Maven die Module kompilieren kann, diese im Eclipse aber nicht mehr lauffähig sind. Hier gibt es auch aus Maven heraus keine richtig gute Lösung. Bewährt hat sich, dass grad für besonders auf Abhängigkeiten angewiesene Module (bspw. .ear / .war - Dateien) jeweils ein zugehöriges "modulx-deps" Modul anzulegen, welches einzig und allein den Zweck hat, die entsprechenden Abhängigkeiten bereitzustellen. Das löst jetzt nicht direkt das Problem des statischen Eclipse Mappings, aber vereinfacht die Administration von Abhängigkeiten deutlich. Um in den pom.xml Dateien konfigurierte Dinge direkt zur Laufzeit in die IDE zu bringen, eignen sich einige zusätzliche Helferchen. Mehr dazu im nächsten Abschnitt. Zweiter wichtiger Punkt ist das Thema Repository. Wie bereits erwähnt kann Maven sich alle konfigurierten Abhängigkeiten direkt aus dem Netz vom zentralen Maven Repository herunterladen. Neben den berühmten "central" Repositories (repo1 und repo2.maven.org) gibt es noch eine nahezu unzählige Menge weiterer Verzeichnisse im Netz. Neben Java.net und Codehaus.org gibt es auch zumeist spezielle Snapshot-Repositories verschiedenster Anbieter. Hier ist es nicht ganz trivial, eine geeignete Auswahl zu treffen. Die Konfiguration der notwendigen Repositories kann man sowohl in der pom.xml des Moduls als auch in der globalen settings.xml vornehmen. Hier ist es naheliegend, dass beides nicht wirklich tragfähig ist. Die settings.xml steht zumeist nicht unter Versionskontrolle eines einzelnen Projektes und die Einführung eines speziellen Repositories in einer speziellen pom.xml ist wiederum nicht administrierbar in größeren Teams. Daher sollte man sich schnell überlegen, ob und in welcher Form man ein eigenes Repository für ein Projektteam oder gar die ganze Firma aufbaut. Unter dem Stichwort "Maven Proxy" oder "Repository Manager" wird man schnell im Internet fündig. Neben der schlankeren Variante von Codehaus (maven-proxy) gibt es auch deutlich komfortablere Produkte, wie beispielsweise den Nexus Repository Manager. Ist eine Alternative gewählt, kann man pro Entwickler in der settings.xml direkt ein verbindliches, zentrales Repository angeben. Dies geschieht im Abschnitt <mirrors>. Hier wird einfach mit einem Vierzeiler der zentrale, firmeneigene Spiegel eingerichtet:
<mirror>
<id>firmen-repository</id>
<name>Unser zentraler Maven Proxy</name>
<url>http://host.firma.de:8080/repository</url>
<mirrorOf>*</mirrorOf>
</mirror>
Enthält der Tag <mirrorOf> den Wert "*" wird dieser für alle Anfragen zu allen Bibliotheken kontaktiert. Dies kann man bei Bedarf auch weiter einschränken. Beispielsweise nur auf ein spezielles Repository. In komplexen und großen Projekten kann es sich auch lohnen, die Maven Proxys nach Verwendungszweck entsprechend zu teilen. Damit kann man ein Repository für "Snapshots", ein weiteres für "firmeninterne Bibliotheken" und ein weiteres für den Rest der Opensource Bibliothekswelt aufbauen. Dies führt zu einer wesentlich übersichtlicheren Struktur und zu einer deutlich einfacheren zentralen Administration. Einen weiteren Vorteil hat der Einsatz eines eigenen Repositories darüberhinaus noch. Die Verwendung von sogenannten Snapshot-Versionen kann deutlich besser kontrolliert werden. Die meisten Opensource Projekte unterscheiden zwischen den sogenannten Stable oder auch Release Versionen und den Nightly Builds bzw. den Snapshot Versionen. Die Verwendung von letzteren ist eigentlich in Summe zu vermeiden. Leider klappt das nicht immer, da bestimmte Fehler mittelfristig nur in den Snapshots behoben werden und erst Wochen oder gar Monate später in ein neues Release einfließen. Ein Softwareprojekt mit einer Snapshot Abhängigkeit ist allerdings blanker Selbstmord, da keinerlei Qualitäts- bzw. Funktionalitätszusagen für solche Versionen zu erwarten sind. Mit einem eigenen Verzeichnisdienst kann man eine ausgewählte und zumindest teilweise überprüfte Version eines Snapshots durchaus kurzfristig in ein Projekt integrieren.
Auch das Thema Softwarequalität kommt bei Maven Projekten nicht zu kurz. Neben dem Checkstyle Plugin ist auch das PMD Plugin Bestandteil der eigentlichen Maven Distribution. Darüber hinaus gibt es noch weitere nützliche Erweiterungen von anderen Opensource Projekten. Findbugs sei hier nur als ein weiteres genannt. Grundsätzlich können diese Plugins an zwei verschiedenen Stellen in den pom.xml Dateien eingetragen werden. Einmal als Prüfung während des Build Prozesses. Dann findet man sie zwischen den Tags <build>[…] </build> oder aber auch als Reporting Plugins. Dann stehen sie zwischen den Tags <reporting>[…] </reporting>. Während die Plugins zwischen den <build>-Tags den eigentlichen Build des Projektes überwachen, sind die Plugins zwischen den <reporting>-Tags für die Informationen auf den Projekt-Report-Seiten zuständig. Nahezu alle Plugins lassen sich mit einer eigenen Konfiguration versorgen. Man kann also pro Projekt entscheiden, welche Metriken man prüfen möchte. Beide Arten sollten nur in der obersten pom.xml Datei im Projekt stehen. Damit lassen sich diese Vorgaben zentral ein oder ausschalten. Wenn die Konfiguration nicht nur beim Build verwendet werden soll, sondern auch den Entwicklern in der IDE zur Verfügung stehen soll, dann muss man noch am Eclipse Plugin eine Konfiguration vornehmen:
<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-eclipse-plugin</artifactId> <configuration> <additionalConfig> <file> <name>.checkstyle</name> <url>http://some.place.org/path/to/file</url> </file> </additionalConfig> </configuration> </plugin>Letztes Problem bleibt der Umstieg der Entwickler von Ant auf Maven. Die vorhandene, jahrelange Erfahrung macht einen Umstieg auf ein neues System nicht immer einfach. Vor allem dann nicht, wenn es auf den ersten Blick ähnlich aussieht und sich dennoch komplett anders verhält. Es bleibt also der letzte Ratschlag, jemanden im Team zu benennen, der sich einarbeitet und zuständig fühlt für Probleme rund um Build und Packaging. Im einfachsten Fall sind die Basics schnell verstanden und gelernt. In Spezialfälle muss man sich einarbeiten. Eine Lösung gibt es immer.