Modulare Java-Zukunft: Das Java Platform Module System erklärt

Nachdem die Java-Welt Jahre lang nicht unbedingt einen Schwerpunkt auf Modularisierung legte – OSGi einmal ausgenommen –, tut sich in Java 9 mit dem JPMS einiges, was sich auf das gesamte Ökosystem auswirken wird.

Sprachen  –  13 Kommentare
Modulare Java-Zukunft: Das Java Platform Module System erklärt

Java 9 bringt eine Menge spannender Erweiterungen mit sich, aber keine hat im Vorfeld für so viel Diskussionen, Vorfreude und Aufruhr gesorgt wie das Modulsystem. Das Java Platform Module System (JPMS) ermöglicht es Entwicklern, Java-Anwendungen auf Artefaktebene zu modularisieren. Aber worum genau geht es dabei und welche Probleme hat Java diesbezüglich? Wie möchte das Modulsystem diese lösen und was bedeutet das für die Migration zu Java 9?

Die Philosophie des Modulsystem, aber auch die Features und Einschränkungen, die es mitbringt, lassen sich nur einordnen, wenn man zunächst den Hintergrund beleuchtet, vor dem dessen Entwicklung stattgefunden hat.

Wenn man ein System modularisiert, relativ unabhängig davon, auf welcher Ebene, gibt es drei grundlegende Eigenschaften, die Entwickler den einzelnen Modulen zuordnen können müssen:

  • einen Namen, der sie innerhalb des Systems identifizer- und referenzierbar macht
  • klar definierte Abhängigkeiten zu anderen Modulen
  • eine ebenso klar definierte API und daraus folgend die Möglichkeit, Interna zu verbergen

Wer zum Beispiel an Java-Klassen denkt, sieht diese drei Eigenschaften gleich: Klassen haben systemweit – idealerweise sogar global – eindeutige Namen, anhand derer andere Klassen sie referenzieren können, und ihre API ist durch die Menge der sichtbaren Methoden und Felder definiert. Interna, insbesondere ihr Zustand, aber auch die Implementierung der API, ist verborgen und, abgesehen von Reflection oder gar Bytecode-Manipulation, nicht zugreifbar.

Schaut man sich statt Klassen allerdings Java Archives (JARs) an, wird offensichtlich, dass Java hier eine ähnliche Modularisierung nicht nennenswert unterstützt. JARs haben zwar Dateinamen, die aber für den Compiler oder die JVM weitgehend bedeutungslos sind. Abhängigkeiten lassen sich nicht ausdrücken, und es gibt keine Möglichkeit, Funktionen innerhalb eines JARs, aber nicht darüber hinaus erreichbar zu machen, ohne auf unangenehme Hacks zurückgreifen zu müssen.

(So können Entwickler zum Beispiel alle Klassen einer Bibliothek in nur einem Paket halten, um dann dort Paketsichtbarkeit zu verwenden. Das nimmt ihnen allerdings die Möglichkeit, den Code durch Pakete ordentlich zu strukturieren.)

Der Grund für die mangelnde Unterstützung von Modularisierung auf Artefaktebene ist der Umgang von Compiler und virtueller Maschine (JVM) mit JARs. Die sind, wie der Name andeutet, tatsächlich nur Behälter für Klassen. Zur Kompilier- oder Laufzeit wird der Bytecode der gesuchten Klassen aus den JARs geladen und unabhängig davon weiterverarbeitet – es existiert keine Laufzeitrepräsentation von JARs. Folgerichtig besteht keine Option, artefaktspezifische Informationen zu verarbeiten.

Das mag zunächst nicht allzu bedeutend klingen. Entwickler und Tools ordnen Artefakten allerdings durchaus diese Eigenschaften – Namen, Abhängigkeiten und APIs – zu. Die so entstehende Diskrepanz zwischen der Vorstellung einer Systemarchitektur und ihrer Abbildung zur Kompilier- und Laufzeit ist nicht nur philosophisch unschön, sondern hat Konsequenzen.

So lassen sich die meisten Schwierigkeiten, die unter dem Namen JAR Hell zusammengefasst werden, darauf zurückführen. Darüber hinaus leiden Sicherheit und Wartbarkeit eines Projekts, insbesondere von Bibliotheken, wenn keine effektive Möglichkeit besteht, Implementierungsdetails zu schützen.

Als Beispiel für Sicherheitsprobleme kann Java selbst dienen: Ein erheblicher Teil der Sicherheitslücken, die Oracle nach dem Erwerb von Sun in Java 7 stopfen musste, stammten daher, dass sich sicherheitskritische Teile des Codes im Java Development Kit (JDK) von Schadcode erreichen und ausnutzen ließen. Wäre eine ordentliche Modularisierung möglich, die den Zugriff verhindert, wäre es dazu vermutlich nicht gekommen.

Ein Extrembeispiel dafür, wie die Wartbarkeit unter mangelnder Kapselung leiden kann, ist das JUnit-Projekt. Einer der Gründe dafür, dass JUnit 5 von Grund auf neu geschrieben wird und ein bestimmender Faktor in der neuen Architektur ist die enge Kopplung von Tools an Implementierungsdetails von Version 4. Hier haben IDE- und Build-Tool-Entwickler Reflection verwendet, um den Zustand von JUnit auszulesen und das in einem Maß, das es schwierig machte, das Projekt weiterzuentwickeln, ohne dabei die Funktionen der Tools zu beeinträchtigen.

Die mangelhafte Unterstützung für Modularisierung plagt das Java-Ökosystem schon lange, weshalb eine Menge Hilfsmittel zum Ausgleich entstanden sind. Zuallererst fallen hier Build-Tools ein, die explizit erlauben, zwei der drei Eigenschaften, nämlich Namen und Abhängigkeiten, nachzureichen. OSGi geht noch einen Schritt weiter und fügt die Möglichkeit zur Kapselung hinzu, indem es nur ausgewählte Pakete exportiert und die anderen unsichtbar sind.

Während Build-Tools nur einen Teil der Probleme lösen, sieht OSGi – vermutlich wegen seiner Komplexität – keine massenhafte Verwendung. Außerdem sind beide Ansätze keine Lösung für das JDK selbst. Um das anzugehen, wurde 2008 Project Jigsaw ins Leben gerufen, in dessen Rahmen das Oracle-Team zum einen die JDK-Code-Basis so umstrukturierte, dass überhaupt sinnvolle Module erkennbar sind, und zum anderen ein System implementierte, das es erlaubt, Module zu erzeugen.