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:
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.
Abb. 2: Task-Objekte
In der Anwendung sieht das dann im dashboard etwa so aus:
Abb. 3: jeeeraaah dashboard
Eine Gantt-Diagramm-Darstellung zeigt eine andere Sicht auf Aufgaben und die geplanten Abläufe:
Abb. 4: jeeeraaah Gantt Diagramm
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.
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.
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.
commonDas 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:
das Submodul ...common.api.domain enthält vor allem die zentralen Interfaces des Domänenmodells, die von beiden Seiten (Frontend und Backend) verwendet werden. Um die Verwendung der Interfaces auf beiden Seiten möglichst konsistent halten zu können, sind sie generisch, was eine starke Typisierung in den implementierenden Klassen ermöglicht.
das Submodul ...common.api.domain.flat enthält “flache” Repräsentationen von Domain-Objekten, die nur Kern-Felder ohne teure Beziehungen enthalten.
das Submodul ...common.api.domain.lazy enthält Lazy-Loading-Varianten, die IDs anstelle von vollständigen Objekten verwenden. Dies ermöglicht verzögertes Laden von Beziehungen und reduziert die Netzwerk- und Speicherlast. Lazy Typen sind für Performance-optimierte Szenarien gedacht, z.B. beim Aufbau von Hierarchien im Gantt-Diagramm.
das Submodul ...common.api.ws_rs enthält die DTO Klassen, mit deren Hilfe frontend und backend kommunizieren. Die DTO Klassen implementieren die generischen Interfaces aus ...common.api.domain.
das Submodul ...common.api.bean enthält (Java-)Bean-Implementierungen der Interfaces aus ...common.api.domain. Genaugenommen sind die Implementierungen keine Java-Beans, da sie fluent accessors anstelle der Java-Beans üblichen get-/set-accessors verwenden. Die Bean-Implementierungen aus diesem Modul sind für die Realisierung von Geschäftslogik im Projekt vorgesehen.
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.
backendDas 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.
frontendDas 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.
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:
...frontend.ui.fx – 72 versteckte public Klassen (größte Verbesserung! 🎯)
...frontend.ui.fx (für MainAppRunner - Classpath-Zugriff)auth, dash, task, task.edit, task.gantt, task.selector, task.view.*, taskgroup.*, test, utilopens, nicht exports!opens-Direktiven ermöglicht (25 Packages geöffnet, aber nicht exportiert)exports (compile-time API) und opens (runtime reflection)backend.persistence.jpa – 10 versteckte public Klassen
...jeeeraaah.backend.persistence.jpa.entity mit 2 JPA-Entities
TaskJPA, TaskGroupJPA...backend.common.mapping.jpa.dto und ...backend.api.ws.rs...backend.persistence.jpa.ee mit 4 CDI-Bean-Implementierungen
TaskServiceJPAEE, TaskGroupServiceJPAEE, TaskRepositoryJPAEE, TaskGroupRepositoryJPAEE...backend.persistence.jpa.internal mit 4 Service-/Repository-Implementierungen
TaskServiceJPA, TaskGroupServiceJPA, TaskRepositoryJPA, TaskGroupRepositoryJPATaskCreationService, TaskLazyMapper, TaskDTOService, TaskGroupDTOService, TaskRelationService)backend.common.mapping.jpa.dto – 3 versteckte public Klassen
...backend.common.mapping.lazy.jpa für interne Lazy-Loading-Mapperfrontend.api.client.ws.rs – 1 versteckte public Klasse
...frontend.api.client.ws.rs.example für Beispiel-CodeDurch 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:
TaskCreationService, TaskLazyMapper, JPAFactoryTaskJPA, TaskGroupJPA (nur für Mapping/API-Module zugänglich)Vorteile dieser Architektur:
Externe Module sehen NICHT:
SimpleExpression, LogicalExpression, JunctionResultat: Änderungen an Implementierungsdetails (z.B. Umbenennung von SimpleExpression → BasicExpression) betreffen nur das Modul selbst, keine Clients.
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:
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
ee/ Package) bleiben vollständig gekapseltTaskJPA, TaskGroupJPA) sind exportiert und für Mapping-Module sichtbarDurch 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:
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:
criteria.restriction.SimpleExpression direkt zu importieren, schlägt zur Compile-Zeit fehlrequires-Direktive wird sofort erkannt (z.B. requires org.slf4j)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.
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.
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).
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:
...backend.persistence.jpa existiert nur in einem Modul.ee, .internal würden ebenfalls diesem Modul zugeordnetResultat 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.
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.
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.
IntelliJ IDEA nutzt die JPMS-Deklarationen für:
Die module-info.java Dateien dienen als selbstdokumentierende Architekturübersicht:
requiresexportsopensBeispiel: Durch Lesen der module-info kann ein neuer Entwickler sofort die Architektur verstehen, ohne externe Dokumentation zu benötigen.
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.
Die JVM kann bei JPMS-Modulen optimieren:
JPMS macht es unmöglich, gegen die gewünschte Architektur zu verstoßen. Beispiel im jeeeraaah-Projekt:
...common.api.ws_._rs) ist Kommunikation möglichDies wird zur Compile-Zeit erzwungen, nicht erst durch Code-Reviews oder Tests.
| 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 |
backend.api.ws_rsInteressanterweise 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.
Die wichtigsten konkreten Vorteile von JPMS für jeeeraaah sind:
Wichtigste Erkenntnisse aus der JPMS-Migration:
opens vs exports verstehen: Frameworks brauchen Reflection (opens), keine compile-time API (exports)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.