Java-Module (JPMS) – Einführung
Was Module in Java sind, welche Probleme JPMS löst und wie es sich zum Classpath verhält.
Das Java Platform Module System (JPMS), eingeführt in Java 9, fügt eine Ebene oberhalb von Paketen hinzu. Ein Modul ist eine benannte, selbstbeschreibende Gruppe von Paketen, die zwei Dinge explizit deklariert: was es von anderen Modulen benötigt und was es ihnen anbietet. Diese Deklaration befindet sich in einer einzigen Datei, module-info.java, im Stammverzeichnis des Moduls. Dieser Teil des Buchs führt durch das Thema; dieses Kapitel erklärt, warum es existiert.
Das Problem, das JPMS löst: der Classpath
Vor Java 9 wurde jedes JAR auf einen einzigen flachen Classpath geworfen. Diese Anordnung hatte chronische Probleme:
- Keine Kapselung. Jede
public-Klasse in jedem JAR war für jeden erreichbar. Eine Klasse, die nur als interner Helfer gedacht war (sun.misc.Unsafe,com.example.internal.*), konnte von jedem verwendet werden und konnte daher nie sicher geändert werden. - Keine deklarierten Abhängigkeiten. Ein JAR gab nie an, welche anderen JARs es benötigte. Eine fehlende Abhängigkeit entdeckte man erst, wenn ein
NoClassDefFoundErrorzur Laufzeit auftrat — möglicherweise in der Produktion. - JAR-Hölle. Zwei JARs, die dasselbe Paket liefern, oder zwei Versionen derselben Bibliothek wurden stillschweigend in der Classpath-Reihenfolge zusammengeführt. Welche Klasse zuerst geladen wurde, gewann.
Module bekämpfen alle drei Probleme: Ein Modul versteckt jedes Paket, das es nicht explizit exportiert, deklariert jedes Modul, das es requires, und die JVM überprüft den gesamten Graphen beim Start — fehlende oder doppelte Module schlagen sofort fehl.
Module vs. Pakete vs. JARs
Diese drei sind leicht zu verwechseln:
| Konzept | Was es gruppiert | Sichtbarkeitsregel |
|---|---|---|
| Paket | Klassen | public/protected/paketprivat innerhalb des JARs |
| JAR | Pakete + Ressourcen | alles public ist auf dem Classpath sichtbar |
| Modul | Pakete | nur exported-Pakete sind für andere Module sichtbar |
Ein Modul wird normalerweise als JAR verpackt (ein „modulares JAR" — ein gewöhnliches JAR mit einer module-info.class in seinem Stammverzeichnis). Der Unterschied liegt im Deskriptor: Legt man das JAR auf den Classpath, werden die Regeln ignoriert; legt man es auf den Modulpfad, erzwingt JPMS sie.
Starke Kapselung, in einem Satz
Die Hauptregel: Ein Paket ist für andere Module unsichtbar, sofern sein Modul es nicht exportiert — auch wenn seine Klassen public sind. public bedeutet jetzt „zugänglich für Code, der dieses Paket lesen kann", und das Lesen eines Pakets erfordert ein exports sowie ein requires. Deshalb konnte das JDK selbst schließlich seine internen Bestandteile verbergen: java.base exportiert java.util, aber nicht jdk.internal.misc.
Das JDK ist ebenfalls modular
Seit Java 9 ist das JDK in ~70 Module aufgeteilt (java.base, java.sql, java.xml, java.net.http, …). java.base ist besonders: Es wird implizit von jedem Modul benötigt und enthält die Sprachgrundlagen (java.lang, java.util, java.io). Jede Klasse, die man je verwendet hat, lebt in einem dieser Module — was das folgende Beispiel sichtbar macht.
Ein praktisches Beispiel: Module zur Laufzeit untersuchen
Man muss kein Modul schreiben, um Module zu sehen: Die Laufzeit-Module-API gibt das Modul jeder Klasse zurück. Dieses Programm fragt mehrere Klassen, zu welchem Modul sie gehören, prüft sein eigenes Modul und wirft einen Blick auf den Boot-Layer, mit dem die JVM gestartet wurde.
Was aus dem Programmaufruf zu entnehmen ist:
String,ArrayListundHttpClientmeldetenjava.base,java.baseundjava.net.http. Jede Klasse gehört zu genau einem Modul, undgetModule()verrät, zu welchem — die Sprachtypen leben alle injava.base, währendHttpClientin seinem eigenen Modul ist, das man mitrequires java.net.httpeinbinden müsste.- Die eigene Klasse des Programms meldete
isNamed() == falseund einengetName()-Rückgabewert vonnull. Code, der vom Classpath ausgeführt wird, landet im unbenannten Modul, einem Kompatibilitäts-Bucket, der nichts explizit benötigt und jedes andere Modul liest. Deshalb kompilieren und laufen Classpath-Programme unter Java 9+ noch immer unverändert. ModuleLayer.boot()lieferte den Graphen der Module, den die JVM beim Start aufgelöst hatte — das Zählen derjava.*-Module zeigt, dass das JDK wirklich in viele Module aufgeteilt ist, kein Monolith.java.baseist nicht offen (isOpen() == false), exportiert aber viele Pakete; es stelltjava.langundjava.utilfür alle bereit, währendjdk.internal.*versteckt bleibt. Ein Paket zu exportieren und ein Modul zu öffnen sind verschiedene Schalter — ein späteres Kapitel kehrt zuopenszurück.- Nichts davon erforderte eine
module-info.java. Die Module-API ist reflektive Metadaten, die für jedes Programm verfügbar sind; das Schreiben eines eigenen Moduls (nächstes Kapitel) ermöglicht es, diese Regeln selbst zu deklarieren, anstatt sie nur beim JDK zu beobachten.
Was der Rest dieses Teils behandelt
module-info.java— die Direktiven:requires,exports,opens,uses,provides.- Modultypen — benannte, automatische und unbenannte Module und wie sie zusammenwirken.
- Services — Entkopplung einer Schnittstelle von ihrer Implementierung mit
uses/providesundServiceLoader. - Migration — eine bestehende Classpath-Anwendung auf den Modulpfad migrieren, ohne einen vollständigen Neuschrieb.
Module sind optional: Eine Java-Anwendung kann dauerhaft auf dem Classpath laufen. Doch sie zu verstehen erklärt das moderne JDK, erschließt jlink-benutzerdefinierte Laufzeiten und gibt Bibliotheken echte Kapselung. Das nächste Kapitel, die module-info.java-Deklaration, schreibt den Deskriptor, der ein Modul zu einem Modul macht.