W3docs

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 NoClassDefFoundError zur 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:

KonzeptWas es gruppiertSichtbarkeitsregel
PaketKlassenpublic/protected/paketprivat innerhalb des JARs
JARPakete + Ressourcenalles public ist auf dem Classpath sichtbar
ModulPaketenur 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.

java— editable, runs on the server

Was aus dem Programmaufruf zu entnehmen ist:

  • String, ArrayList und HttpClient meldeten java.base, java.base und java.net.http. Jede Klasse gehört zu genau einem Modul, und getModule() verrät, zu welchem — die Sprachtypen leben alle in java.base, während HttpClient in seinem eigenen Modul ist, das man mit requires java.net.http einbinden müsste.
  • Die eigene Klasse des Programms meldete isNamed() == false und einen getName()-Rückgabewert von null. 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 der java.*-Module zeigt, dass das JDK wirklich in viele Module aufgeteilt ist, kein Monolith.
  • java.base ist nicht offen (isOpen() == false), exportiert aber viele Pakete; es stellt java.lang und java.util für alle bereit, während jdk.internal.* versteckt bleibt. Ein Paket zu exportieren und ein Modul zu öffnen sind verschiedene Schalter — ein späteres Kapitel kehrt zu opens zurü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/provides und ServiceLoader.
  • 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.

Übungen

Übung
Eine Bibliotheks-JAR enthält eine 'public'-Klasse im Paket 'com.acme.internal', die die Autoren nur zur internen Verwendung innerhalb der Bibliothek vorgesehen haben. Die JAR wird als modulare JAR erstellt, deren 'module-info.java' 'com.acme.api', aber nicht 'com.acme.internal' exportiert. Was passiert, wenn diese JAR auf den MODULPFAD gelegt wird und ein anderes Modul versucht, 'com.acme.internal.Helper' zu importieren?
Eine Bibliotheks-JAR enthält eine 'public'-Klasse im Paket 'com.acme.internal', die die Autoren nur zur internen Verwendung innerhalb der Bibliothek vorgesehen haben. Die JAR wird als modulare JAR erstellt, deren 'module-info.java' 'com.acme.api', aber nicht 'com.acme.internal' exportiert. Was passiert, wenn diese JAR auf den MODULPFAD gelegt wird und ein anderes Modul versucht, 'com.acme.internal.Helper' zu importieren?
Was this page helpful?