14 Java Platform Module System
»Das Ganze ist mehr als die Summe seiner Teile.«
– Aristoteles (384–322 v. u. Z.)
Nachdem wir uns das Zusammenspiel von Klassen angeschaut haben, wollen wir uns in diesem Kapitel mit der Frage beschäftigen, wie einzelne Typen oder Verbände von anderen Programmen gut wiederverwendet werden können. Wir wollen uns das bei einzelnen Komponenten anschauen und bei einer Sammlung von Klassen, die zu Archiven zusammengebunden werden.
14.1 Klassenlader (Class Loader) und Modul-/Klassenpfad
Ein Klassenlader ist dafür verantwortlich, die Binärrepräsentation einer Klasse aus einem Hintergrundspeicher oder Hauptspeicher zu laden. Aus der Datenquelle (im Allgemeinen ist das die .class-Datei) liefert der Klassenlader ein Byte-Array mit den Informationen, die im zweiten Schritt dazu verwendet werden, die Klasse ins Laufzeitsystem einzubringen; das ist Linking. Es gibt vordefinierte Klassenlader und die Möglichkeit, eigene Klassenlader zu schreiben, um etwa verschlüsselte Klassendateien vom Netzwerk zu beziehen oder komprimierte .class-Dateien aus Datenbanken zu laden.
14.1.1 Klassenladen auf Abruf
Nehmen wir zu Beginn ein einfaches Programm mit drei Klassen:
package com.tutego.insel.tool;
public class HambachForest {
public static void main( String[] args ) {
boolean rweWantsToCutTrees = true;
Forrest hambachForest = new Forrest();
if ( rweWantsToCutTrees ) {
Protest<Forrest> p1 = new Protest<>();
p1.believeIn = hambachForest;
}
}
}
class Forrest { }
class Protest<T> {
T believeIn;
java.time.LocalDate since;
}
Wenn die Laufzeitumgebung das Programm HambachForest startet, muss sie eine Reihe von Klassen laden. Das tut sie dynamisch zur Laufzeit. Sofort wird klar, dass es zumindest HambachForest sein muss. Und da die JVM die statische main(String[])-Methode aufruft und Optionen übergibt, muss auch String geladen sein. Unsichtbar stecken noch andere referenzierte Klassen dahinter, die nicht direkt sichtbar sind. So wird zum Beispiel Object geladen, da implizit in der Klassendeklaration von HambachForest steht: class HambachForest extends Object. Intern ziehen die Typen viele weitere Typen nach sich. String implementiert Serializable, CharSequence und Comparable, also müssen diese drei Schnittstellen auch geladen werden. Und so geht das weiter, je nachdem, welche Programmpfade abgelaufen werden. Wichtig ist aber, zu verstehen, dass diese Klassendateien so spät wie möglich geladen werden.
14.1.2 Klassenlader bei der Arbeit zusehen
Im Beispiel lädt die Laufzeitumgebung selbstständig die Klassen (implizites Klassenladen). Klassen lassen sich auch mit Class.forName(String) über ihren Namen laden (explizites Klassenladen).
Um zu sehen, welche Klassen überhaupt geladen werden, lässt sich der virtuellen Maschine beim Start der Laufzeitumgebung ein Schalter mitgeben: -verbose:class. Dann gibt die Maschine beim Lauf alle Typen aus, die sie lädt. Nehmen wir das Beispiel von eben, so ist die Ausgabe mit dem aktivierten Schalter unter Java 11 fast 500 Zeilen lang. Hier folgt ein Ausschnitt:
$ java -verbose:class com.tutego.insel.tool.HambachForest
[0.010s][info][class,load] opened: C:\Program Files\Java\jdk-11\lib\modules
[0.032s][info][class,load] java.lang.Object source: jrt:/java.base
[0.032s][info][class,load] java.io.Serializable source: jrt:/java.base
[0.033s][info][class,load] java.lang.Comparable source: jrt:/java.base
[0.036s][info][class,load] java.lang.CharSequence source: jrt:/java.base
[0.037s][info][class,load] java.lang.String source: jrt:/java.base
…
[0.684s][info][class,load] sun.security.util.Debug source: jrt:/java.base
[0.685s][info][class,load] com.tutego.insel.tool.HambachForest source: file:/C:/
Inselprogramme/target/classes/
[0.687s][info][class,load] java.lang.PublicMethods$MethodList source: jrt:/java.base
[0.687s][info][class,load] java.lang.PublicMethods$Key source: jrt:/java.base
[0.689s][info][class,load] java.lang.Void source: jrt:/java.base
[0.690s][info][class,load] com.tutego.insel.tool.Forrest source: file:/C:/
Inselprogramme/target/classes/
[0.691s][info][class,load] jdk.internal.misc.TerminatingThreadLocal$1 source: jrt:/
java.base
[0.692s][info][class,load] java.lang.Shutdown source: jrt:/java.base
[0.692s][info][class,load] java.lang.Shutdown$Lock source: jrt:/java.base
Ändern wir die Variable rweWantsToCutTrees in true, so wird unsere Klasse Protest geladen, und in der Ausgabe kommt nur eine Zeile hinzu! Das wundert auf den ersten Blick, denn die Klasse referenziert LocalDate. Doch ein LocalDate wird nicht benötigt, also auch nicht geladen. Der Klassenlader bezieht nur Klassen, wenn sie für den Programmablauf benötigt werden, nicht aber durch die reine Deklaration als Attribut. Wenn wir LocalDate mit zum Beispiel LocalDate.now() initialisieren, kommen stattliche 200 Klassendateien hinzu.
14.1.3 JMOD-Dateien und JAR-Dateien
Der Klassenlader bezieht .class-Dateien nicht nur aus Verzeichnissen, sondern in der Regel aus Containern. So müssen keine Verzeichnisse ausgetauscht werden, sondern nur einzelne Dateien. Als Containerformate finden wir JMOD (neu in Java 9) und JAR. Wenn Java-Software ausgeliefert wird, bieten sich JAR- oder JMOD-Dateien an, denn es ist einfacher und platzsparender, nur ein komprimiertes Archiv weiterzugeben als einen großen Dateibaum.
JAR-Dateien
Sammlungen von Java-Klassendateien und Ressourcen werden in der Regel in Java-Archiven, kurz JAR-Dateien, zusammengefasst. Diese Dateien sind im Grunde ganz normale ZIP-Archive mit einem besonderen Verzeichnis META-INF für Metadateien. Das JDK bringt im bin-Verzeichnis das Werkzeug jar zum Aufbau und Extrahieren von JAR-Dateien mit.
JAR-Dateien behandelt die Laufzeitumgebung wie Verzeichnisse von Klassendateien und Ressourcen. Zudem haben Java-Archive den Vorteil, dass sie signiert werden können und illegale Änderungen auffallen. JAR-Dateien können Modulinformationen beinhalten. Dann heißen sie auf Englisch modular JAR.
JMOD-Dateien
Das Format JMOD ist speziell für Module, und es organisiert Typen und Ressourcen. Zum Auslesen und Packen gibt es im bin-Verzeichnis des JDK das Werkzeug jmod.
[»] Hinweis
Die JVM greift selbst nicht auf diese Module zurück. Achten wir auf die Ausgabe des letzten Programms, dann steht in der ersten Zeile:
[0.007s][info][class,load] opened: C:\Program Files\Java\jdk-11\lib\modules
Die Datei module ist ca. 170 MiB groß und liegt in einem proprietären Dateiformat vor.
JAR vs. JMOD
Module können in JMOD- und JAR-Container gepackt werden. Wenn ein JAR kein Modular JAR ist, also keine Modulinformationen enthält, so fehlen zentrale Informationen, wie Abhängigkeiten oder eine Version. Ein JMOD ist immer ein benanntes Modul.
JMOD-Dateien sind nicht so flexibel wie JAR-Dateien, denn sie können nur zur Übersetzungszeit und zum Linken eines Runtime-Images (dafür gibt es das Kommandozeilenwerkzeug jlink) genutzt werden. JMOD-Dateien können nicht wie JAR-Dateien zur Laufzeit verwendet werden. Das Dateiformat ist proprietär und kann sich jederzeit ändern – es ist nichts Genaues spezifiziert.[ 231 ](Die http://openjdk.java.net/jeps/261 besagt, dass es ein ZIP ist. ) Der einziger Vorteil von JMOD: Native Bibliotheken lassen sich standardisiert einbinden.
14.1.4 Woher die kleinen Klassen kommen: Die Suchorte und spezielle Klassenlader
Die Laufzeitumgebung nutzt zum Laden nicht nur einen Klassenlader, sondern mehrere. Das ermöglicht es, unterschiedliche Orte für die Klassendateien festzulegen. Ein festes Schema bestimmt die Suche nach den Klassen:
Klassentypen wie String, Object oder Point stehen in einem ganz speziellen Archiv. Wenn ein eigenes Java-Programm gestartet wird, so sucht die virtuelle Maschine die angeforderten Klassen zuerst in diesem Archiv. Da es elementare Klassen sind, die zum Hochfahren eines Systems gehören, werden sie Bootstrap-Klassen genannt. Die Implementierung dieses Bootstrap-Klassenladers ist Teil der Laufzeitumgebung.
Ist eine Klasse keine Bootstrap-Klasse, beginnt der System-Klassenlader bzw. Applikations-Klassenlader die Suche im Modulpfad (ehemals Klassenpfad/Classpath). Diese Pfadangabe besteht aus einer Aufzählung von Modulen, in denen die Laufzeitumgebung nach den Klassendateien und Ressourcen sucht.
14.1.5 Setzen des Modulpfades
Wo die JVM die Klassen findet, muss ihr mitgeteilt werden, und das ist in der Praxis elementar für die Auslieferung, die auch mit dem englischen Begriff als deployment bezeichnet wird. Java wartet mit dem Laden der Klassen so lange, bis sie benötigt werden. Es gibt zum Beispiel Programmabläufe nur zu besonderen Bedingungen, und wenn dann erst spät ein neuer Typ referenziert wird, der nicht vorhanden ist, fällt dieser Fehler erst sehr spät auf. Dem Compiler müssen folglich nicht nur die Quellen für Klassen und Ressourcen der eigenen Applikation mitgeteilt werden, sondern alle vom Programm referenzierten Typen aus zum Beispiel quelloffenen und kommerziellen Bibliotheken.
Sollen in einem Java-Projekt Dateien aus einem Verzeichnis oder einem externen Modul geholt werden, so besteht der übliche Weg darin, diese Dateien im Modulpfad anzugeben. Diese Angabe ist für alle SDK-Werkzeuge notwendig – am häufigsten ist sie beim Compiler und bei der Laufzeitumgebung zu sehen.
Setzen des Klassenpfades
Vor Java 9 gab es nur JAR-Dateien und Verzeichnisse im Klassenpfad. Auch wenn es ab Java 9 weiterhin den Klassenpfad gibt, sollte er auf lange Sicht leer sein.
Es gibt zwei Möglichkeiten zur Aufnahme von Verzeichnissen und JAR-Dateien in den Klassenpfad:
ein Schalter
eine Umgebungsvariable
Der Schalter -classpath
Die Suchorte lassen sich flexibel angeben, wobei die erste Variante einem SDK-Werkzeug über den Schalter -classpath (kurz -cp) die Klassendateien bzw. Archive liefert:
$ java -classpath classpath1;classpath2 mein.paket.MainClass
Der Klassenpfad enthält Wurzelverzeichnisse der Pakete und JAR-Dateien, also Archive von Klassendateien und Ressourcen.
[zB] Beispiel
Nimm ein Java-Archiv library.jar im aktuellen Verzeichnis, die Ressourcen unter dem bin-Verzeichnis und alle JAR-Dateien im Verzeichnis lib in den Klassenpfad mit auf:
$ java -cp "library.jar;bin/.;lib/*" mein.paket.MainClass
Unter Windows ist der Trenner ein Semikolon, unter Unix ein Doppelpunkt. Das Sternchen steht für alle JAR-Dateien, es ist keine übliche Wildcard, wie z. B. parser*.jar.[ 232 ](Weitere Details unter https://docs.oracle.com/javase/8/docs/technotes/tools/windows/classpath.html) Sehen Kommandozeilen der Betriebssysteme ein *, beginnen sie in der Regel eine eigene Verarbeitung. Daher muss hier die gesamte Pfadangabe in doppelten Anführungszeichen stehen.
Die Umgebungsvariable CLASSPATH
Eine Alternative zum Schalter -cp ist das Setzen der Umgebungsvariablen CLASSPATH mit einer Zeichenfolge, die Pfadangaben spezifiziert:
$ SET CLASSPATH=classpath1;classpath2
$ java mein.paket.MeinClass
Problematisch ist der globale Charakter der Variablen, sodass lokale -cp-Angaben besser sind. Außerdem »überschreiben« die -cp-Optionen die Einträge in CLASSPATH. Zu guter Letzt: Ist weder CLASSPATH noch eine -cp-Option gesetzt, besteht der Klassenpfad für die JVM nur aus dem aktuellen Verzeichnis, also ».«.
Um in Eclipse den Klassenpfad zu erweitern, damit etwa die Klassendateien von Java-Archiven berücksichtigt werden, ist Folgendes zu tun: Im Projekt das Kontaktmenü öffnen und Properties aufrufen, dann links unter Java Build Path gehen und anschließend im Reiter Libraries entweder Add JARs… (JARs sind im Projekt) oder Add External JARs… (JAR-Dateien liegen nicht im Projekt, sondern irgendwo anders im Dateisystem) nutzen.
[»] Hinweis
Die sogenannten Bootstrap-Klassen aus den Paketen java(x).* (wie Object, String) stehen nicht im CLASSPATH.
Classpath-Hell
Java-Klassen in JAR-Dateien auszuliefern, ist der übliche Weg. Es gibt aber zwei Probleme:
Aus Versehen können zwei JAR-Dateien mit unterschiedlichen Versionen im Klassenpfad liegen. Nehmen wir an, es sind parser-1.2.jar und parser-2.0.jar, wobei sich bei der neuen Version API und Implementierung leicht geändert haben. Das fällt vielleicht am Anfang nicht auf, denn einen Ladefehler gibt es für den Typ nicht, er ist ja da – die JVM nimmt den ersten Typ, den sie findet. Nur wenn ein Programm auf die neue API zurückgreift, aber die geladene Klasse vom alten JAR stammt, knallt es zur Laufzeit. Bei doppelten JARs mit unterschiedlichen Versionen führt eine Umsortierung im Klassenpfad zu einem ganz anderen Ergebnis. Zum Glück lässt sich das Problem relativ schnell lösen.
Zwei Java-Bibliotheken – nennen wir sie vw.jar und audi.jar – benötigen je eine Neben-JAR zum Arbeiten. Doch während vw.jar die Version bosch-1.jar benötigt, benötigt audi.jar die Version bosch-2.jar. Das ist ein Problem, denn JARs sind im Standard-Klassenpfad immer global, aber nicht hierarchisch. Es kann also kein JAR ein »lokales« Unter-JAR haben.
Lösungen für das zweite Problem gibt es einige, wobei zu neuen Klassenladern gegriffen wird. Bekannt ist OSGi, das in der Java-Welt aber etwas an Fahrt verloren hat.