Apache Maven Beispiel

Maven ist ein build Tool und Framework für java, welches es sich zum Ziel gemacht hat dem Anwender möglichst viele Schritte abzunehmen. In der allmächtigen pom.xml werden alle relevanten Optionen konfiguriert und ansonsten mit Konventionen in Form von Verzeichnissturktur und Dateinamen gearbeitet.

Das Framework erhält durch seine Pluginfähigkeit eine gute Flexiblität und ermöglicht viele Aufgaben durch ein wenig Konfiguration zu lösen.

Ich will hier eine maven Version meines vorherigen Apache ant Beispiels zeigen, die zum gleichen Ergebnis führt. Zudem gibt es einen kleinen Unit-Test, da das Framework deren Einbindung direkt mitbringt.

Am Anfang ist hier der getting started Guide von maven ein guter Anhaltspunkt http://maven.apache.org/guides/getting-started/index.html.

Mit folgendem Befehl legt man ein neues, leeres Projekt an. Weitere Infos werden anschließend in der pom.xml konfiguriert, dessen finale Version ich weiter unten aufgelistet und mit Kommentaren versehen habe.

mvn archetype:create \
-DarchetypeGroupId=org.apache.maven.archetypes \
-DgroupId=de.benjaminpeter \
-DartifactId=print

Code

Verzeichnisstruktur

Die Grundstruktur von Maven wurde mit dem create Befehl angelegt und ist dabei allgemein wie folgt:

pom.xml
src/main
src/resources
src/test
src/test/resources

Im konkreten Beispiel mit allen Klassen und Ressourcen:

./pom.xml

./src
./src/main
./src/main/java
./src/main/java/de
./src/main/java/de/benjaminpeter
./src/main/java/de/benjaminpeter/print
./src/main/java/de/benjaminpeter/print/LoadFile.java
./src/main/java/de/benjaminpeter/print/Print.java

./src/main/resources
./src/main/resources/resources
./src/main/resources/resources/inputA
./src/main/resources/resources/inputB
./src/main/resources/resources/merge
./src/main/resources/resources/file

./src/test
./src/test/java
./src/test/java/de
./src/test/java/de/benjaminpeter
./src/test/java/de/benjaminpeter/print
./src/test/java/de/benjaminpeter/print/LoadFileTest.java

./src/test/resources
./src/test/resources/resources
./src/test/resources/resources/file

Java src (analog zum ant Beispiel)

import java.io.*;
import java.lang.*;

public class LoadFile {
  public String load() throws FileNotFoundException, UnsupportedEncodingException, IOException {
    // File file = new File("resources/file");
    // FileInputStream fis = new FileInputStream(file);
    // Datei ist nun im jar, also muss sie anders geladen werden
    ClassLoader cl  = this.getClass().getClassLoader();
    InputStream fis = cl.getResourceAsStream("resources/file");
    // import groovy ... file.text ... :-/
    BufferedReader in = new BufferedReader(new InputStreamReader(fis));
    String str = "";
    String ret = "";
    while ((str = in.readLine()) != null) ret += str + '\n';
    return ret.substring(0, ret.length() - 1);
  }
}

import java.lang.*;

public class Print {
  public static void main(String[] args) {
    System.out.println("Hello.");
    try {
      System.out.println("Content:\n"+ 
        new LoadFile().load());
    }
    catch (Exception e) {
      System.err.println("Error: "+ e.getMessage());
    }
  }
}

Ressourcen

File: resources/file

A: Build @buildNumber@
If the writeList method doesn't
catch the checked exceptions
that can occur within it, the
writeList method must specify
that it can throw these
exceptions. Let's modify the
B:
original writeList method to
specify the exceptions it can
throw instead of catching
them. To remind you, here's
the original version of the
writeList method that won't
compile. 
File: resources/inputA

A: Build @buildNumber@
If the writeList method doesn't
catch the checked exceptions
that can occur within it, the
writeList method must specify
that it can throw these
exceptions. Let's modify the
File: resources/inputB

B:
original writeList method to
specify the exceptions it can
throw instead of catching
them. To remind you, here's
the original version of the
writeList method that won't
compile. 
File: resources/merge

#!/bin/sh
cat $1 $2 > $3

Hierbei musste ich @date@ zu @buildNumber@ ändern, da es ohne größeren Aufwand nicht möglich properties in anderen properties zu verwenden. (Falls es jemand besser weiß immer her damit, finde das durchaus wichtig)

pom.xml

<project
   xmlns="http://maven.apache.org/POM/4.0.0"
   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
   xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
  <!-- Basisinformationen zum Projekt -->
  <modelVersion>4.0.0</modelVersion>
  <groupId>de.benjaminpeter.print</groupId>
  <artifactId>Print</artifactId>
  <packaging>jar</packaging>
  <!-- Version des Projekts in maven, der Name des
       Jars kann noch einmal extra definiert werden! -->
  <version>0.0.1</version>
  <name>Print</name>
  <url>http://maven.apache.org</url>
  <!-- Default dependency fuer Unit-Tests, bibliothek
     wird automatisch herunter geladen -->
  <dependencies>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>3.8.1</version>
      <!-- scope: 
           Gibt an, dass diese Abhaengigkeit nur
           zum Testen benötigt wird. Das heißt sie wird
           auch nur beim Ausführen der Tests
           heruntergeladen und ist im
           finalen jar/war nicht enthalten. -->
      <scope>test</scope>
    </dependency>
  </dependencies>
  <build>
    <!-- Binde die resource "resources/file" ein, dies
         wird dynamisch erzeugt wie wir später sehen. -->
    <resources>
      <resource>
        <directory>src/main/resources</directory>
        <includes>
          <include>resources/file</include>
        </includes>
         <!-- Zudem soll hier das
          Filtering aktiviert werden,
         das heißt wir lassen maven einen
         Platzhalter @buildNumber@ in der
         Datei ersetzen. -->
        <filtering>true</filtering>
      </resource>
    </resources>
    <plugins>
      <!--
        Binde ein Plugin ein um die Build Nummer
        variabler erstellen zu können. Hiermit machen
        wir die Variable buildNumber vom aktuellen
        Datum abhängig.
        Auch das Plugin wird von maven bei Bedarf
        dynamisch herunter geladen. -->
      <plugin>
        <groupId>org.codehaus.mojo</groupId>
        <artifactId>buildnumber-maven-plugin</artifactId>
        <configuration>
          <format>{0,date,yyyyMMdd}</format>
          <items>
            <item>timestamp</item>
          </items>
        </configuration>
        <!-- Diese Block besagt, dass das Plugin in
             der validierungsphase (ganz am Anfang)
             ausgeführt werden soll. create ist dabei
             eine von mehreren Funktionen des Plugins
             die dabei aufgerufen werden soll. -->
        <executions>
          <execution>
            <phase>validate</phase>
            <goals>
              <goal>create</goal>
            </goals>
          </execution>
        </executions>
       </plugin> 
       <!-- Mit dem jar Plugin können wir
          die Erzeugung des jar Files etwas
          anpassen. So setzen wir hier die
          MainClass fest um ein einfach
         ausführbares jar zu bekommen. -->
       <plugin>
         <groupId>org.apache.maven.plugins</groupId>
         <artifactId>maven-jar-plugin</artifactId>
         <configuration>
           <archive>
            <manifest>
              <mainClass>Print</mainClass>
              <packageName></packageName>
            </manifest>
            <manifestEntries>
              <mode>development</mode>
              <url>${pom.url}</url>
            </manifestEntries>
          </archive>
        </configuration>
      </plugin>
      <!-- exec Plugin, dies ermöglicht es alle
           Schweinereien zu machen die man
           sonst nicht hinbekommt.
           Wird hier auch zur Verfizierungsphase
           ausgeführt und ruft den Befehl "merge"
           aus dem resources Verzeichnis auf
          um die Datei "file" zu erzeugen.
           
           An dieser Stelle könnte man sicher auch
           ein Ant Plugin einbinden um komplexere
           Abläufe zu realisieren und Abhängigkeiten
           sowie bereits angelegte Dateien erkennen
           zu können. -->
      <plugin>
        <groupId>org.codehaus.mojo</groupId>
        <artifactId>exec-maven-plugin</artifactId>
        <version>1.1</version>
        <executions>
          <execution>
            <phase>verify</phase>
            <goals>
              <goal>exec</goal>
            </goals>
          </execution>
        </executions>
        <configuration>
          <executable>./merge</executable>
          <workingDirectory>${basedir}/src/main/resources/resources</workingDirectory>
          <arguments>
            <argument>inputA</argument>
            <argument>inputB</argument>
            <argument>file</argument>
          </arguments>
        </configuration>
      </plugin>
    </plugins>
    <!-- Setze den Namen der jar Datei so, dass
         auch unsere im Plugin
         erzeugte buildNumber eingesetzt wird. -->
    <finalName>${project.artifactId}-${buildNumber}</finalName>
  </build>
  <properties>
    <!-- Funktioniert leider nicht :-/ -->
    <date>${buildNumber}</date>
  </properties>
</project>

Ausführung

Build

$ mvn install
[INFO] Scanning for projects...
[INFO] ------------------------------------------------------------------------
[INFO] Building Print
[INFO]    task-segment: [install]
[INFO] ------------------------------------------------------------------------
Downloading: http://repo1.maven.org/maven2/net/java/dev/jna/jna/3.0.5/jna-3.0.5.pom
Downloading: http://repo1.maven.org/maven2/net/java/dev/jna/jna/3.0.5/jna-3.0.5.pom
[INFO] [buildnumber:create {execution: default}]
[INFO] Storing buildNumber: 20091030 at timestamp: 1256918122611
[INFO] [resources:resources]
[INFO] Using default encoding to copy filtered resources.
[INFO] [compiler:compile]
[INFO] Compiling 2 source files to /home/dedeibel/proggn/java/test/mvn-example/Print/target/classes
[INFO] [resources:testResources]
[INFO] Using default encoding to copy filtered resources.
[INFO] [compiler:testCompile]
[INFO] Compiling 1 source file to /home/dedeibel/proggn/java/test/mvn-example/Print/target/test-classes
[INFO] [surefire:test]
[INFO] Surefire report directory: /home/dedeibel/proggn/java/test/mvn-example/Print/target/surefire-reports

-------------------------------------------------------
 T E S T S
-------------------------------------------------------
Running LoadFileTest
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.031 sec

Results :

Tests run: 1, Failures: 0, Errors: 0, Skipped: 0

[INFO] [jar:jar]
[INFO] Building jar: /home/dedeibel/proggn/java/test/mvn-example/Print/target/Print-20091030.jar
[INFO] [exec:exec {execution: default}]
[INFO] [install:install]
[INFO] Installing /home/dedeibel/proggn/java/test/mvn-example/Print/target/Print-20091030.jar to /home/dedeibel/.m2/repository/de/benjaminpeter/print/Print/0.0.1/Print-0.0.1.jar
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESSFUL
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 7 seconds
[INFO] Finished at: Fri Oct 30 16:55:26 CET 2009
[INFO] Final Memory: 31M/290M
[INFO] ------------------------------------------------------------------------

Programm

$ java -jar target/Print-20091030.jar         
Hello.
Content:
A: Build 20091030
If the writeList method doesn't
catch the checked exceptions
that can occur within it, the
writeList method must specify
that it can throw these
exceptions. Let's modify the
B:
original writeList method to
specify the exceptions it can
throw instead of catching
them. To remind you, here's
the original version of the
writeList method that won't
compile.

Fazit

Den direkten Vergleich zwischen ant und maven fand ich sehr interessant. Im kleinen Beispiel kann man die Stärken von maven jedoch nur erahnen. So hätte es bei ant wieder einer extra Einbindung der Unit-Tests bedurft, die aber im Grunde für jedes Projekt gleich ist wenn man sich an die Konventionen hält. Weiterhin musste kein Finger krum gemacht werden um eine aktuelle Version der junit Abhängigkeit zu bekommen; ein großer Vorteil bei einem Umzug auf ein anderes System, sofort werden die benötigten Abhängigkeiten ohne Aufwand nachgezogen.

Der Teufel steckt im Detail, während das Anpassen des Dateinamens und Ausführen des merge Skripts bei ant ein Kinderspiel sind, da man hier direkt auf die Abhängigkeiten und jar Erstellung Einfluss hat, scheint der Weg bei Maven für alles ein Plugin installieren zu müssen doch etwas umständlich. Letztendlich ist es aber eine Gute Sache, da es Plugins für die Unterschiedlichsten Aufgaben gibt und diese einem wie maven selbst möglichst viel Arbeit abnehmen. Allgemein sollte man sich vielleicht weniger mit Details aufhalten und gerade bei Verwendung von Frameworks Kompromisse eingehen und sich an die Gegebenheiten gewöhnen. Bei großen Projekten ist es auch so schwer genug die Übersicht zu behalten.

Quellen

Apache maven
Maven getting started

Die Vernachlässigung von tollen Namespaces für meine Klassen sie mir verziehen.

Werbeanzeigen

Apache Ant Beispiel

Apache ant ist ein build Tool, hauptsächlich für Java, was XML Dateien als Konfiguration verwendet.

Ant war früher sehr populär und ist nun z.B. durch maven erweitert oder ivy ersetzt, da diese über automatische Abhängigkeitsauflösung verfügen. Beim Umgang mit maven können einige Ant Grundlagen nicht schaden.

Das folgende Beispiel baut zwei Klassen und packt diese dann mit den Resource files in ein gemeinsames Jar. Die Resource Dateien werden zuvor noch mit einem Zeitstempel versehen und zusammengeführt um die Möglichkeiten von ant zu demonstrieren.

In einem zweiten Beitrag zeiche ich die Umsetzung des gleichen Projektes mit maven. Apache Maven Beispiel

Code

Dateien

./build.xml
./resources
./resources/inputA
./resources/inputB
./resources/merge
./src
./src/LoadFile.java
./src/Print.java

Java Source

import java.io.*;
public class LoadFile {
  public String load() throws FileNotFoundException, UnsupportedEncodingException, IOException {
    char[] buff = new char[10240];
    // File file = new File("resources/file");
    // FileInputStream fis = new FileInputStream(file);
    // Datei ist nun im jar, also muss sie anders geladen werden
    ClassLoader cl  = this.getClass().getClassLoader();
    InputStream fis = cl.getResourceAsStream("resources/file");
    InputStreamReader in = new InputStreamReader(fis, "UTF-8"); 
    in.read(buff, 0, 10240);
    return new String(buff);
  }
}

import java.lang.*;

public class Print {
  public static void main(String[] args) {
    System.out.println("Hello.");
    try {
      System.out.println("Content:\n"+ new LoadFile().load());
    }
    catch (Exception e) {
      System.err.println("Error: "+ e.getMessage());
    }
  }
}

Ressourcen

File: resources/inputA
A: Build @date@
If the writeList method doesn't
catch the checked exceptions
that can occur within it, the
writeList method must specify
that it can throw these
exceptions. Let's modify the

File: resources/inputB
B:
original writeList method to
specify the exceptions it can
throw instead of catching
them. To remind you, here's
the original version of the
writeList method that won't
compile. 

File: resources/merge
#!/bin/sh
cat $1 $2 &gt; $3

Ant build.xml

<project name="Print" default="build" basedir=".">
    <description>
        Print example build file
    </description>
  <!-- set global properties for this build -->
  <property name="src" location="src"/>
  <property name="build" location="build"/>
  <property name="dist"  location="."/>
  <property name="resources" location="resources"/>

  <!-- Vorbereitung der Resourcen -->
  <target name="prep" depends="init">
    <!-- Aufruf des Merge Befehls mit den Dateipfaden als Argumenten -->
    <exec executable="${resources}/merge">
      <arg value="${resources}/inputA"/>
      <arg value="${resources}/inputB"/>
      <arg value="${build}/resources/file.in"/>
    </exec>
    <!-- Setze einen Filter, dieser kann das Vorkommen von @token@
         in Dateien während Kopierbefehlen durch einen Wert ersetzen.
         Dazu muss bei copy zum Beispiel filterting="true" angegeben werden.
         
         Hier wird ein Vorkommen von @date@ durch einen Timestamp ersetzt.
         Die property DSTAMP kommt dabei vom Befehl mkstamp, welcher im
         target init ausgefuehrt wurde.  -->
    <filter token="date" value="${DSTAMP}"/>
    <!-- Kopiere die Datei um und wende dabei die Filter an -->
    <copy tofile="${build}/resources/file" file="${build}/resources/file.in" filtering="true"/>
    <delete file="${build}/resources/file.in"/>
  </target>

  <target name="init">
    <!-- Create the time stamp, verfügbar als ${DSTAMP} -->
    <tstamp/>
    <property name="outjar" value="${dist}/${ant.project.name}-${DSTAMP}.jar"/>
    <!-- Create the build directory structure used by compile -->
    <mkdir dir="${build}"/>
    <mkdir dir="${build}/resources"/>
  </target>

  <target name="compile" depends="init"
      description="compile the source " >
    <!-- Compile the java code from ${src} into ${build} -->
    <javac srcdir="${src}" destdir="${build}"/>
  </target>

  <target name="build" depends="compile, prep"
      description="generate the distribution" >
    <jar jarfile="${outjar}" basedir="${build}">
      <manifest>
        <!-- Muss angegeben werden damit java die Hauptklasse
             beim Ausfuehren des jars finden kann -->
        <attribute name="Main-Class" value="Print"/>
      </manifest>
    </jar>
  </target>

  <target name="clean"
        description="clean up" >
    <!-- Delete the ${build} and ${dist} directory trees -->
    <delete dir="${build}"/>
    <delete>
      <fileset dir="." includes="Print*.jar"/>
    </delete>
  </target>
</project>

Ausführung

$ ant
Buildfile: build.xml

init:
    [mkdir] Created dir: /home/dedeibel/proggn/java/test/ant/build
    [mkdir] Created dir: /home/dedeibel/proggn/java/test/ant/build/resources

compile:
    [javac] Compiling 2 source files to /home/dedeibel/proggn/java/test/ant/build

prep:
     [copy] Copying 1 file to /home/dedeibel/proggn/java/test/ant/build/resources
   [delete] Deleting: /home/dedeibel/proggn/java/test/ant/build/resources/file.in

build:
      [jar] Building jar: /home/dedeibel/proggn/java/test/ant/Print-20091030.jar

BUILD SUCCESSFUL
Total time: 2 seconds

Anwendung:

$ java -jar Print-20091030.jar 
Hello.
Content:
A: Build 20091030
If the writeList method doesn't
catch the checked exceptions
that can occur within it, the
writeList method must specify
that it can throw these
exceptions. Let's modify the
B:
original writeList method to
specify the exceptions it can
throw instead of catching
them. To remind you, here's
the original version of the
writeList method that won't
compile.

Quellen

Apache ant homepage
Writing a simple ant file

Die Vernachlässigung von tollen Namespaces für meine Klassen sie mir verziehen.