Zum Inhalt

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 transitive und 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 zu ClassNotFoundException. 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 in module-info.java explizit 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:

Parametrisierter Test mit CSV-Datenquellen
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)

network-diagnostics/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)

network-diagnostics/src/main/java/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)

network-diagnostics/src/main/java/de/nejathakan/netdiag/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)

network-diagnostics/src/main/java/de/nejathakan/netdiag/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)

network-diagnostics/src/test/java/de/nejathakan/netdiag/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:

Automatisierte Tests kompilieren und ausführen
mvn clean test

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.

    Weiter zu Teil 2.1