14.2 Module entwickeln und einbinden
Das JPMS (Java Platform Module System), auch unter dem Projektnamen Jigsaw bekannt, ist eine der größten Neuerungen seit Java 9. Im Mittelpunkt steht die starke Kapselung: Implementierungsdetails kann ein Modul geheim gehalten. Selbst Hilfscode innerhalb des Moduls, auch wenn er öffentlich ist, darf nicht nach außen dringen. Zweitens kommt eine Abstraktion von Verhalten über Schnittstellen hinzu, die interne Klassen aus dem Modul implementieren können, wobei dem Nutzer die konkreten Klassen nicht bekannt sind. Als dritten Punkt machen explizite Abhängigkeiten die Interaktion mit anderen Modulen klar. Eine grafische Darstellung hilft auch bei großen Architekturen, die Übersicht über Nutzungsbeziehungen zu behalten.
14.2.1 Wer sieht wen?
Klassen, Pakete und Module lassen sich als Container mit unterschiedlichen Sichtbarkeiten betrachten:
Ein Typ, sei es Klasse oder Schnittstelle, enthält Attribute und Methoden.
Ein Paket enthält Typen.
Ein Modul enthält Pakete.
Private Eigenschaften in einem Typ sind nicht in anderen Typen sichtbar.
Nicht öffentliche Typen sind in anderen Paketen nicht sichtbar.
Nicht exportierte Pakete sind außerhalb eines Moduls nicht sichtbar.
Ein Modul ist definiert
durch einen Namen,
durch die Angabe, was es exportiert, und
durch die Angabe, welches Modul es zur Arbeit selbst benötigt.
Interessant ist der zweite Aspekt, also dass ein Modul etwas exportiert. Wenn nichts exportiert wird, ist auch nichts nach außen sichtbar. Alles, was Außenstehende sehen sollen, muss in der Modulbeschreibung aufgeführt sein, denn nicht alle öffentlichen Typen des Moduls sind standardmäßig öffentlich, ansonsten wäre das kein Fortschritt zu JAR-Dateien. Mit dem neuen Modulsystem haben wir also eine ganz andere Sichtbarkeit. Aus der Viererbande public, private, paketsichtbar und protected bekommt public eine viel feinere Abstufung. Denn was public ist, bestimmt das Modul, und das sind:
Typen, die das Modul für alle exportiert
Typen für explizit aufgezählte Module
alle Typen im gleichen Modul
Der Compiler und die JVM achten auf die Einhaltung der Sichtbarkeit, und auch Tricks mit Reflection sind nicht mehr möglich, wenn ein Modul keine Freigabe erteilt hat.
Modultypen
Wir wollen uns in dem Abschnitt intensiver mit drei Modultypen beschäftigen. Wenn wir neue Module schreiben, dann sind das benannte Module. Daneben gibt es aus Kompatibilitätsgründen automatische Module und unbenannte Module, mit denen wir vorhandene JAR-Dateien einbringen können. Die Bibliothek der Java SE ist selbst in Module unterteilt, wir nennen sie Plattform-Module. Die Laufzeitumgebung zeigt mit einem Schalter --list-modules alle Plattform-Module an. Der Einsatz des Schalters vor Java 9 führt zu einem Fehler.
[zB] Beispiel
Liste die ca. 70 Module auf:
$ java --list-modules
java.base@14
java.compiler@14
java.datatransfer@14
java.desktop@14
…
jdk.unsupported@14
jdk.unsupported.desktop@14
jdk.xml.dom@14
jdk.zipfs@14
Im Ordner D:\Program Files\Java\jdk-14\bin>java --list-modules liegen JMOD-Dateien.
14.2.2 Plattform-Module und ein JMOD-Beispiel
Das Kommandozeilenwerkzeug jmod zeigt an, was ein Modul exportiert und benötigt. Nehmen wir die JDBC-API für Datenbankverbindungen als Beispiel: Die Typen liegen in einem eigenen Modul mit dem Namen java.sql:
C:\Program Files\Java\jdk-14\bin>jmod describe ..\jmods\java.sql.jmod
java.sql@14
exports java.sql
exports javax.sql
requires java.base mandated
requires java.logging transitive
requires java.transaction.xa transitive
requires java.xml transitive
uses java.sql.Driver
platform windows-amd64
Wir können daraus Folgendes ablesen:
den Namen
die Pakete, die das Modul exportiert: java.sql und javax.sql
die Module, die java.sql benötigt: java.base ist hier immer drin; hinzu kommen weitere.
Die Meldung mit uses steht im Zusammenhang mit dem Service-Locator – wir können das vorerst ignorieren.
Die Information über die Plattform (windows-amd64) schreibt jmod mit hinein, es ist die Belegung der System-Property os.arch auf dem Build-Server.
14.2.3 Interne Plattformeigenschaften nutzen, --add-exports
Als Sun im letzten Jahrhundert mit der Entwicklung der Java-Bibliotheken begann, kamen eine Reihe interner Hilfsklassen mit in die Bibliothek. Viele beginnen mit den Paketpräfixen com.sun und sun. Die Typen wurden immer als interne Typen kommuniziert, doch bei einigen Entwicklern waren die Neugierde und das Interesse so groß, dass die Warnungen von Sun/Oracle ignoriert wurden. In Java 9 gab es den großen Knall, da public seitdem nicht mehr automatisch public für alle Klassen außerhalb des Moduls ist: Die internen Klassen werden nicht mehr exportiert, sind also nicht mehr benutzbar.
Akt 1: Der Quellcode
Es kommt zu einem Compilerfehler, wie in folgendem Beispiel:
package com.tutego.insel.tool;
public class ShowRuntimeArguments {
public static void main( String[] args ) throws Exception {
System.out.println( java.util.Arrays.toString(
jdk.internal.misc.VM.getRuntimeArguments() ) );
}
}
Unser Programm greift auf die VM-Klasse zurück, um die eigentliche Belegung der Kommandozeile zu erfragen. Was wir in der main(String[] args)-Methode über args empfangen, enthält keine JVM-Argumente.
Akt 2: Der Compilerfehler
Ab Java 9 lässt sich das Programm nicht mehr ohne Compilerfehler übersetzen:
$ javac com/tutego/insel/tool/ShowRuntimeArguments.java
com\tutego\insel\tool\ShowRuntimeArguments.java:6: error: package jdk.internal.misc is not visible
jdk.internal.misc.VM.getRuntimeArguments() ) );
^
(package jdk.internal.misc is declared in module java.base, which does not export
it to the unnamed module)
1 error
Das Problem dokumentiert der Compiler: jdk.internal.misc ist nicht für unser Programm zugänglich.
Akt 3: Der magische Compiler-Schalter
Zwar ist die Klasse VM selbst public und die Methode getRuntimeArguments() ebenfalls public, aber jdk.internal.misc wurde nicht exportiert, also ist der Zugriff von unserem Programm nicht möglich, denn die JVM realisiert eine Zugriffskontrolle. Allerdings können wir diese abschalten. Mit dem Schalter --add-exports stellen wir aus dem Modul java.base das Paket jdk.internal.misc unserer Klasse bereit. Die allgemeine Syntax ist so:
--add-exports <source-module>/<package>=<target-module>(,<target-module>)*
Die Angabe ist für den Compiler und für die Laufzeitumgebung zu setzen. Für unser Beispiel bedeutet das:
$ javac --add-exports java.base/jdk.internal.misc=ALL-UNNAMED com/tutego/insel/tool/ShowRuntimeArguments.java
Akt 4: Die zickige JVM
Das Programm ist compiliert, führen wir es aus:
$ java com/tutego/insel/tool/ShowRuntimeArguments
Exception in thread "main" java.lang.IllegalAccessError: class com.tutego.insel.tool.ShowRuntimeArguments (in unnamed module @0x4d591d15) cannot access class jdk.internal.misc.VM (in module java.base) because module java.base does not export jdk.internal.misc to unnamed module @0x4d591d15
at com.tutego.insel.tool.ShowRuntimeArguments.main(ShowRuntimeArguments.java:6)
Es funktioniert nicht! Aber die Fehlermeldung kommt uns bekannt vor, und wir wissen, warum …
Akt 5: Die JVM will das, was der Compiler will. Schluss
Wir müssen den gleichen Schalter wie für den Compiler setzen:
$ java --add-exports java.base/jdk.internal.misc=ALL-UNNAMED ShowRuntimeArguments
[--add-exports=java.base/jdk.internal.misc=ALL-UNNAMED]
Wir sehen die Ausgabe – das Programm funktioniert.
Eine Angabe wie java.base/jdk.internal.misc, bei der vorne das Modul steht und hinter dem / der Paketname, ist oft ab Java 9 anzutreffen. Hinter dem Gleichheitszeichen steht entweder unser Paket, das die Typen in jdk.internal.misc sehen kann, oder – wie in unserem Fall – ALL-UNNAMED.
jdeps
Hätten wir das Programm schon erfolgreich ab Java 9 übersetzt, würde es zur Laufzeit ebenfalls knallen. Da es nun sehr viel Programmcode gibt, haben die Java-Entwickler bei Oracle das Kommandozeilenprogramm jdeps entwickelt. Es meldet, wenn interne Typen im Programm vorkommen:
$ jdeps com/tutego/insel/tool/ShowRuntimeArguments.class
ShowRuntimeArguments.class -> java.base
com.tutego.insel.tool -> java.io java.base
com.tutego.insel.tool -> java.lang java.base
com.tutego.insel.tool -> java.util java.base
com.tutego.insel.tool -> jdk.internal.misc JDK internal API (java.base)
Anders als beim Java-Compiler ist der volle Dateiname, also mit .class, nötig. Die Meldung »JDK internal API« bereitet uns darauf vor, dass es gleich Ärger geben wird.
So kann relativ leicht eine große Codebasis untersucht werden, und Entwickler können proaktiv den Stellen auf den Grund gehen, die problematische Abhängigkeiten haben.
14.2.4 Neue Module einbinden, --add-modules und --add-opens
Jedes Java SE-Projekt basiert auf dem Modul java.se, was diverse Modulabhängigkeiten nach sich zieht (siehe Abbildung 14.1).
[»] Hinweis
Nicht alle installierten Module sind im java.se-Modul enthalten. Dazu zählen die JDK-Module und java.smartcardio. Sie befinden sich jedoch standardmäßig im Modulpfad, wie folgender »Einzeiler« zeigt:
ModuleLayer.boot().modules().stream()
.map( Module::getName )
.sorted()
.reduce( ( s1, s2 ) -> s1 + ", " + s2 )
.ifPresent( System.out::println );
Er listet folgende Module auf:
java.base, java.compiler, java.datatransfer, java.desktop, java.instrument, java.logging, java.management, java.management.rmi, java.naming, java.net.http, java.prefs, java.rmi, java.scripting, java.security.jgss, java.security.sasl, java.smartcardio, java.sql, java.sql.rowset, java.transaction.xa, java.xml, java.xml.crypto, jdk.accessibility, jdk.attach, jdk.charsets, jdk.compiler, jdk.crypto.cryptoki, jdk.crypto.ec, jdk.crypto.mscapi, jdk.dynalink, jdk.editpad, jdk.httpserver, jdk.internal.ed, jdk.internal.jvmstat, jdk.internal.le, jdk.internal.opt, jdk.jartool, jdk.javadoc, jdk.jconsole, jdk.jdeps, jdk.jdi, jdk.jdwp.agent, jdk.jfr, jdk.jlink, jdk.jshell, jdk.jsobject, jdk.jstatd, jdk.localedata, jdk.management, jdk.management.agent, jdk.management.jfr, jdk.naming.dns, jdk.naming.rmi, jdk.net, jdk.scripting.nashorn, jdk.sctp, jdk.security.auth, jdk.security.jgss, jdk.unsupported, jdk.unsupported.desktop, jdk.xml.dom, jdk.zipfs
Alle diese Module können ohne Schalter direkt verwendet werden.
Neue Module zu den Kernmodulen hinzufügen und öffnen
Sollen externe Module zu den Kernmodulen hinzugenommen werden, so geschieht das mit dem Schalter --add-modules. Ein weiterer Schalter ist --add-opens, der ein Paket für Reflection öffnet. Neben --add-opens gibt es das ähnliche --add-exports, das alle öffentlichen Typen und Eigenschaften zur Übersetzungs- bzw. Laufzeit öffnet; --add-opens geht für Reflection einen Schritt weiter.
14.2.5 Projektabhängigkeiten in Eclipse
Um Module praktisch umzusetzen, wollen wir in Eclipse zwei neue Java-Projekte aufbauen: com.tutego.greeter und com.tutego.main. Wir legen im Projekt com.tutego.greeter eine Klasse com.tutego.insel.greeter.Greeter an und in com.tutego.main die Klasse com.tutego.insel.main.Main. Im Package-Explorer sieht das so aus wie in Abbildung 14.2:
Jetzt ist eine wichtige Vorbereitung in Eclipse nötig: Wir müssen einstellen, dass com.tutego.main das Java-Projekt com.tutego.greeter benötigt. Dazu gehen wir in das Projekt com.tutego.main und rufen im Kontextmenü Project auf, alternativ im Menüpunkt Project • Properties oder über die Tastenkombination (Alt)+(¢). Im Dialog navigieren wir links auf Java Build Path und aktivieren den Reiter Projects. Wir wählen nun Add…, und im Dialog wählen wir aus der Liste com.tutego.greeter. Ok schließt den kleinen Dialog, und unter Required projects on the build path taucht eine Abhängigkeit auf (siehe Abbildung 14.3).
Wir können jetzt zwei einfache Klassen implementieren. Wir beginnen mit der Klasse für das Projekt com.tutego.greeter:
package com.tutego.insel.greeter;
public class Greeter {
private Greeter() { }
public static Greeter instance() {
return new Greeter();
}
public void greet( String name ) {
System.out.println( "Hey "+ name );
}
}
Dann folgt die Hauptklasse im Projekt com.tutego.main:
package com.tutego.insel.main;
import com.tutego.insel.greeter.Greeter;
public class Main {
public static void main( String[] args ) {
Greeter.instance().greet( "Chris" );
}
}
Da wir in Eclipse vorher die Abhängigkeit gesetzt haben, gibt es keinen Compilerfehler.
14.2.6 Benannte Module und module-info.java
Die Modulinformationen werden über eine Datei module-info.java (kurz Modulinfodatei) deklariert, Annotationen kommen nicht zum Einsatz. Diese zentrale Datei ist der Hauptunterschied zwischen einem Modul und einer einfachen JAR-Datei. In dem Moment, in dem die spezielle Klassendatei module-info.class im Modulpfad vorhanden ist, beginnt die Laufzeitumgebung, das Projekt als Modul zu interpretieren.
Testen wir das, indem wir in unseren Projekten com.tutego.greeter und com.tutego.main eine Modulinfodatei anlegen. Das kann Eclipse über das Kontextmenü Configure • Create module-info.java für uns machen (siehe Abbildung 14.4).
Für das erste Modul com.tutego.greeter entsteht:
/**
*
*/
/**
* @author Christian
*
*/
module com.tutego.greeter {
exports com.tutego.insel.greeter;
requires java.base;
}
Die zweite Modulinfodatei sieht so aus (die Kommentare sind ausgeblendet):
module com.tutego.main {
exports com.tutego.insel.main;
requires com.tutego.greeter;
requires java.base;
}
Hinter dem Schlüsselwort module steht der Name des Moduls, den Eclipse automatisch so wählt, wie das Eclipse-Projekt heißt.[ 233 ](Zur Benennung von Modulen gibt es Empfehlungen in dem englischsprachigen Beitrag http://mail.openjdk.java.net/pipermail/jpms-spec-experts/2017-May/000687.html. ) Darauf folgt ein Block in geschweiften Klammern.
Zwei Schlüsselwörter fallen ins Auge, die wir schon vorher bemerkt haben: exports und requires.
Das Projekt/Modul com.tutego.greeter exportiert ausschließlich das Paket com.tutego.insel.greeter, andere Pakete nicht. Es benötigt (requires) java.base, wobei das Modul Standard ist und die Zeile gelöscht werden kann.
Das Projekt/Modul com.tutego.main exportiert das Paket com.tutego.insel.main, und es benötigt com.tutego.greeter – diese Information nimmt sich Eclipse selbstständig aus den Projektabhängigkeiten.
[»] Info
Ein Modul requires ein anderes Modul, aber exports ein Paket.
Beginnen wir mit den Experimenten in den beiden module-info.java-Dateien:
Modul | Aktion | Ergebnis |
---|---|---|
com.tutego.greeter | // requires java.base; | Auskommentieren führt zu keiner Änderung, da java.base immer benötigt (required) wird. |
com.tutego.greeter | // exports com.tutego. insel.greeter; | Compilerfehler im main-Modul: »The type com.tutego. insel.greeter.Greeter is not accessible« |
exports com.tutego. insel. greeter to god; | Nur das Modul god bekommt Zugriff auf com.tutego. insel.greeter. Das main-Modul meldet »The type com.tutego.insel.greeter. Greeter is not accessible«. | |
com.tutego.greeter | exports com.tutego. insel.closer; | Das Hinzufügen führt zum Compilerfehler »The package com.tutego.insel.closer does not exist or is empty«. |
com.tutego.main | // requires com.tutego. greeter; | Compilerfehler »The import com.tutego.insel.greeter cannot be resolved« |
com.tutego.main | // exports com.tutego. insel.main; | Kein Ergebnis, denn c.t.i.m wird von keinem Modul benötigt (required). |
Die Zeile mit exports com.tutego.insel.greeter to god zeigt einen qualifizierten Export.
Übersetzen und Packen von der Kommandozeile
Setzen wir ins Wurzelverzeichnis des Moduls com.tutego.greeter ein Batch-Skript compile.bat; wir nehmen an, dass das JDK-bin-Verzeichnis im PATH ist.
rmdir /s /q lib
mkdir lib
javac -d bin src\module-info.java src\com\tutego\insel\greeter\Greeter.java
jar --create --file=lib/com.tutego.greeter@1.0.jar --module-version=1.0 -C bin .
jar --describe-module --file=lib/com.tutego.greeter@1.0.jar
Das Skript führt folgende Schritte aus:
Löschen eines vielleicht schon angelegten lib-Ordners
Anlegen eines neuen lib-Ordners für die JAR-Datei
Übersetzen der zwei Java-Dateien in den Zielordner bin
Anlegen einer JAR-Datei. --create (abkürzbar zu –c) instruiert das Werkzeug, eine neue JAR-Datei anzulegen. –file (oder kurz –f) bestimmt den Zielnamen, –module-version unsere Versionsnummer, und –C wechselt das Verzeichnis und beginnt ab dort, die Dateien einzusammeln. Die Kommandozeilensyntax beschreibt Oracle auf der Webseite http://docs.oracle.com/en/java/javase/14/docs/specs/man/jar.html.
Die Option --describe-module (oder kurz –d) zeigt die Modulinformation und führt zu folgender (vereinfachten) Ausgabe: com.tutego.greeter@1.0 jar:file:///C:/…/com.tutego.greeter/lib/com.tutego.greeter@1.0.jar/!module-info.class exports com.tutego.insel.greeter requires java.base.
Für das zweite Projekt ist die compile.bat sehr ähnlich, dazu kommt ein Aufruf der JVM, um das Programm zu starten.
rmdir /s /q lib
mkdir lib
javac -d bin --module-path ..\com.tutego.greeter\lib src\module-info.java
src\com\tutego\insel\main\Main.java
jar -c -f=lib/com.tutego.main@1.0.jar --main-class=com.tutego.insel.main.Main
--module-version=1.0 -C bin .
java -p lib;..\com.tutego.greeter\lib -m com.tutego.main
Änderungen gegenüber dem ersten Skript sind:
Beim Compilieren müssen wir den Modulpfad mit --module-path (oder kürzer mit -p) angeben, weil ja das Modul com.tutego.greeter required ist.
Beim Anlegen der JAR-Datei geben wir über –main-class die Klasse mit der main(…)-Methode an.
Startet die JVM das Programm, lädt sie das Hauptmodul und alle abhängigen Module. Wir geben beide lib-Ordner mit den JAR-Dateien an und mit –m das sogenannte initiale Modul für die Hauptklasse.
14.2.7 Automatische Module
JAR-Dateien spielen seit 20 Jahren eine zentrale Rolle im Java-System; sie vom einen zum anderen Tag abzuschaffen, würde große Probleme bereiten. Ein Blick auf http://mvnrepository.com/repos offenbart über 7,7 Millionen Artefakte. Es gehen zwar auch Dokumentationen und andere Dateien in die Statistik ein, doch diese Zahl verdeutlicht, wie viele JAR-Dateien im Umlauf sind.
Damit JAR-Dateien ab Java 9 eingebracht werden können, gibt es zwei Lösungen: Wir können das JAR entweder in den Klassenpfad oder in den Modulpfad setzen. Kommt ein JAR in den Modulpfad und hat es keine Modulinfodatei, entsteht ein automatisches Modul. Bis auf eine kleine Einschränkung funktioniert das für die meisten existierenden Java-Bibliotheken.
Ein automatisches Modul hat gewisse Eigenschaften für den Modulnamen und Konsequenzen in den Abhängigkeiten:
Ohne Modulinfo haben die automatischen Module keinen selbst gewählten Namen, sondern sie bekommen vom System einen Namen zugewiesen, der sich aus dem Dateinamen ergibt.[ 234 ](Automatic-Module-Name in die META-INF-Datei zu setzen ist eine Alternative, dazu später mehr. ) Vereinfacht gesagt: Angehängte Versionsnummern und die Dateiendung werden entfernt und alle nichtalphanumerischen Zeichen werden durch Punkte ersetzt, jedoch nicht zwei Punkte hintereinander.[ 235 ](http://docs.oracle.com/en/java/javase/14/docs/api/java.base/java/lang/module/ModuleFinder.html#of(java.nio.file.Path...). ) Die Version wird erkannt. Die Dokumentation gibt das Beispiel foo-bar-1.2.3-SNAPSHOT.jar an, was zum Modulnamen foo.bar und der Version 1.2.3-SNAPSHOT führt.
Automatische Module exportieren immer alle ihre Pakete. Wenn es also eine Abhängigkeit zu diesem automatischen Modul gibt, kann der Bezieher alle sichtbare Typen und Eigenschaften verwenden.
Automatische Module können alle anderen Module lesen, auch die unbenannten.
Auf den ersten Blick scheint eine Migration für das Modulsystem einfach: Alle JARs kommen in den Modulpfad, egal ob es Modulinfodateien gibt oder nicht. Allerdings gibt es JAR-Dateien, die von der JVM als automatisches Modul abgelehnt werden – nämlich wenn sie Typen eines Pakets enthalten und dieses Paket sich schon in einem anderen aufgenommenen Modul befindet. Module dürfen keine split packages enthalten, also das gleiche Paket noch einmal enthalten. Die Migration erfordert dann
das Zusammenlegen der Pakete zu einem Modul,
die Verschiebung in unterschiedliche Pakete oder
die Nutzung des Klassenpfades.
14.2.8 Unbenanntes Modul
Eine Migration auf eine neue Java-Version sieht in der Regel so aus, dass zuerst die JVM gewechselt und dann geprüft wird, ob die vorhandene Software weiterhin funktioniert. Laufen die Testfälle durch und es gibt keine Auffälligkeiten im Testbetrieb, kann der Produktivbetrieb unter der neuen Version erfolgen. Gibt es keine Probleme, können nach einiger Zeit die neuen Sprachmittel und Bibliotheken verwendet werden.
Eine vorhandene Java-8-Software muss inklusive aller Einstellungen und Einträge im Klassenpfad weiterhin laufen. Das heißt, eine Laufzeitumgebung ab Java 9 kann den Klassenpfad nicht ignorieren. Da es intern nur einen Modulpfad gibt, müssen auch diese JAR-Dateien zu Modulen werden. Die Lösung ist das unbenannte Modul (engl. unnamed module): Jedes JAR im Klassenpfad – dabei spielt es keine Rolle, ob es eine modul-info.class enthält – kommt in das unbenannte Modul. Davon gibt es nur eines, wir sprechen also im Singular, nicht Plural.
»Unbenannt« sagt schon, dass das Modul keinen Namen hat und folglich auch keine Abhängigkeit zu den JAR-Dateien im unbenannten Modul existieren kann. Das ist der Unterschied zu einem automatischen Modul. Ein unbenanntes Modul hat die gleiche Eigenschaft wie ein automatisches Modul, dass es alle Pakete exportiert. Und weil es zur Migration gehört, hat ein unbenanntes Modul auch Zugriff auf alle anderen Module.
14.2.9 Lesbarkeit und Zugreifbarkeit
Die Laufzeitumgebung sortiert Module in einen Graphen ein. Die Abhängigkeit der Module führt dabei zur sogenannten Lesbarkeit (engl. readability): Benötigt Modul A Modul B, so liest A Modul B, und B wird von A gelesen. Für die Funktionsweise des Modulsystems ist dies elementar, denn so werden zur Übersetzungszeit schon Fehler ausgeschlossen, wie Zyklen oder gleiche Pakete in unterschiedlichen Modulen. Die Lesbarkeit ist zentral für eine zuverlässige Konfiguration (engl. reliable configuration).
Einen Schritt weiter geht der Begriff der Erreichbarkeit bzw. Zugänglichkeit (engl. accessibility). Wenn ein Modul ein anderes Modul grundsätzlich lesen kann, bedeutet das noch nicht, dass es an alle Pakete und Typen kommt, denn nur diejenigen Typen sind sichtbar, die exportiert worden sind. Lesbare und erreichbare Typen nennen sich erreichbar.
Die nächste Frage ist, welcher Modultyp auf welchen anderen Modultyp Zugriff hat. Tabelle 14.2 fasst die Lesbarkeit zusammen.
Modultyp | Ursprung | Exportiert Pakete | Hat Zugriff auf |
---|---|---|---|
Plattformmodul | JDK | Explizit | |
Benannte Module | Container mit Modulinfo im Modulpfad | Explizit | Plattformmodule, andere benannte Module, automatische Module |
Automatische Module | Container ohne Modulinfo im Modulpfad | Alle | Plattformmodule, andere benannte Module, automatische Module, unbenanntes Modul |
Unbenanntes Modul | Klassendateien und JARs im Klassenpfad | Alle | Plattformmodule, benannte Module, automatische Module |
Der Modulinfodatei kommt dabei die größte Bedeutung zu, denn sie macht aus einem JAR ein Modular JAR. Fehlt die Modulinformation, bleibt es ein normales JAR, wie Java-Entwickler es seit 20 Jahren kennen. Die JAR-Datei kann neu in den Modulpfad kommen oder in den bekannten Klassenpfad. Das ergibt vier Kombinationen:
Modulpfad | Klassenpfad | |
---|---|---|
JAR mit Modulinformation | Wird benanntes Modul. | Wird unbenanntes Modul. |
JAR ohne Modulinformation | Wird automatisches Modul. | Wird unbenanntes Modul. |
JAR-Archive im Klassenpfad sind das bekannte Verhalten, weswegen auch ein Wechsel von Java 8 auf Java 11 möglich sein sollte.
14.2.10 Modul-Migration
Nehmen wir an, unsere monolithische Applikation hat keine Abhängigkeiten zu externen Bibliotheken und soll modularisiert werden. Dann besteht der erste Schritt darin, die gesamte Applikation in ein großes benanntes Modul zu setzen. Als Nächstes müssen die einzelnen Bereiche identifiziert werden, damit nach und nach die Bausteine in einzelne Module wandern. Das ist nicht immer einfach, zumal zyklische Abhängigkeiten nicht unwahrscheinlich sind. Bei der Modularisierung des JDK hatten die Oracle-Entwickler viel Mühe.
Das Problem mit automatischen Modulen
Traditionell generieren Build-Werkzeuge wie Maven oder Gradle JAR-Dateien, und ein Dateiname hat sich irgendwie ergeben. Werden jedoch diese JAR-Dateien zu automatischen Modulen, spielt der Dateiname plötzlich eine große Rolle. Doch bewusst wurde der Dateiname vermutlich nie gewählt. Referenziert ein benanntes Modul ein automatisches Modul, bringt das zwei Probleme mit sich: Ändert sich der Dateiname – lassen wir die Versionsnummer einmal außen vor –, heißt auch das automatische Modul anders, und die Abhängigkeit kann nicht mehr aufgelöst werden. Das zweite Problem ist größer: Viele Java-Bibliotheken haben noch keine Modulinformationen, und folglich werden Entwickler eine Abhängigkeit zu diesem automatischen Modul über den abgeleiteten Namen ausdrücken.
Nehmen wir zum Beispiel die beliebte Open-Source-Bibliothek Google Guava. Die JAR-Datei hat den Dateinamen guava-27.0-jre.jar – das automatische Modul heißt folglich guava. Ein benanntes Modul (nennen wir es M1) kann über required guava eine Abhängigkeit ausdrücken. Konvertiert Google die Bibliothek in ein echtes Java 9-Modul, dann wird sich der Name ändern – geplant ist com.google.guava. Und ändert sich der Name, führen alle Referenzierungen in Projekten zu einem Compilerfehler. Ein Alias wäre eine tolle Idee, das gibt es jedoch nicht. Und das Problem besteht ja nicht nur im eigenen Code, der Guava referenziert: Referenziert das eigene Modul M1 ein Modul M2, das wiederum Guava referenziert, so gibt es das gleiche Problem – wir sprechen von einer transitiven Abhängigkeit. Die Änderung des Modulnamens von Guava wird zum Problem, denn wir müssen warten, bis M2 den Namen korrigiert, damit M1 wieder gültig ist.
Eine Lösung mildert das Problem ab: In der JAR-Manifest-Datei kann ein Eintrag Automatic-Module-Name gesetzt werden – das »überschreibt« den automatischen Modulnamen.
[zB] Beispiel
Apache Commons setzt den Namen so:
Automatic-Module-Name: org.apache.commons.lang3
Benannte Module, die Abhängigkeiten zu automatischen Modulen besitzen, sind also ein Problem. Es ist zu hoffen, dass die zentralen Java-Bibliotheken, auf die sich so viele Lösungen stützen, schnell Modulinformationen einführen. Das wäre eine Lösung von unten nach oben, englisch bottom-up. Das ist das Einzige, was erfolgversprechend ist, aber wohl auch eine lange Zeit benötigen wird. Auch jetzt, einige Zeit nach dem Release von Java 9, haben nur wenige Java-Bibliotheken eine Modulinformation; Automatic-Module-Name kommt häufiger vor.