Java-Systems-Engineering: Build-Toolchains, JPMS-Module & Industrielles Testing
Erstellt: 07.11.2026 Zuletzt aktualisiert: 07.11.2026
Executive Summary: Professionelle Enterprise-Java-Anwendungen werden nicht mit Ad-hoc-Compilerbefehlen übersetzt; sie basieren auf industriellen Build-Automatisierungssystemen, expliziter modularer Kapselung und automatisierten Test-Suiten. Das Java Platform Module System (JPMS) erzwingt strikte Architektur- und Zugriffsgrenzen über alle Anwendungsschichten hinweg, während Build-Engines mehrstufige Lebenszyklen von der Kompilierung bis zur Artefakt-Verifikation orchestrieren. Dieser Leitfaden bildet Teil 1.2 (Grundlagen) unseres Java-Systems-Engineering-Curriculums: Er dekonstruiert Maven- und Gradle-Build-Lifecycles, konfiguriert Kapselungsschranken in
module-info.java, implementiert parametrisierte Test-Suiten mit JUnit 5 und baut eine modulare IPv4-CIDR-Subnetz-Berechnungsengine.
-
Maven- & Gradle-Lifecycles
Standard-Projektstrukturen, deterministische Lifecycle-Phasen, transitive Abhängigkeitsgraphen (DAG) und Build-Cache-Mechanismen dekonstruieren.
-
JPMS (Project Jigsaw)
Die Classpath-Hölle durch echte Modularisierung,
exports,requires transitiveund reflektive Laufzeit-Freigaben (opens) eliminieren. -
Industrielles Testing (JUnit 5 & Mockito)
Verlässliche Test-Suiten mit
@ParameterizedTest,@CsvSource, modularen Lebenszyklus-Hooks und Mockito 5-Testdoubles konstruieren. -
IPv4-Subnetz-Rechner Praxislabor
Ein produktionsreifes, modulares Maven-Projekt mit bitweiser IP-Arithmetik und 100% automatisierter Testabdeckung aufbauen.
1. Build-Toolchains unter der Haube: Maven vs. Gradle
Im Enterprise-Java-Ökosystem dominieren zwei etablierte Build-Automatisierungswerkzeuge: Apache Maven und Gradle.
%%{init: {'flowchart': {'wrappingWidth': 600}}}%%
flowchart TB
subgraph MavenEngine ["Apache Maven (Standard-Lifecycle)"]
direction TB
M_VAL["1. validate<br/>Projektkorrektheit prüfen"]
M_COM["2. compile<br/>src/main nach target/classes kompilieren"]
M_TST["3. test<br/>Unit-Tests mit Surefire ausführen"]
M_PKG["4. package<br/>Klassen in JAR / WAR bündeln"]
M_VRF["5. verify<br/>Integrationsprüfungen (Failsafe)"]
M_INS["6. install<br/>Artefakt in lokales ~/.m2 ablegen"]
M_VAL --> M_COM --> M_TST --> M_PKG --> M_VRF --> M_INS
end
subgraph GradleEngine ["Gradle (DAG Task-Graph)"]
direction TB
G_CFG["Konfigurationsphase<br/>build.gradle.kts auswerten"]
G_DAG["DAG-Graphen berechnen<br/>Task-Abhängigkeiten auflösen"]
G_EXE["Ausführungsphase<br/>Nur geänderte Tasks ausführen"]
G_CFG --> G_DAG --> G_EXE
end
Das standardisierte Maven-Verzeichnislayout
Maven standardisiert die Projektstruktur über die gesamte weltweite Java-Industrie hinweg (Convention over Configuration):
my-app/
├── pom.xml # Project Object Model Deskriptor
└── src/
├── main/
│ ├── java/ # Produktiver Java-Quellcode
│ └── resources/ # Konfigurationsdateien, Properties, SQL-Skripte
└── test/
├── java/ # Test-Quellcode (JUnit / Mockito)
└── resources/ # Testdaten und Mock-Fixtures
Koordinaten & Konfliktauflösung im Abhängigkeitsbaum
Maven identifiziert Bibliotheken deterministisch über ihre Koordinaten: GroupId:ArtifactId:Version (GAV).
Wenn in einem komplexen Baum transitive Versionskonflikte auftreten, greift Maven nach einer festen Regel: Nearest-Definition-Wins (die Version, die im Abhängigkeitsbaum am nächsten zur Wurzel liegt, gewinnt). Gradle hingegen wählt standardmäßig die neueste Version (Highest-Version-Wins).
2. Das Java Platform Module System (JPMS / Project Jigsaw)
Mit Java 9 führte das JDK das Java Platform Module System (JPMS) ein, um Java von einem unvalidierten, flachen Classpath auf einen explizit definierten Modulgraphen mit echter Kapselung umzustellen.
Classpath-Hölle vs. Modulare JPMS-Grenzen
- Flacher Classpath (
-cp): Alle JARs landen in einem gemeinsamen Topf. Fehlende Klassen führen erst zur Laufzeit zuClassNotFoundException. Doppelte Klassen erzeugen undefiniertes Verhalten je nach Lade-Reihenfolge. Interne Klassen können beliebig per Reflection manipuliert werden. - Modulpfad (
--module-path): Die JVM validiert bereits beim Start, ob alle deklarierten Module vorhanden sind. Klassen sind für andere Module strikt unsichtbar, es sei denn, ihr Paket wird inmodule-info.javaexplizit exportiert.
%%{init: {'flowchart': {'wrappingWidth': 600}}}%%
flowchart TB
subgraph ModuleA ["Modul: com.network.core"]
EXP["Exportiertes API-Paket:<br/>com.network.core.api"]
INT["Gekapseltes internes Paket:<br/>com.network.core.internal"]
end
subgraph ModuleB ["Modul: com.network.cli"]
CLI["CLI-Consumer-Dienst"]
end
CLI ==>|"Erlaubt (Compile & Runtime)"| EXP
CLI -.->|"BLOCKIERT durch JVM-Kapselung"| INT
Direktiven in module-info.java
module com.network.diagnostics {
// 1. Modulabhängigkeiten (Readability-Graph)
requires java.base; // Implizit in jedem Java-Modul vorhanden
requires java.net.http; // Standardmodul des JDKs einbinden
// 2. Transitive Sichtbarkeit: Wer dieses Modul benötigt,
// erhält automatisch auch Zugriff auf org.slf4j
requires transitive org.slf4j;
// 3. Strikte Kapselung: Nur öffentliche Klassen in diesem Paket sind sichtbar
exports de.nejathakan.netdiag.api;
// 4. Qualifizierter Export: Nur für das Monitoring-Modul freigeben
exports de.nejathakan.netdiag.internal to com.infrastructure.monitor;
// 5. Reflektiver Zugriff (Deep Reflection für Test-Engines & Serializer)
opens de.nejathakan.netdiag to org.junit.platform.commons;
}
3. Industrielle Testarchitektur: JUnit 5 & Mockito 5
In der professionellen Softwareentwicklung gilt ungetesteter Code als nicht einsatzfähig. Modernes Java setzt auf JUnit 5 (Jupiter) und Mockito 5.
JUnit 5 Architektur
JUnit 5 trennt das Ausführungssystem strikt vom Programmiermodell: 1. JUnit Platform: Die Laufzeit-Basis zum Starten von Test-Frameworks auf der JVM. 2. JUnit Jupiter: Die moderne API und das Extension-Modell für Java 8+. 3. JUnit Vintage: Abwärtskompatibilitäts-Engine für alte JUnit 3- und 4-Tests.
Parametrisierte Tests (@ParameterizedTest)
Parametrisierte Tests ermöglichen es, denselben Prüfalgorithmus über umfangreiche Datensätze ohne Codeduplikation auszuführen:
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class MathTest {
@ParameterizedTest(name = "Präfix /{0} muss Netzmaske {1} ergeben")
@CsvSource({
"24, 255.255.255.0",
"16, 255.255.0.0",
"8, 255.0.0.0",
"30, 255.255.255.252",
"32, 255.255.255.255"
})
void testPrefixToNetmaskConversion(int prefix, String expectedMask) {
assertEquals(expectedMask, calculateNetmask(prefix));
}
private String calculateNetmask(int prefix) {
int mask = prefix == 0 ? 0 : 0xFFFFFFFF << (32 - prefix);
return String.format("%d.%d.%d.%d",
(mask >>> 24) & 0xFF,
(mask >>> 16) & 0xFF,
(mask >>> 8) & 0xFF,
mask & 0xFF
);
}
}
4. Hands-On Systems Lab: network-diagnostics IPv4-Subnetz-Rechner
Wir errichten nun ein vollständig lauffähiges Maven-Projekt mit JPMS-Modularisierung und lückenloser parametrisierter Testabdeckung mit JUnit 5:
- Führt bitweise vorzeichenlose 32-Bit-Arithmetik für IPv4-Adressen aus.
- Berechnet Netzadresse, Broadcastadresse, Subnetzmaske und nutzbare Hostzahlen.
- Behandelt RFC 3021 (Punkt-zu-Punkt /31-Links) und Single-Host /32-Routen korrekt.
- Enthält die vollständige pom.xml, module-info.java, Fachklassen und automatisierte Testfälle.
Verzeichnisstruktur auf der Festplatte
network-diagnostics/
├── pom.xml
└── src/
├── main/
│ └── java/
│ ├── module-info.java
│ └── de/nejathakan/netdiag/
│ ├── SubnetDetails.java
│ └── SubnetCalculator.java
└── test/
└── java/
└── de/nejathakan/netdiag/
└── SubnetCalculatorTest.java
Schritt 1: Projekt-Deskriptor (pom.xml)
<?xml version="1.0" encoding="UTF-8"?>
<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
https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>de.nejathakan</groupId>
<artifactId>network-diagnostics</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.release>21</maven.compiler.release>
<junit.jupiter.version>5.10.2</junit.jupiter.version>
</properties>
<dependencies>
<!-- JUnit 5 Jupiter Aggregator -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>${junit.jupiter.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<!-- Maven Compiler Plugin konfiguriert für Java 21 LTS -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version>
<configuration>
<release>21</release>
</configuration>
</plugin>
<!-- Maven Surefire Plugin zur Testausführung auf dem JPMS-Modulpfad -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.2.5</version>
</plugin>
</plugins>
</build>
</project>
Schritt 2: Der Moduldeskriptor (module-info.java)
module de.nejathakan.netdiag {
// Öffentliches API-Paket exportieren
exports de.nejathakan.netdiag;
// Reflektiven Zugriff für JUnit 5 Testläufer öffnen
opens de.nejathakan.netdiag to org.junit.platform.commons;
}
Schritt 3: Unveränderliches Datenmodell (SubnetDetails.java)
package de.nejathakan.netdiag;
/**
* Unveränderlicher Datenträger (Record) für IPv4-Subnetzmetriken.
*/
public record SubnetDetails(
String cidrInput,
String ipAddress,
int prefixLength,
String netmask,
String networkAddress,
String broadcastAddress,
String firstUsableIp,
String lastUsableIp,
long totalHosts,
long usableHosts
) {}
Schritt 4: Bitweise Subnetz-Berechnungsengine (SubnetCalculator.java)
package de.nejathakan.netdiag;
import java.util.regex.Pattern;
public class SubnetCalculator {
private static final Pattern CIDR_PATTERN = Pattern.compile(
"^([0-9]{1,3}\\.){3}[0-9]{1,3}/([0-9]|[1-2][0-9]|3[0-2])$"
);
/**
* Parst einen IPv4-CIDR-String und liefert berechnete Subnetz-Metadaten.
*
* @param cidr CIDR-String (z.B. "192.168.1.50/24")
* @return SubnetDetails mit exakten Adress- und Hostgrenzen
*/
public SubnetDetails calculate(String cidr) {
if (cidr == null || !CIDR_PATTERN.matcher(cidr.trim()).matches()) {
throw new IllegalArgumentException("Ungültiges CIDR-Format. Erwartet wird IP/Präfix (z.B. 192.168.1.0/24)");
}
String[] parts = cidr.trim().split("/");
String ipStr = parts[0];
int prefix = Integer.parseInt(parts[1]);
int ipInt = parseIpv4StringToInt(ipStr);
int netmaskInt = prefix == 0 ? 0 : 0xFFFFFFFF << (32 - prefix);
int networkInt = ipInt & netmaskInt;
int broadcastInt = networkInt | ~netmaskInt;
long totalHosts = 1L << (32 - prefix);
long usableHosts;
String firstUsable;
String lastUsable;
if (prefix == 32) {
usableHosts = 1;
firstUsable = formatIntToIpv4(networkInt);
lastUsable = formatIntToIpv4(networkInt);
} else if (prefix == 31) {
// RFC 3021 Punkt-zu-Punkt-Links
usableHosts = 2;
firstUsable = formatIntToIpv4(networkInt);
lastUsable = formatIntToIpv4(broadcastInt);
} else {
usableHosts = totalHosts - 2;
firstUsable = formatIntToIpv4(networkInt + 1);
lastUsable = formatIntToIpv4(broadcastInt - 1);
}
return new SubnetDetails(
cidr.trim(),
ipStr,
prefix,
formatIntToIpv4(netmaskInt),
formatIntToIpv4(networkInt),
formatIntToIpv4(broadcastInt),
firstUsable,
lastUsable,
totalHosts,
usableHosts
);
}
// Wandelt "X.X.X.X" in ein 32-Bit Integer um
public static int parseIpv4StringToInt(String ip) {
String[] octets = ip.split("\\.");
int result = 0;
for (int i = 0; i < 4; i++) {
int octet = Integer.parseInt(octets[i]);
if (octet < 0 || octet > 255) {
throw new IllegalArgumentException("Oktett außerhalb des Bereichs [0-255]: " + octet);
}
result = (result << 8) | octet;
}
return result;
}
// Wandelt ein 32-Bit Integer zurück in die Dotted-Decimal-Notation
public static String formatIntToIpv4(int ip) {
return String.format("%d.%d.%d.%d",
(ip >>> 24) & 0xFF,
(ip >>> 16) & 0xFF,
(ip >>> 8) & 0xFF,
ip & 0xFF
);
}
}
Schritt 5: JUnit 5 Testsuite (SubnetCalculatorTest.java)
package de.nejathakan.netdiag;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import org.junit.jupiter.params.provider.ValueSource;
import static org.junit.jupiter.api.Assertions.*;
@DisplayName("IPv4 Subnetz-Rechner Test-Suite")
class SubnetCalculatorTest {
private SubnetCalculator calculator;
@BeforeEach
void setUp() {
calculator = new SubnetCalculator();
}
@Nested
@DisplayName("Standard-Subnetzberechnungen")
class StandardSubnetTests {
@ParameterizedTest(name = "{0} -> Netz: {1}, Maske: {2}, Hosts: {3}")
@CsvSource({
"192.168.1.100/24, 192.168.1.0, 255.255.255.0, 192.168.1.255, 254",
"10.0.50.1/16, 10.0.0.0, 255.255.0.0, 10.0.255.255, 65534",
"172.16.8.99/20, 172.16.0.0, 255.255.240.0, 172.16.15.255, 4094",
"192.168.0.1/30, 192.168.0.0, 255.255.255.252, 192.168.0.3, 2"
})
void testValidSubnetCalculations(
String cidr,
String expectedNet,
String expectedMask,
String expectedBcast,
long expectedUsable
) {
SubnetDetails details = calculator.calculate(cidr);
assertAll("Subnetz-Eigenschaften",
() -> assertEquals(expectedNet, details.networkAddress()),
() -> assertEquals(expectedMask, details.netmask()),
() -> assertEquals(expectedBcast, details.broadcastAddress()),
() -> assertEquals(expectedUsable, details.usableHosts())
);
}
}
@Nested
@DisplayName("RFC 3021 und Edge-Case Subnetze")
class EdgeCaseSubnetTests {
@Test
@DisplayName("RFC 3021 /31 Subnetz hat 2 nutzbare Hosts ohne Broadcast")
void testRfc3021Subnet() {
SubnetDetails details = calculator.calculate("10.0.0.0/31");
assertEquals("10.0.0.0", details.firstUsableIp());
assertEquals("10.0.0.1", details.lastUsableIp());
assertEquals(2, details.usableHosts());
}
@Test
@DisplayName("/32 Host-Route hat 1 Host und entspricht der Eingangs-IP")
void testSingleHostRoute() {
SubnetDetails details = calculator.calculate("192.168.1.1/32");
assertEquals("192.168.1.1", details.networkAddress());
assertEquals("255.255.255.255", details.netmask());
assertEquals(1, details.usableHosts());
}
}
@Nested
@DisplayName("Validierung und Ausnahmebehandlung")
class ExceptionHandlingTests {
@ParameterizedTest(name = "Ungültiges CIDR: {0}")
@ValueSource(strings = {
"invalid-ip",
"256.0.0.1/24",
"192.168.1.1/33",
"10.0.0.1/-1",
"",
"192.168.1.1"
})
void testInvalidCidrInputs(String invalidInput) {
assertThrows(IllegalArgumentException.class, () -> calculator.calculate(invalidInput));
}
}
}
Ausführen und Validieren des Projekts unter Linux
Öffne ein Terminal im Verzeichnis network-diagnostics und führe den Maven-Testzyklus aus:
Erwartete Ausgabe:
[INFO] -------------------------------------------------------
[INFO] T E S T S
[INFO] -------------------------------------------------------
[INFO] Running de.nejathakan.netdiag.SubnetCalculatorTest
[INFO] Tests run: 12, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.128 s -- in de.nejathakan.netdiag.SubnetCalculatorTest
[INFO]
[INFO] Results:
[INFO]
[INFO] Tests run: 12, Failures: 0, Errors: 0, Skipped: 0
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
Nächste Schritte
Mit Build-Automatisierung, Modulkapselung und robuster Testabdeckung gerüstet, vertiefen wir im nächsten Kapitel dynamische Datenstrukturen und Typensicherheit:
-
Teil 2.1: Collections Framework, Generics & Speichermodell
HashMap-Bucket-Treeification, Generics-Kovarianz und Kontravarianz (PECS) beherrschen und einen speichereffizienten LRU-Cache entwickeln.