main_java

JPMS in Aktion - jeeeraaah

JPMS (Java Platform Module System) ist eine Technologie zur Modularisierung von Java Anwendungen. Es wurde 2017 mit der Java Version 9 veröffentlicht.

Für das JDK selbst wird JPMS meist als großer Erfolg gewertet, da es seit dem nicht mehr als ein einziger riesiger Monolith (rt.jar) ausgeliefert werden muss, der schon aufgrund seiner Größe nicht mehr zum sich immer weiter verbreitenden Architekturmodell Microservices passte.

In der Java User Community hingegen kämpft JPMS aus verschiedenen Gründen weiter um Akzeptanz:

Modularisierung ist aber ein entscheidender Faktor für die Entwicklung von gut wartbaren, gut verständlichen und gut erweiterbaren, großen Softwaresystemen (siehe den Beitrag modular software in java).

Das Projekt jeeeraaah wurde als “proof of concept” (POC) für die Möglichkeit der Verwendung von JPMS in Enterprise Java Systemen gestartet. Ziel ist, anhand einer überschaubaren, aber nicht trivialen Anwendung zu überprüfen, ob und wie Modularisierung großer Java Applikationen mit JPMS eine valide Alternative zu anderen Architekturansätzen wie z. B. Microservices ist.

Gleichzeitig soll kritisch geprüft werden, ob die Vorteile von Modularisierung mit JPMS die Nachteile überwiegen, z. B. die Komplexität der Modularisierung selbst, die Komplexität der Build- und Deployment-Prozesse, … .

Fachlich geht es im Projekt jeeeraaah im Kern um die Verwaltung von Aufgaben (Tasks) und die Planung von Arbeitsabläufen. Dazu sollen zusammengehörige Tasks in Gruppen (TaskGroups) organisiert werden. Abb. 1 zeigt das zentrale Objektmodell:

TaskGroup - Task
Abb. 1: UML - TaskGroup-Task

Die Idee ist, Aufgaben in Teilaufgaben zu gliedern (Tasks und SubTasks) und für alle Aufgaben Abläufe (Predecessor- und Successor-Tasks) planen zu können.

Task-Objects
Abb. 2: Task-Objekte

In der Anwendung sieht das dann im dashboard etwa so aus:

Task-Objects
Abb. 3: jeeeraaah dashboard

Eine Gantt-Diagramm-Darstellung zeigt eine andere Sicht auf Aufgaben und die geplanten Abläufe:

Task-Objects
Abb. 4: jeeeraaah Gantt Diagramm

Der Technologiestack

Ein Ziel des POCs ist, die Versionen der eingesetzten Technologien dauerhaft auf einem möglichst modernen Stand zu halten. Updates aller Technologien gehören daher zur Tagesordnung.

jeeeraaah ist eine client-server Java Anwendung, deren Bestandteile (bis auf eine Ausnahme, dazu später mehr) mit Java 25 entwickelt wurden. Dabei kommen aktuell folgende Technologien zum Einsatz:

Das Backend ist eine Jakarta EE 10 / Microprofile 6.1 Anwendung. Als Application Server wird Open Liberty verwendet. Im Frontend kommt JavaFX 25 zum Einsatz.

Frontend und Backend sind weitestgehend mit JPMS modularisiert. Die Kommunikation zwischen ihnen erfolgt über REST APIs, die mit Jakarta-RS implementiert wurden. Die (De-) Serialisierung der Daten erfolgt mit Jackson, was einen komfortablen und gleichzeitig effizienten Umgang auch mit zirkulären Datenstrukturen (siehe Task/TaskGroup Objektmodell) erlaubt. Die build Prozesse für beide Anwendungen werden mit Apache Maven realisiert.

Für das Identity and Access Management (IAM) wird Keycloak verwendet. Das frontend kommuniziert direkt mit Keycloak, um die Authentifizierung der Benutzer durchführen zu lassen. Das Open Liberty backend ist so konfiguriert, dass es die von Keycloak ausgestellten Token akzeptiert und die Autorisierung für alle eingehenden Requests durchführen kann.

Die persistente Datenhaltung im Backend wird mit einer Postgres Datenbank realisiert. Sie wird genau wie Keycloak in einem von docker-compose orchestrierten Container betrieben. In diesem POC liegen die jeeeraaah- zusammen mit den keycloakk-Daten in ein und derselben Datenbank, sie sind aber jeweils explizit einem eigenen Schema zugeordnet. Die jeeeraaah Zugriffe auf die Datenbank sind durchgängig mit JPA (hibernate) umgesetzt.

Die Modulstruktur

jeeeraaah/
├── backend/                    # Server-Komponenten
│   ├── api/ws_rs/              # REST API Server (Open Liberty)
│   ├── persistence/            # JPA Entities & Repositories
│   └── common/                 # gemeinsame Backend-Klassen, Mappings DTO <-> JPA
├── frontend/                   # Client-Komponenten
│   ├── api.client/ws_rs/       # REST API Client
│   ├── ui/fx/                  # JavaFX UI
│   └── common/                 # gemeinsame Frontend-Klassen, Mappings DTO <-> Bean <-> JavaFXBean
└── common/api/                 # API Domain Model Types (geteilt)

Bis auf das maven Modul r-uu.app.jeeeraaah.backend.api.ws_rs sind alle Module mit JPMS modularisiert. Warum das Modul r-uu.app.jeeeraaah.backend.api.ws_rs eine Ausnahme ist, wird in module backend beschrieben.

Architektur

Das Backend ist in zwei Maven Hauptmodule aufgeteilt: api und persistence. Das api Modul enthält die REST API Schnittstellen, die mit Jakarta-RS implementiert wurden. Im persistence Modul befindet sich die Datenzugriffsschicht, die mit JPA (hibernate) implementiert wurde.

Das Frontend ist ebenfalls in zwei Maven Module aufgeteilt: ui und api.client. Das ui Modul enthält die JavaFX Komponenten, die für die Darstellung der Benutzeroberfläche verantwortlich sind. Das api.client Modul enthält die Logik für die Kommunikation mit dem backend über REST APIs.

Das Bindeglied zwischen Frontend und Backend ist das maven Modul common, das Objekte und Objekt-Mappings enthält, die von beiden Seiten verwendet werden.

Modul common

Das common.api.domain Maven Modul enthält zentrale Schnittstellen und Basisklassen des Domänenmodells. Dieses Modul bildet das Fundament für das jeeeraaah Task-Management-System und definiert:

Das Modul ist so konzipiert, dass es als Bindeglied zwischen Frontend und Backend fungiert, um ein konsistentes Domänenmodell über alle Anwendungsschichten hinweg zu gewährleisten.

Der Aufbau des Moduls spiegelt die Struktur des gesamten Projekts wider:

Ergänzend zu den Submodulen enthält das common Modul noch das Submodul ...common.api.mapping, in dem die Mappings zwischen Java-Beans und DTOs definiert werden. Die Mappings werden aktuell mit MapStruct implementiert.


Hinweis 1: möglicher Verzicht auf DTOs Es ist durchaus denkbar, dass die Bean-Implementierungen aus `...common.api.bean` auch für die Realisierung von **DTO**s verwendet werden. In diesem Fall könnte das `...common.api.ws_rs` Submodul entfallen. Aktuell ist es aber so, dass die **DTO**s und die Bean-Implementierungen getrennt sind, um eine klare Trennung zwischen den beiden Schichten zu gewährleisten.

Hinweis 2: möglicher Verzicht auf MapStruct Die **MapStruct** Mappings implementieren die Umwandlung aktuell quasi "manuell", d. h. die typischen **MapStruct** Features wie automatisches Mapping von gleichnamigen Feldern oder die Verwendung von Mapping-Methoden für die Umwandlung von komplexeren Objekten werden nicht bzw. nur sehr eingeschränkt genutzt. Das hat sich in diesem Projekt im Laufe der Zeit in diese Richtung entwickelt. Im Nachhinein wäre ein Verzicht auf **MapStruct** und die direkte Implementierung der Mappings von Hand wahrscheinlich die bessere Wahl gewesen, da die Verwendung von **MapStruct** hier mehr Komplexität z. B. im Build-Prozess mit sich bringt und die typischen Vorteile von **MapStruct** durch automatisierte Code-Generierung für die Umwandlung zumindest aktuell nicht zum Tragen kommt. Die Implementierung funktioniert allerdings, ist gut getestet und es ist durchaus denkbar, dass durch zukünftige Erweiterung des **jeeeraaah** Objektmodells die typischen Vorteile von **Mapstruct** wieder zu Tage treten.

Modul backend

Das Backend Maven Modul besteht wieder aus zwei Hauptmodulen: ...api.ws_rs und ...persistence.

Das ...api.ws_rs Modul enthält die REST API Schnittstellen, die mit Jakarta-RS implementiert wurden. Es ist das einzige Maven Modul im Projekt jeeeraaah, das nicht mit JPMS implementiert wurde.

Der Grund hierfür liegt in der WAR Deployment Architektur, die Standard für Jakarta EE Application Server wie Open Liberty ist. Jakarta EE Application Server deployen WAR-Dateien standardmäßig auf dem classpath. Theoretisch ließe sich das WAR auch mit JPMS bauen und auf dem modulepath deployen. Die Jakarta EE Server APIs, mit denen das WAR interagiert, sind aber selbst nicht JPMS konform, was dazu führt, dass die JPMS Kapselungsmechanismen nicht greifen würden. Da JPMS in diesem Kontext also keine signifikanten Vorteile bringen würde, wurde auf die in diesem Fall entstehende zusätzlische Komplexität für das Deployment des Moduls mit JPMS verzichtet.

Im ...persistence Maven Modul befindet sich die mit JPA (hibernate) implementierte Datenzugriffsschicht. Auch hier gibt es ein ...common Modul, das die Mappings zwischen JPA-Entity-Typen und Jakarta-WS-RS-DTOs definiert.

Modul frontend

Das frontend ist ebenfalls in zwei Maven Module aufgeteilt: ...ui und ...api.client. Das ...ui Modul enthält die JavaFX Komponenten, die für die Darstellung der Benutzeroberfläche verantwortlich sind. Das ...api.client Modul enthält die Logik für die Kommunikation mit dem backend über REST APIs. Auch hier gibt es ein ...common Modul, das die Mappings zwischen JavaFX-Objekten und Jakarta-RS-DTOs definiert.

Konkrete Vorteile von JPMS im Projekt jeeeraaah

Quantitative Kapselungsmetriken (Stand: 28. Februar 2026)

Die folgende Beschreibung von Metriken für die Kapselung beziehen sich auf ein Refactoring, das unter starker Zuhilfenahme von Agentic KI mit claude sonnet 4.5 durchgeführt wurde. Nicht alle quantitativen Angaben wurden manuell geprüft. Durch zwischenzeitliche weitere Refactorings und weitere Optimierungen können sich die Werte (weiter positiv) verändern.

Die jeeeraaah Anwendung besteht aus 10 JPMS-Modulen, die zusammen 24 Packages exportieren (drastisch reduziert von vorher 46 Packages). Von insgesamt 149 public Typen (Klassen, Interfaces, Enums, Records) sind:

Diese Kapselungsrate von 53.7% zeigt den konsequenten Einsatz von JPMS zur Kapselung von Implementierungsdetails. Über die Hälfte aller public Typen bleibt verborgen und ist nur intern verfügbar.

Verbesserung gegenüber vorherigem Stand:

App-Module mit versteckten Implementierungsklassen

...frontend.ui.fx – 72 versteckte public Klassen (größte Verbesserung! 🎯)

backend.persistence.jpa – 10 versteckte public Klassen

backend.common.mapping.jpa.dto – 3 versteckte public Klassen

frontend.api.client.ws.rs – 1 versteckte public Klasse

Starke Kapselung durch gezielte Package-Exports

Durch JPMS können Module explizit definieren, welche Packages nach außen sichtbar sind (exports) und welche intern bleiben. Dies verhindert ungewollte Abhängigkeiten und fördert saubere Architekturen.

Beispiel: backend.persistence.jpa – Service-Implementation-Hiding mit Dual-Export-Strategie

Das Modul nutzt eine Dual-Export-Strategie: Service-Interfaces werden public exportiert, während JPA-Entities nur qualifiziert an autorisierte Module exportiert werden:

module de.ruu.app.jeeeraaah.backend.persistence.jpa {
    // Public Export: Service-Interfaces und Utilities
    exports de.ruu.app.jeeeraaah.backend.persistence.jpa;
    
    // Qualified Export: JPA-Entities (nur an autorisierte Module)
    exports de.ruu.app.jeeeraaah.backend.persistence.jpa.entity
         to de.ruu.app.jeeeraaah.backend.common.mapping.jpa.dto,
            de.ruu.app.jeeeraaah.backend.api.ws.rs;
    
    // Implementierungsdetails vollständig versteckt:
    // - internal: TaskServiceJPA, TaskGroupServiceJPA, TaskRepositoryJPA, TaskGroupRepositoryJPA
    // - ee: TaskServiceJPAEE, TaskGroupServiceJPAEE, TaskRepositoryJPAEE, TaskGroupRepositoryJPAEE
    
    // CDI-Zugriff über 'opens' ermöglichen (kein compile-time-import!)
    opens de.ruu.app.jeeeraaah.backend.persistence.jpa.entity 
       to org.hibernate.orm.core;
    opens de.ruu.app.jeeeraaah.backend.persistence.jpa.ee 
       to weld.se.shaded;
    opens de.ruu.app.jeeeraaah.backend.persistence.jpa.internal 
       to weld.se.shaded;
}

Externe Module (z.B. Frontend) sehen nur:

Vorteile dieser Architektur:

  1. Compile-time Safety: REST-Controller können Service-Implementierungen nicht direkt importieren
  2. Interface-basierte Programmierung: Erzwingt Programmierung gegen Abstraktion statt Implementierung
  3. Entity-Schutz: JPA-Entities sind nicht frei verfügbar, sondern nur für autorisierte Module
  4. DTO-basierte REST-API: Strikte Trennung zwischen Persistence Layer (JPA) und API Layer (DTOs)
  5. Flexibilität: Implementierungen können ausgetauscht werden ohne API-Changes
  6. Wartbarkeit: Klare Trennung zwischen öffentlicher API, qualifiziert exportierten Entities und privater Implementierung

Externe Module sehen NICHT:

Resultat: Änderungen an Implementierungsdetails (z.B. Umbenennung von SimpleExpressionBasicExpression) betreffen nur das Modul selbst, keine Clients.

Explizite Abhängigkeiten

Jedes Modul deklariert seine Abhängigkeiten mit requires, was die Abhängigkeitsstruktur transparent macht und zirkuläre Abhängigkeiten zur Compile-Zeit verhindert.

Statistik für jeeeraaah-App: Über die 10 App-Module hinweg werden explizite requires-Direktiven verwendet, um Abhängigkeiten zu deklarieren. Dies umfasst:

Verbesserte Wartbarkeit

Die klare Modultrennung (z.B. ...common.api.domain, ...backend.persistence, ...frontend.ui) erleichtert das Verständnis der Architektur und ermöglicht gezielte Änderungen ohne unerwartete Seiteneffekte.

Beispiel: Änderungen in backend.persistence.jpa

Durch die Modularisierung können mit jlink Custom Runtime Images erstellt werden, die nur die tatsächlich benötigten Module enthalten. Dies reduziert die Deployment-Größe erheblich.

Beispiel aus dem Projekt:

Compile-Time-Validierung

JPMS prüft bereits zur Compile-Zeit, ob alle Abhängigkeiten aufgelöst werden können und ob auf nicht exportierte Packages zugegriffen wird. Dies verhindert viele Runtime-Fehler.

Konkrete Beispiele aus dem Projekt:

  1. Fehlende Exports sofort erkannt: Versuch, criteria.restriction.SimpleExpression direkt zu importieren, schlägt zur Compile-Zeit fehl
  2. Module-not-found Fehler: Fehlende requires-Direktive wird sofort erkannt (z.B. requires org.slf4j)
  3. Zirkuläre Abhängigkeiten unmöglich: JPMS verhindert A → B → A zur Compile-Zeit

Klare Schnittstellen

Die Verwendung von module-info.java erzwingt eine bewusste Entscheidung, welche Packages öffentlich sind. Dies führt zu besser durchdachten APIs und minimiert die Gefahr von ungewollten Abhängigkeiten.

Beispiel aus common.api.domain:

module de.ruu.app.jeeeraaah.common.api.domain {
    exports de.ruu.app.jeeeraaah.common.api.domain;
    exports de.ruu.app.jeeeraaah.common.api.domain.exception;
    exports de.ruu.app.jeeeraaah.common.api.domain.flat;
    exports de.ruu.app.jeeeraaah.common.api.domain.lazy;
    
    // Alle anderen Packages bleiben verborgen
}

Resultat: Nur 4 Packages mit Domain-Interfaces sind öffentlich, alle internen Hilfsklassen bleiben verborgen.

Transitive Dependencies Management

JPMS erlaubt präzise Kontrolle über transitive Abhängigkeiten durch requires transitive. Module, die eine API exportieren, können sicherstellen, dass konsumierende Module automatisch Zugriff auf benötigte Typen haben.

Beispiel: Das Modul ...common.api.domain deklariert requires transitive de.ruu.lib.jpa.core, sodass alle Module, die ...common.api.domain verwenden, automatisch Zugriff auf JPA-Core-Typen haben – ohne diese explizit zu deklarieren.

Statistik: Im Projekt werden 23 requires transitive-Direktiven verwendet, um API-Boundaries sauber zu definieren.

Gezielte Reflection-Zugriffe

Mit opens können gezielt nur bestimmte Packages für bestimmte Frameworks geöffnet werden, anstatt alles über den Classpath zugänglich zu machen.

Beispiel aus backend.persistence.jpa:

// Nur für Hibernate und Weld (CDI), nicht für alle
opens de.ruu.app.jeeeraaah.backend.persistence.jpa 
   to org.hibernate.orm.core, weld.se.shaded;

// EE-Implementierungen nur für CDI
opens de.ruu.app.jeeeraaah.backend.persistence.jpa.ee 
   to weld.se.shaded;

Dies minimiert die Angriffsfläche und erhält maximale Kapselung, wo Reflection nicht benötigt wird.

Statistik: Das Projekt verwendet 27 qualifizierte opens-Direktiven (nur für spezifische Frameworks) und vermeidet weitestgehend unqualifizierte opens (an alle).

Vermeidung von Split Packages

JPMS erzwingt, dass ein Package nur in einem Modul existieren kann. Dies verhindert das “Split Package Problem”, bei dem verschiedene JARs Klassen im gleichen Package liefern, was zu Klassenkonflikten führen kann.

Beispiel aus dem Projekt:

Resultat für jeeeraaah-App: Über alle 10 App-Module und 24 exportierte Packages hinweg gibt es null Split-Package-Konflikte.

Im jeeeraaah-Projekt hat dies zu einer saubereren Package-Struktur geführt, bei der jedes Modul einen eindeutigen Package-Namespace besitzt.

Compile-Time Dependency Graph

Der Module-Graph ist bereits zur Build-Zeit vollständig bekannt. Maven und IntelliJ können Abhängigkeitsprobleme sofort erkennen, noch bevor die Anwendung gestartet wird.

Konkrete Erfahrung: Fehlende requires-Deklarationen werden bereits beim Kompilieren erkannt, nicht erst zur Laufzeit mit ClassNotFoundException.

Service Encapsulation

JPMS ermöglicht es, Service-Implementierungen vollständig zu verbergen und nur Interfaces zu exportieren. Dies fördert lose Kopplung und austauschbare Implementierungen.

Beispiel: Das ...backend.persistence.jpa Modul exportiert nur seine Services, nicht die internen JPA-Entity-Implementierungsdetails.

Bessere IDE-Unterstützung

IntelliJ IDEA nutzt die JPMS-Deklarationen für:

Dokumentation durch Code

Die module-info.java Dateien dienen als selbstdokumentierende Architekturübersicht:

Beispiel: Durch Lesen der module-info kann ein neuer Entwickler sofort die Architektur verstehen, ohne externe Dokumentation zu benötigen.

Versionskonflikte minimieren

Da jedes Modul explizit seine Abhängigkeiten deklariert, werden Versionskonflikte früher erkennbar. Die Kombination mit Maven’s BOM (Bill of Materials) ermöglicht zentrale Versionsverwaltung bei gleichzeitiger modularer Klarheit.

Performance-Optimierung zur Laufzeit

Die JVM kann bei JPMS-Modulen optimieren:

Mehrschichtige Architektur erzwingen

JPMS macht es unmöglich, gegen die gewünschte Architektur zu verstoßen. Beispiel im jeeeraaah-Projekt:

Dies wird zur Compile-Zeit erzwungen, nicht erst durch Code-Reviews oder Tests.

Konkrete Zahlen aus dem jeeeraaah-Projekt

Metrik Wert Vorteil
Module mit JPMS 10 Klare Strukturierung
Exportierte Packages gesamt 24 Minimale API-Oberfläche (Reduzierung von 46 → 24)
Durchschnittliche exports pro Modul 2.4 Fokussierte öffentliche APIs
Kapselungsrate 53.7% Über die Hälfte aller Typen versteckt
Versteckte public Typen 80 von 149 Implementierungsdetails geschützt
Größte Verbesserung frontend.ui.fx 72 UI-Typen versteckt (23 Packages internal)
JPAFactory Entfernt Test-Utility ohne produktiven Wert eliminiert
Compile-Zeit Fehlerfrüherkennung ~20+ Fehler verhindert Verhinderte Runtime-Fehler
Module-Graph Tiefe 4-5 Ebenen Überschaubare Abhängigkeiten
Qualifizierte opens 27 Direktiven Minimale Reflection-Angriffsfläche
Split-Package-Konflikte 0 Saubere Package-Struktur

Pragmatische Ausnahme: backend.api.ws_rs

Interessanterweise ist ...backend.api.ws_rs bewusst nicht mit JPMS modularisiert. Der Grund: Jakarta EE Server wie Open Liberty deployen WARs traditionell auf dem classpath (nicht dem modulepath). Da die Jakarta EE APIs selbst nicht vollständig JPMS-konform sind, würden die Kapselungsvorteile nicht greifen.

Diese pragmatische Entscheidung zeigt: JPMS wird dort eingesetzt, wo es echten Mehrwert bringt, nicht dogmatisch überall.

Zusammenfassung der Vorteile

Die wichtigsten konkreten Vorteile von JPMS für jeeeraaah sind:

  1. 🛡️ Starke Kapselung (53.7%) - Über die Hälfte aller Implementierungen bleibt verborgen
  2. 📊 Transparente Abhängigkeiten - Der gesamte Dependency-Graph ist explizit
  3. Frühe Fehlererkennung - Viele Fehler werden zur Compile-Zeit gefangen
  4. 📝 Selbstdokumentierend - module-info.java zeigt die Architektur
  5. 🎯 Erzwungene Architektur - Schichttrennung wird technisch durchgesetzt
  6. 🔒 Minimale Reflection - Nur wo nötig, nur für spezifische Frameworks
  7. 🧩 Saubere Modularisierung - Klare Grenzen zwischen Komponenten
  8. 🚀 Zukunftssicher - Vorbereitet für jlink, GraalVM Native Image

Wichtigste Erkenntnisse aus der JPMS-Migration:

JPMS ist im jeeeraaah-Projekt keine theoretische Spielerei, sondern ein praktisches Werkzeug, das täglich hilft, die Architektur sauber zu halten und Fehler früh zu erkennen.