Build und CI/CD
Vom Quellcode zur laufenden Anwendung, ohne dass jemand von Hand etwas tut: was Maven in welchen Phasen erledigt, wie Abhängigkeitskonflikte entstehen, was eine CI-Pipeline prüft – und wie man neue Versionen ausrollt, ohne dass es jemand merkt.
Ein Build verwandelt Quellcode in etwas Lauffähiges: kompilieren, testen, verpacken. Damit das auf jedem Rechner gleich abläuft, beschreibt man ihn als Code – in Java mit Maven oder Gradle. Die Werkzeuge laden auch die benötigten Bibliotheken samt deren Abhängigkeiten, und genau dort entstehen die tückischsten Fehler.
Continuous Integration heißt: Jede Änderung wird sofort automatisch gebaut und getestet. Continuous Delivery heißt: Jede erfolgreich getestete Version könnte per Knopfdruck live gehen; bei Continuous Deployment geht sie es automatisch. Eine Pipeline führt all das in festen Stufen aus – und mit Strategien wie Rolling, Blue-Green oder Canary merken Nutzer vom Ausrollen nichts.
Der Build als Code
Früher bestand ein Build oft aus einer Anleitung im Wiki und einem Entwickler, der „wusste, wie es geht“. Heute steht alles in einer Datei im Repository – bei Maven in der pom.xml. Maven folgt dabei einem festen Lebenszyklus: einer Kette von Phasen, an die Plugins ihre Arbeit hängen. Wer eine Phase aufruft, bekommt alle davor mit:
Maven hat zwei Stärken, die man leicht übersieht: Konvention statt Konfiguration – Quellen liegen in src/main/java, Tests in src/test/java, und niemand muss das einstellen. Und den Maven Wrapper (mvnw): ein kleines Skript im Repository, das genau die richtige Maven-Version herunterlädt. Damit baut jeder Rechner gleich, ohne Installation. Gradle verfolgt dieselben Ziele mit einer Programmiersprache statt XML und mit einem ausgefeilten Cache, der unveränderte Arbeitsschritte überspringt.
Abhängigkeiten und ihre Konflikte
Eine Spring-Boot-Anwendung deklariert vielleicht zwanzig Abhängigkeiten – und bekommt Hunderte. Jede Bibliothek bringt ihre eigenen mit, die transitiven Abhängigkeiten. Brauchen zwei davon dieselbe Bibliothek in verschiedenen Versionen, muss das Werkzeug sich für eine entscheiden. Auf dem Klassenpfad kann nur eine liegen.
Das ist der Grund für einen der rätselhaftesten Fehler im Java-Alltag: NoSuchMethodError oder ClassNotFoundException zur Laufzeit, obwohl alles kompiliert. Kompiliert wurde gegen eine Version, geladen wird eine andere. Abhilfe schaffen mvn dependency:tree zum Nachsehen, ausdrücklich festgelegte Versionen und BOMs („Bill of Materials“) – Versionslisten, die zusammenpassende Versionen vorgeben. Spring Boot liefert so eine für Hunderte Bibliotheken.
Reproduzierbare Builds
Ein Build ist reproduzierbar, wenn derselbe Quellcode immer exakt dieselben Bytes ergibt. Das klingt selbstverständlich, ist es aber nicht: Zeitstempel in JAR-Dateien, die Reihenfolge von Dateien, Versionsbereiche wie „1.+“, die heute etwas anderes auflösen als morgen. Reproduzierbarkeit ist wichtig, weil man nur dann prüfen kann, dass ein ausgeliefertes Programm wirklich aus dem behaupteten Quellcode stammt. Maven unterstützt sie mit der Eigenschaft project.build.outputTimestamp.
Continuous Integration
CI ist zuerst eine Arbeitsweise, dann ein Werkzeug: Alle integrieren ihre Änderungen mindestens täglich in den Hauptzweig, und jede Integration wird automatisch gebaut und getestet. Wer das nicht tut, erlebt die „Integrationshölle“ – wochenlang getrennte Branches, die beim Zusammenführen nicht mehr zusammenpassen. Das Werkzeug dafür ist eine Pipeline, etwa in GitHub Actions, GitLab CI oder Jenkins:
Die wichtigsten Prinzipien einer guten Pipeline:
- Schnell scheitern: Billige Prüfungen zuerst. Wer zehn Minuten auf einen Integrationstest wartet, um dann zu erfahren, dass ein Komma fehlt, wartet beim nächsten Mal nicht mehr.
- Einmal bauen, überall ausrollen: Dasselbe Artefakt – dieselbe JAR-Datei, dasselbe Container-Image – wandert von Test über Abnahme bis Produktion. Wer für jede Umgebung neu baut, testet etwas anderes, als er ausliefert.
- Rot heißt: alles stehen lassen. Ein kaputter Hauptzweig hat Vorrang vor neuen Funktionen. Sonst gewöhnt sich das Team an rote Pipelines, und die Pipeline verliert jeden Wert.
Ausrollen ohne Ausfall
Die neue Version ist gebaut und getestet. Jetzt soll sie auf sechs Server – ohne dass ein Nutzer eine Fehlermeldung sieht. Drei gängige Strategien:
Oft kommen Feature Flags dazu: Neuer Code wird ausgeliefert, aber per Schalter erst später – oder nur für einen Teil der Nutzer – aktiviert. Ausliefern und Freischalten werden damit zwei getrennte Entscheidungen, und ein Problem lässt sich mit einem Klick abschalten, ohne neu auszurollen.
Für Entwickler übersetzt
<!-- pom.xml: Versionen zentral festlegen -->
<dependencyManagement>
<dependencies>
<dependency> <!-- BOM: passende Versionen für vieles -->
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement># .github/workflows/build.yml – eine minimale CI für Maven
name: build
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with: { distribution: temurin, java-version: '21', cache: maven }
- run: ./mvnw -B verify| Befehl | Wofür |
|---|---|
./mvnw dependency:tree | Welche Bibliothek zieht welche andere herein – und welche Version hat gewonnen? |
./mvnw versions:display-dependency-updates | Welche Abhängigkeiten sind veraltet? |
maven-enforcer-plugin | Bricht den Build ab, wenn Regeln verletzt sind – etwa widersprüchliche Versionen oder eine zu alte Java-Version. |
-SNAPSHOT-Versionen | Veränderliche Entwicklungsstände. Gehören nie in ein Release, denn morgen kann sich dahinter etwas anderes verbergen. |
| SBOM | Eine maschinenlesbare Liste aller enthaltenen Bibliotheken, etwa mit dem CycloneDX-Plugin. Damit weiß man sofort, ob eine neu bekannte Lücke einen betrifft. |
Häufige Irrtümer
NoSuchMethodError ist das typische Zeichen.