Wer hat Angst vor Java 9?

#JAVAPRO #JCON2017 #Java9 #ProjectJigsaw

Das neue Modulsystem Jigsaw von Java 9 sorgt für große Verunsicherung in der Java-Community, da wichtige Vertreter und Tool-Hersteller schwere Kritik an der Umsetzung geübt und dem zugehörigen Java-Specification-Request (JSR) zunächst sogar die Zustimmung verweigert haben. Lohnt es sich schon Java 9 zu verwenden? Wer ist am stärksten von den Änderungen betroffen? Und was muss man tun, um seine Anwendung sicher nach Java 9 zu migrieren?

Das Java-Platform-Module-System

Der Versuch ein Modulsystem für Java zu schaffen begann bereits im Jahr 2005. In Java 9 haben wir nach mehreren gescheiterten Versuchen (JSR 277, JSR 294) nach zwölf Jahren nun endlich ein Ergebnis dieses langen Prozesses. Im Verlauf der Diskussionen haben sich die Erwartungen und Anforderungen
an ein allgemeines Modulsystem für Java stark verschoben. Im JSR 277 waren noch ein Versionierungsschema, ein Distributionsformat für Module und ein an Maven angelehntes Repository für Module vorgesehen. Mit dem JSR 294 wurde dies zunächst noch mit dem Konzept der Superpackages ergänzt, später war der Plan leichtgewichtigere Modules zu integrieren. Beide JSRs wurden zurückgezogen und zugunsten des JSR 376 aufgegeben.
Sehen wir uns zunächst an, was das tatsächliche Modulsystem alles beinhaltet. Die wichtigsten Änderungen stecken in diesen 6 Projekten, die zum JSR 376 gehören:

  • JEP 200 – Modularisierung des JDK
  • JEP 201 – Modularisierung des Quellcodes
  • JEP 220 – Modulare Runtime-Images
  • JEP 260 – Verkapselung interner APIs
  • JEP 261 – Das Modulsystem
  • JEP 282 – Das jlink-Tool

Es wird also nicht nur ein Modulsystem für die Anwendungen von Java-Entwicklern geschaffen, sondern die Core-Java-APIs selbst wurden in Module unterteilt. Das hat viele Vorteile. Die Weiterentwicklung des JDK wird einfacher, da durch die Modularisierung des Quellcodes die einzelnen Module leichter unabhängig voneinander weiterentwickelt werden können. Der größte Vorteil ist jedoch, dass zur Laufzeit nicht mehr alle Module benötigt werden. So ist es möglich die passende Distribution für jede Anwendung zu bauen. Vor allem die Entwickler von Client-Anwendungen wird das freuen, da Sie Ihren Kunden kleinere Downloads anbieten können. (Abb. 1) zeigt die Unterteilung der Core-APIs in Module.
Um das alles umzusetzen, waren Änderungen am Compiler, der Java Virtual Machine (JVM), dem Layout des Java Development Kit (JDK) und der Java Runtime Environment (JRE) sowie an vielen bestehenden APIs nötig. Zudem mussten neue Tools und Speicherformate geschaffen werden.

Das modulare JDK9. (Abb. 1)

Das modulare JDK9. (Abb. 1)

Warum das Alles?

In Java 9 hat sich also vieles ganz grundlegend geändert. Solch ein massiver Umbau erfordert eine Menge Arbeit und birgt erhebliche Risiken für Entwickler und Endanwender. Gleichzeitig wurde auf die Entwicklung aufregender neuer Features verzichtet. Weshalb ist es so wichtig Java zu modularisieren? In „The State of the Module System“, einem informellen Überblick über das JSR 376, gibt Marc Reinhold, Chefarchitekt der Java Plattform „Reliable configuration“ und „Strong encapsulation“ als Hauptziele an. Also verlässliche Konfiguration und starke Verkapselung.
Mit verlässlicher Konfiguration soll der fehleranfällige Classpath-Mechanismus, die berühmt-berüchtigte „Classpath-Hell“ abgelöst werden. Und die starke Verkapselung soll Entwicklern endlich eine Möglichkeit bieten, den Zugriff auf Java-Types auch auf Package-Ebene zu beschränken.

Classpath-Hell

Bislang werden Anwendungsklassen standardmäßig auf Anforderung der Runtime vom Classpath geladen. Dazu wird die Anwendung gestartet und sobald wir eine neue Klasse verwenden, wird der Classpath danach durchsucht. Die erste Klasse die gefunden wird, wird geladen und befindet sich danach in einem gemeinsamen Pool mit den anderen Klassen. In dieser Ursuppe ist jede mühsam aufgebaute Aufteilung in einzelne JAR-Dateien wieder verschwunden. Die Anordnung von JAR-Dateien auf dem Classpath bestimmt welche Klasse geladen wird. Gibt es dieselbe Klasse mehr als einmal, kommt es zur Verschattung.
Bei doppelten Klassen wird diejenige aus dem JAR geladen, die auf dem Classpath weiter vorne liegt. Im schlimmsten Fall bekommen wir sogar eine Mischung von Klassen aus verschiedenen Versionen, wenn etwa Version 1 zuerst auf dem Classpath liegt, Version 2 aber zusätzliche Klassen enthält, die in Version 1 nicht enthalten sind. Dann wird von allen doppelten Klassen Version 1 geladen, für die zusätzlichen Klassen Version 2. (Abb. 2) zeigt ein Beispiel für dieses Problem. Das impl.jar hat eine Abhängigkeit auf lib-v1.jar, während das app.jar lib-v2.jar referenziert.

Classpath Hell - Im schlimmsten Fall kommt es zu einer Mischung von Klassen aus verschiedenen Versionen. (Abb. 2)

Classpath Hell – Im schlimmsten Fall kommt es zu einer Mischung von Klassen aus verschiedenen Versionen. (Abb. 2)

Wenn lib-v2.jar zuerst auf dem Classpath liegt, werden beide Klassen aus lib-v2.jar geladen. Kommt aber lib-v1.jar zuerst auf den Classpath, wird die Klasse Filter in Version 1 geladen, die Klasse Sorter in Version 2.

Natürlich versucht man als Entwickler zu vermeiden, zwei unterschiedliche Versionen eines JAR auf dem Klassenpfad zu haben. Bisweilen sind diese Fälle jedoch gut versteckt. Manche Bibliotheken packen zum Beispiel neben der eigentlichen Bibliothek auch gleich alle Abhängigkeiten in ein sogenanntes „Über-JAR“. Gerade Bibliotheken für generische Aufgaben wie Logging, landen so oft unbemerkt in mehreren Versionen auf dem Classpath. Zudem werden fehlende Abhängigkeiten erst dann erkannt, wenn die bereits laufende Anwendung eine Klasse vom Classloader anfordert. Das kann zum Beispiel durch eine Benutzerinteraktion ausgelöst werden. Fehlt die Klasse dann auf dem Classpath, bekommt man den gefürchteten NoClassDefFound Error. So erkennt man schlimmstenfalls erst nach Tagen oder Wochen einen Fehler im Produktionsbetrieb. Das ist ein schwerwiegendes Problem.

Reliable-Configuration

Das Modulsystem verhindert diese Probleme, indem es bereits zu Beginn die Abhängigkeiten analysiert und sicherstellt, dass jede Abhängigkeit durch exakt ein Modul zur Verfügung steht. Es sorgt ebenso dafür, dass jedes Package von höchstens einem Modul zur Verfügung gestellt wird und dass in Sonderfällen, in denen ein Package zweimal existiert, das Verhalten genau definiert ist. Das ist zum Beispiel bei der gleichzeitigen Verwendung von Modulepath und Classpath möglich.

Die Konfiguration eines Moduls erfolgt über die module-info Datei, die im Wurzelverzeichnis der Klassen eines Moduls liegt und zusammen mit dem Quellcode kompiliert wird. Mit der module-info legen wir fest, welche Abhängigkeiten ein Modul hat und welche Packages es exportiert.

(Listing 1)
module de.eppleton.modulea {
   // modulea hat eine READ-Abhängigkeit auf moduleb
   requires de.eppleton.moduleb;
}

Mit der module-info Klasse in (Listing 1) legen wir zunächst den Namen des Moduls de.eppleton.mod1 fest. Dabei wird empfohlen dem Reverse-Domain-Name-Pattern zu folgen und das Modul nach seinem Basis-Package zu benennen. Mit dem Schlüsselwort requires geben wir Abhängigkeiten an. In unserem Fall soll das Modul Klassen aus dem Modul de. eppleton.moduleb lesen dürfen. Die Java-Runtime nutzt dann die module-info der Module auf dem Modulepath und kann daraus einen Abhängigkeitsgraphen aufbauen. Fehlt eine Abhängigkeit, wird das sofort erkannt und die Anwendung startet nicht. Durch dieses Fail-fast-Verhalten werden Abhängigkeitsfehler zur Laufzeit zuverlässig verhindert und die Konfiguration wird verlässlich.

Strong-Encapsulation

Die module-info legt ebenfalls fest, welche der Packages von anderen Modulen benutzt werden dürfen. Dafür gibt es exports Statements. Sie bestimmen die öffentliche Schnittstelle. Wird ein Package nicht exportiert, ist es für andere Module nicht sichtbar. Das ist ganz einfach eine Erweiterung der von Klassen und Methoden bekannten Zugriffsbeschränkungen auf Package-Ebene. Innerhalb eines Package konnte man in Java immer schon fein granular ausdrücken, ob etwas private, package-private, protected oder public ist. In (Listing 2) exportiert unser Modul 2 Packages.

(Listing 2)
module de.eppleton.moduleb {
   // moduleb exportiert die packages:
   exports de.eppleton.moduleb.api;
   exports de.eppleton.moduleb.spi;
}

Mit den neuen exports Statements kann man auch für Packages den Zugriff beschränken. So kann man verhindern, dass Benutzer auf Implementierungsdetails zugreifen und vermeidet späteren Ärger mit API-Nutzern, wenn sich die Implementierung ändert. Der Zugriff eines Package kann auch auf bestimmte Module beschränkt werden. Man spricht dann von Qualified-Exports. Das ist hilfreich, wenn ein bestehendes Framework modularisiert wird. Die Entwickler des Frameworks halten sich so den Zugriff auf Implementierungsdetails offen, während der Nutzer des Frameworks nur die öffentliche APIs verwenden kann. In (Listing 3) exportiert user moduleb das Package de.eppleton.moduleb.impl, aber nur für modulea.

(Listing 3)
module de.eppleton.moduleb {
   // moduleb exportiert die packages:
   exports de.eppleton.moduleb.api;
   exports de.eppleton.moduleb.spi;
   // niemand ausser modulea darf diese Package lesen:
   exports de.eppleton.moduleb.impl
   to de.eppleton.modulea;
}

Bei der im JEP 260 beschriebenen Verkapselung interner APIs wurde diese Vorgehensweise verwendet. So können zum Beispiel bestimmte Java-Module nach wie vor auf interne Packages wie sun.reflect zugreifen, ohne dass der Zugriff auch für normale Anwendungsentwickler geöffnet werden muss.

JMOD Archive

Die Module der Java-Plattform stecken in JMOD-Dateien. Diese liegen in der JDK-Installation im Verzeichnis jmods. Das JMOD soll die Unzulänglichkeiten des JAR-Formats beheben. JMOD-Dateien können native Bibliotheken, Konfigurationsdateien und andere Daten enthalten. Anders als JARs werden diese Archive nicht zur Laufzeit verwendet, sondern zur Compile-Zeit und der neu eingeführten Link-Zeit. Das Linken bezeichnet den Bau eines angepassten Java-Runtime-Image mit dem jlink-Tool. Die Linkzeit wurde als eine neue optionale Phase zwischen dem Kompilieren und Ausführen der Anwendung eingeführt. In dieser
Phase sind zusätzliche Optimierungen möglich.

Das jlink-Tool

Der Bau einer angepassten Runtime ist vor allem für den Bau von Client-Anwendungen wichtig, aber auch für IoT-Anwendungen (Internet-of-Things) von Bedeutung. Gerade für Embedded-Systeme ist Speicherplatz ein limitierender Faktor. Ohne die Möglichkeit die Anwendungsgröße zu limitieren, würde Java hier schnell als Plattform uninteressant. Das jlink-Tool hat eine ganze Reihe von Parametern, um selektiv JRE-Bestandteile beim Bau der Runtime wegzulassen und zu komprimieren. Ein einfacher Aufruf sieht folgendermaßen aus:

(Listing 4)
$ jlink --module-path $JAVA_HOME/jmods:dist \
        --add-modules de.eppleton.modulea --output helloworld

Der module-path gibt an, wo die Module zu finden sind. Im Verzeichnis dist liegen in unserem Beispiel die Module unserer Anwendung. Unter jmods im Java-Home-Verzeichnis liegen die Module der Java-API. Mit dem Parameter list des Java-Executable können Sie sich nun anzeigen lassen, welche Module im Zielverzeichnis gelandet sind:

(Listing 5)
$ ./bin/java -list-modules
de.eppleton.modulea@1.0
de.eppleton.moduleb@1.0
java.base@9-ea

Die Anwendung enthält also nur unsere beiden Module sowie das java.base Modul. jlink hat mithilfe unserer module-info Klassen den Abhängigkeitsgraphen ermittelt und nur das nötigste eingepackt. Wir können den Speicherbedarf aber noch weiter reduzieren, indem wir unnötigen Ballast weglassen und Dateien komprimieren. Jlink hat dazu eine Vielzahl von Parametern. In diesem Beispiel komprimieren wir unsere Demo-Anwendung noch weiter:

(Listing 6)
$ jlink --module-path $JAVA_HOME/jmods:dist \
        --add-modules de.eppleton.modulea \
        --output helloworld --exclude-files *.diz --strip-debug \
        --compress=2 --no-header-files --no-man-pages

Die Größe einer Hello-World-Anwendung lässt sich so von über 180 MB in Java 8 auf bis zu 13-20 MB reduzieren. Je nach Betriebssystem variiert die minimale Größe. Die beste Kompression war in Experimenten des Autors unter Linux möglich, die schlechteste unter OS X.
Ein weiteres praktisches Highlight des jlink-Tools ist die Möglichkeit ein Executable zu basteln. Der folgende zusätzliche Parameter baut ein Executable mit dem Namen launcher im bin Verzeichnis der Runtime:

(Listing 7)
--launcher launcher=de.eppleton.modulea/de.eppleton.modulea.Main

Der Wert des Parameters ist der Name des Moduls und der auszuführenden Kasse, getrennt durch einen Schrägstrich. Damit lässt sich dann die Anwendung ganz einfach per Doppelklick starten.

Kritik am Modulsystem

Die Entwicklung des Modulsystems für Java 9 war von heftiger Kritik begleitet. Einigen ging der Umbau nicht weit genug, gerade die Nutzer von Modulsystemen wie OSGi vermissen viele Features, die von einem Modulsystem erwartet werden. Besonders für die Hersteller von Tools und Frameworks bedeuten Änderungen wie die Verkapselung interner APIs enorme Wartungsaufwände und es kommt zu inkompatiblen Änderungen. Zum ersten Mal in der Geschichte des JCP hat daher die zuständige Expertenkommission einem JSR die Zustimmung verweigert. Inzwischen haben die Entwickler nachgebessert und in einer zweiten Abstimmung wurde das JSR genehmigt. Dem Release von Java 9 mit dem Modulsystem steht nun nichts mehr im Wege.

Dennoch gibt es nach wie vor berechtigte Kritik am Modulsystem. Ein ganz grundlegender Mangel ist, dass auf die Versionierung von Modulen komplett verzichtet wurde. Durch die Versionierung von Modulen wäre es zum Beispiel möglich, dasselbe Modul in unterschiedlichen Versionen zu laden, ohne dabei in die Classpath-Hell zu geraten. Das Java 9 Modulsystem verhindert Versionskonflikte zur Laufzeit, indem Mehrdeutigkeiten zur Laufzeit verhindert werden. Es löst aber dadurch nicht das häufig auftretende Problem, dass eine Anwendung unterschiedliche Versionen einer Bibliothek benötigen kann. Es kann ebenfalls nicht entscheiden, ob eine verfügbare Version eines Moduls tatsächlich zum Rest der Anwendung kompatibel ist.

Eine weitere Kritik betrifft die sogenannten Automatic-Modules. Legt man eine nicht modularisierte JAR-Bibliothek auf den Modulepath, wird die fehlenden module-info durch automatisch ermittelte Daten ersetzt. Näheres dazu finden Sie im folgenden Artikel „Java 9 Migration“. Dabei wird auch ein Modulname vergeben, der aus dem Namen des JAR-Archivs ermittelt wird. Das ist ein Problem für die Entwickler von Software-Bibliotheken. Wer jetzt seine Bibliothek für Java 9 modularisieren will, hat es mit vielen Abhängigkeiten zu tun, die selbst noch nicht modularisiert sind. So kann es leicht zu Konflikten kommen. Irgendwann wird die Third-Party-Bibliothek modularisiert, der Name ändert sich und man ist auf dem „Highway to Module-Hell“. Joda-Time-Entwickler Stephen Colebourne hat dazu einen fundierten Beitrag veröffentlicht , der das an konkreten Beispielen beschreibt und empfiehlt, die eigene Bibliothek nicht modularisiert zu veröffentlichen, solange sie nichtmodularisierte Abhängigkeiten hat.

Fazit

Java 9 ist ein Release, das im Vorfeld für große Kontroversen gesorgt hat. Mit dem Release eines Modularen JDK und des Modulsystems hat Java 9 einen für die Zukunftssicherheit wichtigen ersten Schritt unternommen, der aber noch weitere Nachbesserungen erfordert.

Anton Epple

Anton Epple ist Java-Entwickler. Zudem ist er als Berater tätig, von Startups bis hin zu weltweit führenden Firmen. Er hat sich auf Clienttechnologien spezialisiert und „DukeScript“ entwickelt, eine moderne Java-basierte Desktoptechnologie. Er gewann den „Duke‘s Choice Award“, ist Mitglied im NetBeans-Dream-Team, JavaONE-Rockstar und Java-Champion.

Sie haben jetzt einen ersten Eindruck von Java 9, wollen aber noch mehr? – JAVAPRO-Experte und Autor Anton Epple gibt sein Wissen auch weiter. Dazu hält Anton Epple immer wieder Kurse und Schulungen. Auf zwei dieser Schulungen stützen sich seine beiden Artikel.


06. Dezember 2017 – Java 9 Bootcamp

In diesem eintägigen Workshop lernen Sie die wichtigsten neuen Features von Java 9 kennen. Java 9 ist durch Projekt Jigsaw modular geworden. Hier lernen Sie, wie Sie Module nutzen und wie Sie selbst Module bauen können. Sie lernen neue Tools, wie die JShell kennen, die Ihnen völlig neue Ansätze beim Debugging bieten. Und wir sehen uns an, welche neuen Features das neueste Release zur Verfügung stellt. Wir stellen dabei die Neuerungen in den Vordergrund, die in der täglichen Entwicklungsarbeit relevant sind. So lernen Sie in einem kompakten Format anhand vieler Beispielanwendungen und Demos sicher und schnell alles Wissenswerte über das neue Release.

07. Dezember 2017 – Java 9 Migration

Java 9 hält mehr Herausforderungen für Entwickler bereit, als jedes bisherige Release. Die enorme Leistung, die Java Plattform zu modularisieren hat zu vielen internen Änderungen geführt, die nahezu jede Anwendung betreffen. In diesem Workshop lernen Sie Ihre Anwendung auf Java 9 Kompatibilität zu prüfen und erfolgreich zu portieren.

Weitere Infos unter eppleton.de/kurse

(Visited 107 times, 1 visits today)

Leave a Reply