Java Projekt

Basisstruktur für ein Java-Projekt
Projektverzeichnis

In diesen Artikel möchte ich beschreiben, wie man sein Java-Projekt organisieren und übersetzen kann. Die von mir verwendete Projektstrutur basiert auf den Java Blueprints Guidelines.

Projektstruktur

Die Projektstruktur baut auf den Projektkonventionen vom Sun bzw. Oracle auf (siehe Abbildung).

  • build: Beinhaltet den kompilierten Quelltext.
  • components: Beinhaltet die Teilkomponenten des Projekts. Die Struktur der Teilkomponenten gleicht denen des Projekts.
  • dist: Beinhaltet alle Dateien des Gesamtpakets. Das Verzeichnis wird zusammengepackt und ausgeliefert.
  • docs: Beinhaltet die (statische) Dokumentation des Projekts.
  • lib: Beinhaltet die Abhängigkeiten des Projekts. Diese können direkt im SCM gespeichert sein oder wähernd des Builds temporär abgelegt werden.
  • setup: Beinhaltet Konfigurationsdateien wie z. B. SQL-Skripte.
  • src: Beinhaltet den Quelltext des Projekts.
  • test: Beinhaltet Testskripte für Integrationstests. Der Quelltext der Tests wird unter src/test abgelegt.

build, dist und ggf. lib enthalten temporäre Dateien, die nicht im SCM eingecheckt werden sollten.

Ant-Skript zum Übersetzen

Mit dem folgenden Ant-Skript kann das Projekt übersetzt werden. Der Projektname zu Beginn wird auch als Dateiname für das zu erstellende JAR-File verwendet. Der Projektname sollte dabei nur aus Buchstaben und dem Bindestrich bestehen. Die Version und Dateiendung wird später ergänzt.

<?xml version="1.0" encoding="UTF-8"?>
<project name="projectname" default="all" basedir=".">

Alle Defaultwerte in diesem Skript lassen sich überschreiben. Dazu wird als nächstes ein externes File mit Properties eingelesen. Dieses Properties File muss nicht existieren bzw. kann leer sein.

	<property file="build.properties" />

Jetzt werden die Defaultwerte definiert. Als erstes wird die Versionsnummer angegeben. Sie setzt sich aus den drei Bestandteilen Hauptversion (major), Nebenversion (minor) und Patch-Level (maintenance) zusammen. Es sollten nur diese drei Werte überschrieben werden. Die zusammengesetzte Versionsnummer wird im Skript gebildet. Davon gibt es zwei, die komplette hier im Beispiel lautet 1.0.0, die Kompatibilitätsversion lautet 1.0 und wird als Suffix des JAR-Files verwendent.

	<!-- ==================== Options ==================== -->

	<property name="version.major" value="1" />
	<property name="version.minor" value="0" />
	<property name="version.maintenance" value="0" />
	<property name="version.compatible" value="${version.major}.${version.minor}" />
	<property name="version.full" value="${version.major}.${version.minor}.${version.maintenance}" />

Als nächstes sind die Verzeichnisse der Eingangsdaten dran ...

	<!-- ==================== Input Files ==================== -->

	<property name="src.java.dir" location="src/java" />
	<property name="src.conf.dir" location="src/conf" />
	<property name="src.test.dir" location="src/test" />
	<property name="lib.dir" location="lib" />

... und die Verzeichnisse der Ausgangsdaten.

	<!-- ==================== Output Files ==================== -->

	<property name="build.dir" location="build" />
	<property name="build.classes.dir" location="${build.dir}/classes" />
	<property name="build.test.dir" location="${build.dir}/test-classes" />
	<property name="dist.dir" location="dist" />
	<property name="filename.jar" value="${ant.project.name}-${version.compatible}.jar" />

Die zum Kompilieren notwendigen Classpaths werden getrennt nach Compile-Zeit und Lauftzeit notiert. Jeweils für das Projekt selbst wie auch für die Unit-Test.

	<!-- ==================== Classpaths ==================== -->

	<path id="classpath.compile">
	</path>

	<path id="classpath.run">
		<path refid="classpath.compile" />
	</path>

	<path id="classpath.test.compile">
		<path refid="classpath.compile" />
		<pathelement location="lib/junit-4.8.2.jar" />
	</path>

	<path id="classpath.test.run">
		<path refid="classpath.test.compile" />
	</path>

Jetzt werden die Targets definiert. Als erstes das Meta-Target all, welches einen vollständigen Build ausführt. Das folgende prepare-Target erstellt lediglichdie Verzeichnisse der Ausgangsdaten.

	<!-- ==================== Target Definitions ==================== -->

	<target name="all" depends="package,test,doc">
	</target>

	<target name="prepare">
		<mkdir dir="${build.classes.dir}" />
		<mkdir dir="${build.test.dir}" />
		<mkdir dir="${dist.dir}" />
	</target>

Das compile-Target kompiliert die Java-Quellen und kopiert die Konfigurationsdateien (src/conf) sowie die Lizenzdateien.

	<target name="compile" depends="prepare">
		<javac srcdir="${src.java.dir}" destdir="${build.classes.dir}" classpathref="classpath.compile" encoding="UTF-8" target="1.6" includeantruntime="false" />

		<copy todir="${build.classes.dir}">
			<fileset dir="${src.conf.dir}" includes="**/*" />
		</copy>

		<copy todir="${build.classes.dir}/META-INF">
			<fileset dir="${basedir}" file="LICENSE.txt" />
			<fileset dir="${basedir}" file="NOTICE.txt" />
		</copy>
	</target>

Das folgende Target erstellt im dist-Verzeichnis die JavaDocs. Alternativ oder zusätzlich könnten Dateien aus dem doc-Verzeichnis nach dist kopiert werden.

	<target name="doc" depends="prepare">
		<javadoc sourcepath="${src.java.dir}" destdir="${dist.dir}/javadoc" overview="overview.html">
			<classpath>
				<path refid="classpath.compile" />
			</classpath>
		</javadoc>
	</target>

Das test-Target führt die Unit-Tests aus. Ein scheitern eines oder sogar aller Tests, bricht den Build nicht ab. Der Build gilt dann als erfolgreich, aber instabil (vergleiche Buildstatus von Jenkins).

	<target name="test" depends="compile">
		<javac srcdir="${src.test.dir}" destdir="${build.test.dir}" encoding="UTF-8" target="1.6" includeantruntime="false">
			<classpath>
				<path refid="classpath.test.compile" />
				<pathelement path="${build.classes.dir}" />
			</classpath>
		</javac>

		<junit printsummary="true" showoutput="true" fork="true">
			<classpath>
				<path refid="classpath.run" />
				<path refid="classpath.test.run" />
				<pathelement path="${build.classes.dir}" />
				<pathelement path="${build.test.dir}" />
			</classpath>
			<assertions enablesystemassertions="true" />
			<batchtest>
				<fileset dir="${build.test.dir}" />
			</batchtest>
		</junit>
	</target>

Das folgende Target erzeugt das JAR-File des Projekts und kopiert die Abhängkeiten aus dem lib-Verzeichnis. Das dist-Verzeichnis kann im Anschluss z. B. als ZIP zusammengepackt und ausgeliefert werden.

	<target name="package" depends="compile">
		<manifestclasspath property="classpath.manifest" jarfile="${lib.dir}/${filename.jar}">
			<classpath refid="classpath.run" />
		</manifestclasspath>
		<jar destfile="${dist.dir}/${filename.jar}" basedir="${build.classes.dir}" compress="true" filesetmanifest="merge" duplicate="fail">
			<manifest>
				<attribute name="Implementation-Version" value="${version.full}" />
				<attribute name="Class-Path" value="${classpath.manifest}" />
			</manifest>
		</jar>

		<copy todir="${dist.dir}">
			<path refid="classpath.run" />
		</copy>
	</target>

Die beiden Targets zum Aufräumen unterschieden sich nur im Detail, clean löscht nur die temporären Dateien des Builds, während distclean alle erzeugten Dateien löscht.

	<target name="clean">
		<delete dir="${build.dir}" />
	</target>

	<target name="distclean" depends="clean">
		<delete dir="${dist.dir}" />
	</target>

</project>

Zurück