Über unsMediaKontaktImpressum
Dr. Rüdiger Grammes, Martin Lehmann & Dr. Kristine Schaal 15. Februar 2018

Java 9 bringt das neue Modulsystem Jigsaw

Java 9 ist seit September 2017 verfügbar [1]. Es enthält mit "Project Jigsaw" das neue Modulsystem, das Java um Module als neues Sprachfeature erweitert. Jigsaw ist eine sehr grundlegende Strukturänderung von Java-Plattform und -Sprache – höchste Zeit also, sich das genauer anzusehen.

Wir erläutern in diesem Artikel die nötigen Grundlagen von Jigsaw und beschreiben einige fortgeschrittene Aspekte des Modulsystems sowie das Thema Migration.

Das vorliegende Tutorial zum Java-Modulsystem Jigsaw erschien ursprünglich im Februar 2017, wurde aber im Februar 2018 nach Erscheinen von Java 9 von den Autoren überarbeitet und auf den neuesten Stand gebracht.

Java 9 und Jigsaw: Einführung von Modulen

Die Einführung von Modulen in Java hat eine sehr lange Historie. Mit OSGi wurde schon im Jahr 2000 eine Spezifikation eines Java-basierten Modulsystems eingeführt. OSGi wird weiterentwickelt, führt aber eher ein Nischendasein, zum Beispiel im Embedded-Umfeld.

Im Java Specification Request (JSR) 277 wurde ab 2005 ein erster Versuch unternommen, ein natives Modulsystem in Java zu etablieren: Es sah unter anderem Modul-Versionierung und -Repository vor, kam aber nie über den Draft-Status hinaus und ist seit Sommer 2016 zurückgezogen (zusammen mit JSR 294).

2008 wurde das neue Modulsystem Jigsaw angekündigt, danach aber mehrfach verschoben, vor allem, da sich die Modularisierung des monolithisch gewachsenen JDKs als sehr komplex herausgestellt hat. Als "Project Jigsaw" wird es im JSR 376 spezifiziert [2] und fasst so verschiedene Aktivitäten zusammen:

  • Aktivitäten rund um die Java-Spracherweiterung für das neue Modulsystem und
  • Aktivitäten rund um die Modularisierung der Java-Plattform selbst (worunter auch Reorganisation des Java-Quellcodes, die Definition der Plattform-Module sowie die Kapselung interner APIs fallen).

Verschiedene Java Enhancement Proposals (JEPs 200, 201, 220, vor allem 260, 261 und 282) treiben die Entwicklung von Jigsaw voran [3]. Seit Anfang 2016 ist Jigsaw in Java 9-Early-Access-Builds integriert. Im September 2017 wurde das Release für Java 9 freigegeben. Aktuell ist seit Januar 2018 die Version 9.0.4 verfügbar.

Java 9 und Jigsaw: Warum überhaupt ein Modulsystem?

Java ist über 20 Jahre alt. Seitdem ist die Java-Plattform stark monolithisch gewachsen und eine Menge unerwünschter Abhängigkeiten sind entstanden [7]. Solche Altlasten soll Jigsaw beseitigen, indem die Plattform aufgeräumt und modularisiert wird.

Bei dieser Modularisierung der Java-Plattform wollten die Java-Architekten aber nicht haltmachen. Stattdessen soll die Modularisierung von Anwendungen unterstützt werden: Entwickler strukturieren schon lange ihre Anwendungen in fachliche und technische Komponenten, um sie übersichtlicher und wartbarer zu machen. Dafür bot Java bisher keine direkte Unterstützung, weswegen man auf Package-Namenskonventionen [8] bzw. auf externe Tools wie zum Beispiel Maven zurückgegriffen hat [9].

Jigsaw erlaubt nun die native Modulbildung innerhalb der Sprache und verfolgt dabei vor allem drei Ziele [10]:

  • Reliable Configuration: Der fehleranfällige Classpath soll abgelöst werden. Wenn eine Klasse mehrfach (z. B. in mehreren JARs) auf ihm enthalten ist, entscheidet allein die Suchreihenfolge, welche Instanz der Klasse benutzt wird. Dies soll durch Modul-Abhängigkeiten und den neuen Modul-Path aufgelöst werden.
  • Strong Encapsulation: Ein Jigsaw-Modul definiert sein öffentliches Komponenten-API. Andere Module können auf nicht-öffentliche Klassen nicht zugreifen.
  • Scalable Platform: Die modularisierte Java-Plattform ermöglicht, angepasste, schlankere Runtime-Images für eine Anwendung zu bauen und nur auszurollen, was diese wirklich benötigt.

Vergleich zwischen OSGi und Jigsaw

Wie unterscheidet sich Jigsaw von einem bekannten Modulsystem wie OSGi? In OSGi werden Komponenten als OSGi-Bundles über Classloader-Mechanismen gekapselt, was jedoch umgangen werden kann. In Jigsaw kann der Zugriffsschutz nicht umgangen werden, da er tief in der Plattform verankert ist.

OSGi unterstützt Komponenten-Versionierung. Jigsaw kennt eine solche Versionierung von Modulen nicht, was auch stark kritisiert wurde. Außerdem haben Jigsaw-Module – anders als OSGi-Bundles – keinen Lebenszyklus: Jigsaw-Module lassen sich zur Laufzeit also nicht starten oder stoppen.

Java 9 und Jigsaw-Module: Abhängigkeiten mit Reads, Zugriffsschutz mit Exports

Ein Jigsaw-Modul ist eine Gruppierung von Java-Packages plus Ressourcen. Ein Modul wird kompiliert, als JAR-File paketiert und als "Modular JAR" auf den neuen Module-Path gelegt.

Der Modul-Deskriptor module-info.java definiert den Modulnamen, alle Modul-Abhängigkeiten und legt die Sichtbarkeitsregeln auf die Inhalte des Moduls fest. Abb.1 zeigt dies beispielhaft für zwei Module moda und modb.

Damit moda überhaupt Klassen aus modb benutzen kann, benötigt es eine "Reads"-Abhängigkeit auf modb (blauer Pfeil in der Abbildung). Dies legt Abhängigkeiten zwischen Modulen über deren Namen fest.

modb definiert über seine "Exports" (roter Pfeil), welche seiner Packages für andere Module sichtbar und zugreifbar sind. Die Granularität eines Exports ist dabei immer ein Java-Package. In unserem Beispiel darf moda nur auf das Package pkgb von modb zugreifen, nicht aber auf dessen interne Package pkgbinternal. Per Default sind keine Packages exportiert: Was ein Modul also nicht ausdrücklich mit Exports freigibt, ist für andere Module nicht sichtbar.

Diese neue Exports-Sichtbarkeit spielt mit den altbekannten Sichtbarkeitsmodifiern von Klassen, Attributen, Methoden (also mit public, private, protected, package-sichtbar) wie folgt zusammen: Die Sichtbarkeitsmodifier gelten unverändert innerhalb eines Moduls – über Modulgrenzen hinweg gelten sie auch, greifen jedoch erst, nachdem Reads und Exports erfolgreich überprüft wurden. Will in unserem Beispiel eine Klasse A aus moda auf eine Klasse B in modb zugreifen, so werden sowohl zur Compile- als auch zur Laufzeit diese Zugriffsprüfungen vorgenommen:

  • Check der Readability (neu): Existiert eine Reads-Beziehung von moda zu modb?
  • Check der Accessibility (neu): Exportiert modb das Package der Klasse B?
  • Erst danach werden, wie bisher, die Sichtbarkeitsmodifier public, private, protected, package-sichtbar für B geprüft.
    • Sind Methoden bzw. Attribute von B public, so sind sie innerhalb des Moduls modb von überall zugreifbar. Aus anderen Modulen wie moda sind sie nur zugreifbar, wenn modb das Package der Klasse B exportiert – denn erst damit wird der Typ B überhaupt nach außen sichtbar.
    • Ist das Package von B exportiert, so sind private Methoden bzw. Attribute – wie bisher – aus anderen Modulen trotzdem nicht zugreifbar. Beim Zugriff über Reflection gelten zusätzliche Regeln, die wir unten erläutern.

Java 9 und Jigsaw: Moduldefinition in der module-info.java

Einen Modul-Deskriptor legt man als Java-Datei an. Er muss als module-info.java im Toplevel-Package abgelegt sein. Wir illustrieren das an einem Beispiel mit vier Modulen moda, modb, modc und modmain.

Listing 1: module-info von Modul moda

module moda {
    // Read-Abhaengigkeiten von moda zu modb und modc
    requires modb;
    requires transitive modc;  // transitive Abhaengigkeit

    // Exports von Packages von moda
    exports pkga1;
    exports pkga2 to modmain;  // ... nur an modmain

    opens pkga3;               // ... nur zur Laufzeit
}

Listing 1 zeigt die module-info von moda mit allen wesentlichen Aspekten eines solchen Modul-Deskriptors:

  • Modulname "moda". Die Namenskonvention für Module ist ähnlich wie die für Java-Identifier: So sind Punkt und Underscore im Modulnamen erlaubt, Bindestriche nicht.
  • Alle Reads-Abhängigkeiten von moda durch Angabe von requires.
  • Exports der Packages, die moda nach außen exportiert. Dabei kann jeder Export gezielt auf ein oder mehrere Zielmodule beschränkt werden (im Beispiel wird pkga2 nur an modmain exportiert).

Jedes Reads und jedes Exports muss in der module-info einzeln angegeben werden! Leider gibt es keine Wildcard-Unterstützung für Exports. Packages bilden – wie bisher – keine echte Hierarchie: Ein Exports von x.y exportiert also x.y.impl nicht mit. Wie erwähnt, wird ein Package nicht exportiert, sofern es in der module-info nicht explizit angegeben ist. Alle Typen solcher "Concealed Packages" sind außerhalb ihres Moduls nicht sichtbar und nicht zugreifbar.

Modul-Graphen mit allen Abhängigkeiten

Abb.2 zeigt alle Abhängigkeiten unserer vier Beispiel-Module. Die Beispiele unseres Artikels finden sich allesamt auf Github, zusammen mit unserem DepVis-Tool, mit dem auf Basis von GraphViz solche Jigsaw-Modul-Graphen visualisiert werden können [11].

Der Abhängigkeitsgraph zeigt zusätzlich das Plattform-Modul java.base, das Java-Basis-Klassen der Java-Plattform enthält, unter anderem aus java.lang, java.io, java.util, java.net. Da jedes Modul diese Basis-Klassen braucht, erstellt Jigsaw automatisch eine Abhängigkeit zu java.base (als "requires mandated"). Ein Reads zu java.base ist immer implizit vorhanden und muss nicht in der module-info angegeben werden.

Compile und Run

Die Java-Datei module-info.java wird zusammen mit den Sourcen des Moduls kompiliert. Wir kompilieren in das Zielverzeichnis mods und paketieren jedes unserer vier Module in je ein Modular JAR:

javac -d mods --module-path mlib --module-source-path src $(find src -name "*.java")

jar --create --file=mlib/moda.jar    -C mods/moda .
jar --create --file=mlib/modb.jar    -C mods/modb .
jar --create --file=mlib/modc.jar    -C mods/modc .
jar --create --file=mlib/modmain.jar -C mods/modmain .  

Dem Java-Compiler übergeben wir dabei den Module-Path als mlib: Dort werden Module gesucht, zusätzlich zu den Plattform-Modulen wie java.base. Schon der Compiler prüft, ob alle abhängigen Module vorhanden sind. Zur Compile-Zeit sind zyklische Abhängigkeiten zwischen Modulen nicht erlaubt. Die vier Modular JARs liegen nach obigem Compile- und Paketierungsschritt in mlib ab.

Beim Start unserer Anwendung geben wir dem Java-Launcher ebenfalls den Module-Path mit und starten mit –m die Main-Klasse pkgmain.Main aus Modul modmain.

java --module-path mlib -m modmain/pkgmain.Main

Fehlt eine Modulabhängigkeit auf dem Module-Path oder in der Java-Plattform, so startet die JVM nicht.

Transitive Abhängigkeiten

Reads-Abhängigkeiten zwischen Modulen sind nicht transitiv. Im folgenden Beispiel gibt die Methode createC der Klasse A aus moda eine Instanz von Typ C aus modc zurück. Eine Klasse Main von Modul modmain ruft die Methode auf, erhält also eine Instanz eines C als Ergebnis.

Listing 2: modmain/Main nutzt moda/A 

// Klasse aus Modul modapublic class A {
    // gibt Typ aus modc zurueck
 public C createC() {
        return new C();
    }
}

// Klasse aus Modul modmain (mit reads nur auf moda)public class Main {
   public static void main(String[] args) {

        // kompiliert immer, auch wenn Typ C nicht sichtbar ist
        Object myc1 = new A().createC();

        // kompiliert nur, wenn C in Main sichtbar ist
        pkgc.C myc2 = new A().createC();

...


Listing 2 zeigt, dass eine Klasse in modmain den Rückgabetyp C nur verarbeiten kann, wenn dieser für modmain sichtbar ist. Ein Zugriff von modmain auf modc ist eigentlich nur möglich, wenn modmain ein Reads auf modc besitzt. Nun sollen Abhängigkeiten von moda aber auch dort gekapselt sein: Wer moda benutzt, soll nicht zusätzlich alle Abhängigkeiten von moda kennen müssen.

Solche transitiven Abhängigkeiten kann man in einer module-info als requires transitive deklarieren: Der Modul-Deskriptor von moda in Listing 1 gibt jedem abhängigen Modul automatisch eine transitive Reads-Abhängigkeit zu modc (grüner Pfeil in Abb.2). Das funktioniert auch über eine Kette von requires transitive-Abhängigkeiten.

requires transitive ist ein mächtiges Feature. Damit lassen sich Aggregator-Module definieren, die selber keinen eigenen Inhalt besitzen, sondern nur Abhängigkeiten zu anderen Modulen an einer zentralen Stelle unter ihrem Namen bündeln. Die Java-Plattform hat über diesen Mechanismus die Sprachprofile java.compact1,2,3 definiert (die aber im finalen Java 9-Release nicht mehr enthalten sind):

Listing 3: Aggregator-Module

cd $JAVA_HOME/jmods
$JAVA_HOME/bin/jmod describe java.compact*.jmod
java.compact1@9-ea
    requires mandated java.base
    requires transitive java.logging
    requires transitive java.scripting
...
java.compact2@9-ea
    requires mandated java.base
    requires transitive java.compact1
    requires transitive java.rmi
    requires transitive java.sql
    requires transitive java.xml
...
java.compact3@9-ea
    requires mandated java.base
    requires transitive java.compact2
    requires transitive java.compiler
    requires transitive java.instrument
    requires transitive java.management
    requires transitive java.naming
    requires transitive java.prefs
    requires transitive java.security.jgss
    requires transitive java.security.sasl
    requires transitive java.sql.rowset
    requires transitive java.xml.crypto
...

Jigsaw: Typensichtbarkeit über Modulgrenzen hinweg

Bei den neuen Exports-Sichtbarkeitsregeln geht es letztlich immer um Typensichtbarkeit, nachstehend gezeigt an "Ableitung", "Interface" und "Exceptions". Zur Laufzeit verlieren Objekte ihren konkreten Typ an der Modulgrenze nicht. Der Typ ist außerhalb seines Moduls jedoch nur sichtbar und nutzbar, wenn sein Package exportiert wurde (s. unsere Github-Beispiele "example_derived_private-package-protected" und "example_exceptions"[11]).

Betrachten wir zunächst folgende Ableitungshierarchie (vgl. auch Abb.3):

  • Das exportierte Package pkga enthält die Klasse Data.
  • Das nicht exportierte Package pkgainternal enthält die abgeleitete Klasse InternalData.

Eine Factory-Klasse erzeugt Instanzen von Data (s. Listing 4). Sie ist exportiert und kann aus anderen Modulen aufgerufen werden:

Listing 4: Factory zur Erzeugung von Data-Instanzen

package pkga;

import pkgainternal.*;

public class Factory {
    public Data createData() { 
        return new Data(); 
    }

    public Data createInternalData1() {
        return new InternalData(); 
    }

    public InternalData createInternalData2() {
        return new InternalData(); 
    }
}

Ein Aufruf der Methoden createData bzw. createInternalData1 kompiliert, denn ihr Ergebnistyp Data ist ebenfalls exportiert. Dagegen führt ein Aufruf von createInternalData2 zu einem Compile-Fehler, denn InternalData ist nicht exportiert. Ein Aufruf von createInternalData1 liefert eine Instanz von InternalData gekapselt als dessen Oberklasse Data. Das kompiliert, denn nach außen ist der Typ der Oberklasse ja sichtbar. Ein Cast auf die nicht sichtbare Unterklasse InternalData ist dagegen nicht möglich und verhindert einen Aufruf InternalData-spezifischer Methoden.

Analog ist das Verhalten bei Interface und Implementierung: Wäre Data ein Interface, so erhielte eine Klasse in einem anderen Modul eine Implementierungsinstanz, sähe aber nur den Interface-Typ und die dort spezifizierten Methoden. Und genauso verhalten sich auch Ableitungshierarchien von Exceptions: Wird ein eigener Exception-Typ definiert, aber nicht aus seinem Modul exportiert, so ist er in anderen Modulen nicht sichtbar. Solche interne Exceptions können über Modulgrenzen geworfen werden, außerhalb aber nur als Oberklasse wie Exception oder RuntimeException gefangen werden.

Zugriff mit Reflection?

Die beschriebenen Sichtbarkeitsregeln greifen sowohl zur Compile- als auch zur Laufzeit. Auf nicht exportierte Packages kann aus anderen Modulen nicht zugegriffen werden, auch nicht per Reflection. Was bei der Anwendungsentwicklung sinnvoll ist, gilt in dieser Schärfe nicht für technische Frameworks wie Spring (für Dependency-Injection) oder Hibernate (zur Persistierung von Datenklassen).

Der Umgang mit Reflection in Jigsaw hat 2016 einiges Hin und Her erlebt und wurde mit verschiedenen Konzepten umgesetzt. Im September 2016 entschied man sich letztlich, "Strong Encapsulation" umzusetzen. Dabei unterscheidet man "Shallow" und "Deep Reflection":

  • Shallow Reflection ist ein Reflection-Zugriff, der "sowieso" möglich wäre, weil ein Typ auch direkt statisch nutzbar ist.
  • Deep Reflection bedeutet, dass ein Zugriff per Reflection auf einen Typ durchgeführt wird, der eigentlich nicht zugreifbar ist, zum Beispiel weil er private oder protected ist. Dies war bis Java8 durch setAccessible(true) möglich und ist nun nicht mehr per se erlaubt.

Seit dem Jigsaw-Build 142 gibt es einen neuen Ansatz, der den Export zur Laufzeit für Reflection und die Kapselung vor Deep Reflection vereinigt: opens erlaubt Zugriff nur zur Laufzeit, nicht zur Compile-Zeit und öffnet ein Package für "Deep Reflection" mit setAccessible(true). Ist ein Package eines Moduls also mit opens markiert, so kann eine externe Bibliothek wie zum Beispiel "Spring per Deep Reflection" darauf zugreifen, also auch private Klassen sehen und instanzieren.

Tabelle 1: opens, exports und mögliche Zugriffe zur Compile- und Laufzeit

Zugriff... Compile-Zeit Reflection (Shallow) Reflection (Deep)
exports pkg Erlaubt Erlaubt Nicht erlaubt
opens pkg Nicht erlaubt Erlaubt Erlaubt
exports pkg und opens pkg Erlaubt Erlaubt Erlaubt

Als syntaktischen Zucker kann man ein Modul als "Open Module" beschreiben, womit automatisch alle Packages des Modules mit opens geöffnet werden.

Binding von Modulen zur Startzeit mit uses/provides

Mit uses/provides werden Abhängigkeiten nicht statisch zur Compile-Zeit, sondern erst zur Startzeit aufgelöst. Das illustrieren wir am Beispiel eines JDBC-Treibers: Dessen Auswahl wird in Anwendungen i.d.R. erst zur Startzeit festgelegt (unser Github-Beispiel "example_uses-provides" [11]).

Listing 5 zeigt, wie das Java-Modul java.sql (oben) die Schnittstelle java.sql.Driver über uses definiert, die ein Treiber implementieren muss: Das Modul com.mysql.jdbc (unten) bietet mit provides eine solche Implementierung an:

Listing 5: Uses/Provides

module java.sql {
...
   // Definiert Schnittstelle und exportiert sie
   uses java.sql.Driver;
   exports java.sql;
}


module com.mysql.jdbc {
   requires java.sql;
...
   // Implementiert Schnittstelle?
   provides java.sql.Driver with com.mysql.jdbc.Driver;
}

Zur Laufzeit stellt java.util.ServiceLoader Implementierungen einer Schnittstelle zur Verfügung. Er bietet alle beim Start vorhandenen Implementierungen über einen Iterator an, die Auswahl muss die Anwendung selbst vornehmen.

Split Package ist nicht erlaubt

Module müssen disjunkte Packages haben, zwei oder mehr Module dürfen also kein Package gleichen Namens enthalten. Ein solches "Split Package" auf zwei oder mehr Module führt zu Fehlern (s. Github-Beispiel "example_splitpackage" [11]):

  • Werden Module mit namensgleichen Packages gemeinsam kompiliert, so führt das zu einem Compile-Fehler.
  • Werden solche Module getrennt (erfolgreich) kompiliert, so führt ein gemeinsames Laden zu Laufzeitfehlern beim JVM-Start.

Diese Restriktion gilt selbst für interne "Concealed Packages", also für Packages, die gar nicht exportiert sind. Und sie gilt selbst für Packages, die keine Klassenduplikate in den betroffenen Modulen enthalten!

Erweiterung des statischen Modells

Der Modul-Deskriptor module-info muss zur Compile-Zeit vollständig vorhanden sein. Da module-info zu einer .class-Datei kompiliert wird, kann man diese im JAR nicht einfach ändern. Man kann jedoch per Kommandozeilenoption an Java-Compiler bzw. -Launcher den Module-Deskriptor erweitern. Tabelle 2 zeigt die Möglichkeiten.

Tabelle 2: Kommandozeilenoptionen für javac und java

Man kann ... Kommandozeilenoption
weitere Packages exportieren --add-exports
(auch in Manifest.MF möglich)
eine neue Read-Beziehung einführen --add-reads
ein weiteres Modul dazuladen --add-modules
("Nimm-Alles" über ALL-MODULE-PATH)
Module mit Klassen patchen --patch-module

Wenn man viele Optionen benötigt, wird das schnell unübersichtlich und muss konsistent in Skripten, Tools und im Build-System wie Jenkins gepflegt werden. Um die Redundanz zu vermeiden, kann man die Optionen auch in eine Datei auslagern und an Java-Compiler bzw. -Launcher mit @file als Datei übergeben.

Testen mit Blackbox-Tests

Module beeinflussen die Organisation von Tests. Wenn wir ein Modul moda mit Blackbox-Tests testen wollen, also Tests gegen die öffentliche Schnittstelle von moda, so können wir unseren Testcode in ein eigenes Test-Modul ablegen. Dieses Test-Modul erhält eine "Reads"-Abhängigkeit auf moda. Die zu testende öffentliche Schnittstelle des Moduls muss exportiert sein und ist somit für die Tests zugreifbar. Abb.4 zeigt das am Beispiel.

Testen mit Whitebox-Tests

Dagegen benötigen Whitebox-Tests, also Tests des Implementierungsgeheimnisses, Zugriff auf nicht-exportierte Klassen von moda. Im Beispiel sollen Klassen aus dem Package pkgainternal getestet werden. Es gibt mehrere Varianten, die Tests zu strukturieren:

  • Variante 1a: Wir erstellen, analog zum obigen Vorgehen beim Blackbox-Test, ein separates Modul, modtest.whitebox. Dann müsste moda sein Package pkgainternal exportieren. Diesen Zugriff kann man mit exports to modtest.whitebox einschränken. Damit hat moda aber eine unschöne statische Abhängigkeit zum Testcode!
  • Variante 1b: Alternativ kann man den Zugriff zur Compile- und Startzeit gezielt über Kommandozeilenoptionen freischalten:
javac --add-exports modfib/pkgfib.internal=modtest.whitebox
java  --add-exports modfib/pkgfib.internal=modtest.whitebox

Das bedeutet jedoch Aufwand für die Pflege der entsprechenden Skripte.

  • Variante 2a: Wir erstellen die Testklassen im zu testenden Modul moda. Dann haben sie Zugriff auf pkgainternal. Wenn die Testklassen aber im Modul moda liegen, werden sie immer mit moda mit-paketiert und mit-deployed.
  • Variante 2b: Wir nutzen die Option, ein Modul mit externen Klassen zu patchen (--patch-module). So können wir den Testcode separat verwalten und trotzdem zur Compile- und Laufzeit zu moda dazupatchen, wenn wir Tests ausführen. Der Compile-Aufruf zum Patchen des Moduls lautet:
javac  --patch-module moda=src --add-reads moda=junit -d patches/moda
        ...  src/modtest.whitebox/pkga/WhiteBoxTest.java

Die Option --patch-module moda=src besagt, dass die Testklassen kompiliert werden sollen, als wären sie Teil von moda. Die "Reads"-Abhängigkeit von moda auf junit ist notwendig, da unsere Testklassen eine Reads-Abhängigkeit auf JUnit benötigen.

Starten kann man die Tests mit diesem Aufruf:

java  --patch-module moda=patches/moda --add-reads moda=junit
        ...  -m junit/org.junit.runner.JunitCore pkga.WhiteBoxTest

Wir patchen Testklassen so zur Test-Zeit in das Modul moda.

Der Classpath existiert weiter

In Java 9 wird der Module-Path neu eingeführt, auf dem Module gesucht werden (sozusagen der Ersatz des alten Classpath für Module). Java 9 unterstützt daneben auch weiterhin den altbekannten Classpath. Es ist also weiterhin möglich, "alte" JAR-Dateien bzw. Klassen auf diesem Classpath abzulegen.

In einer reinen Plattform-Migration kann man eine bestehende Java8-Anwendung also zunächst einfach auf dem Classpath belassen und zunächst nur JDK- bzw. JRE-Version nach Java 9 "anheben". Ein solches Vorgehen nutzt keine Module (außer die System-Module der Java-Plattform selbst, z. B. java.base). Vorsicht bei gleichzeitiger Verwendung von Module-Path und Classpath: Eine Klasse wird zuerst auf dem Module-Path gesucht, erst danach im Classpath. Anders als bei Named Modules wird dabei nicht auf Split Package geprüft!

Modulkategorien für die Migration von Java-Anwendungen

Es gibt verschiedene Faktoren, die man bei der Migration einer bestehenden Anwendung nach Jigsaw berücksichtigen muss. Die eigentliche Anwendung muss entsprechend ihrer fachlichen Säulen und technischen Schichten aufgeteilt werden. Bei bestehenden Anwendungen muss man dazu häufig ein gewachsenes Abhängigkeitsgeflecht auflösen. Dies erfordert oft größere Refactorings, zum Beispiel zum Aufbrechen von Zyklen.

Auch das Ökosystem muss eingebunden werden: Manche Third-Party-Bibliotheken stehen vielleicht schon in Modulform (also als Modular JAR) zur Verfügung, andere noch nicht oder eventuell nie (weil sie nicht mehr weiterentwickelt werden). Eine "Big Bang"-Umstellung ist nicht nur riskant, sondern oft unmöglich – eine schrittweise Migration ist wie so oft auch hier der bessere Weg. Jigsaw unterstützt die Migration von Anwendungen durch verschiedene Kategorien von Modulen: Es gibt Explicit Modules, Open Modules (als spezielle Form von Explicit Modules), Automatic Modules und das Unnamed Module.

Tabelle 3: Modulkategorien und ihre Beziehungen

Modulkategorie Beschreibung
Unnamed Module Alle Inhalte des Classpaths werden automatisch als sogenanntes Unnamed Module zusammengeführt.
  • Es hat Reads-Beziehungen auf sämtliche Module des Module-Paths.
  • Es exportiert sämtliche Packages aller seiner Klassen und JAR-Dateien.
  • Aber nur Automatic Modules haben automatisch eine Reads-Beziehung auf das Unnamed-Module.
  • Named Module Jedes Explicit Module, jedes Open Module und jedes Automatic Module hat einen Modulnamen. Sie sind daher "Named Modules".
  • Explicit Module
  • Vollwertige Module in Jigsaw heißen "Explicit Modules". Sie haben eine module-info und benennen so Reads-Abhängigkeiten sowie die Exports ihrer Packages, wie zu Beginn des Artikels vorgestellt.
  • Explicit Modules und Open Modules haben ohne Weiteres keine Reads-Beziehungen auf das Unnamed Module, können dessen Klassen also nicht sehen.
  • Reads-Beziehungen zu Automatic Modules sind dagegen möglich.
  • Open Module
  • Wie Explicit Module. Zusätzlich exportiert ein Open Module seine sämtlichen Packages implizit zur Laufzeit für Deep Reflection.
  • Automatic Module
  • Man kann ein "normales" JAR-File, das kein module-info enthält, auf den Module-Path legen. Es wird dann zu einem Automatic Module.
    • Es hat Reads-Beziehungen auf sämtliche Module: Ein Automatic Module kann also sämtliche exportierte Packages der Explicit Modules benutzen.
    • Alle seine Packages werden automatisch exportiert (auch für Deep Reflection).
    Andere Module haben ihrerseits vollen Zugriff auf alle Packages eines solchen Automatic Module, sofern sie eine Reads-Beziehung dahin haben. Nur Automatic Modules haben eine Reads-Beziehung auf das Unnamed Module (im Gegensatz zu den Explicit Modules).

    Automatic Modules mit Jigsaw: Automatic for the People

    Vor allem die Automatic Modules sind ein wichtiges Hilfsmittel für die Migration, indem man alte JAR-Files, v.a. externe Bibliotheken zunächst ohne großen Aufwand als Module nutzen kann. Auch Automatic Modules haben einen Namen, der von Jigsaw generiert wird. Es nutzt dazu den Dateinamen der JAR-Datei und entfernt dabei etwaige Versionsnummern. So wird beispielsweise aus junit-4.12.jar automatisch der Modulname junit. Automatic Modules bilden eine Brücke zum alten Classpath: Im Gegensatz zu Explicit Modules ist das Unnamed Module für jedes Automatic Module sichtbar.

    Neue Werkzeuge: module-info auslesen und generieren

    jar, jdeps und javap können den Inhalt der module-info.class aus einem JAR ausgeben. Das neue jmod-Tool kann dies für JMOD-Dateien (die in Java9 als JVM-internes Paketierungsformat dazukommen).

    Umgekehrt kann man mit jdeps eine module-info-Datei aus einem "normalen" JAR generieren. jdeps kann dabei alle Reads-Beziehungen vollständig ermitteln und generiert nicht nur die passenden requires-Statements, sondern sogar requires transitive, wenn Typen eines fremden JARs in der Schnittstelle stehen. Welche Packages als öffentliche Schnittstelle des neuen Moduls exportiert werden sollen, kann jdeps allerdings nicht wissen und generiert sicherheitshalber ein exports für alle Packages (die man also manuell ausdünnen muss). jdeps ist ein wertvolles Werkzeug, um ein altes JAR in ein Modul zu verwandeln, da es die Reads-Abhängigkeiten vollständig generiert und einem zumindest die Schreibarbeit bei den exports abnimmt.

    Fazit

    Wir haben die Grundlagen sowie verschiedene fortgeschrittene Features von Jigsaw vorgestellt und gezeigt, wie eine Migration einer Altanwendung aussehen kann. Zweifelsohne sind Module der richtige Schritt zur Komponentenorientierung. Jedoch gibt es einiges an Detailkritik [5]:

    1. Der Modifier public bedeutet ab Java9 nicht mehr, dass eine Klasse, Methode o. Ä. für jeden zugreifbar ist – ohne Exports eben nur noch innerhalb eines Moduls. Das wird von vielen als drastische Semantikänderung empfunden.
    2. Das gesamte Konzept ist sehr statisch: Die Reads-Abhängigkeiten basieren auf statischen Modulnamen. Wünschenswert wäre auch ein "Scope" für Reads-Beziehungen, wie man sie von Maven kennt (Scope "test" oder "runtime").
    3. Der Module-Deskriptor module-info muss zur Compile-Zeit vollständig vorhanden sein und wird durch den Compile-Schritt "festgezurrt". Man kann jedoch per Kommandozeilenoption weitere Reads und Exports an Java-Compiler bzw. -Launcher mitgeben und so den Module-Deskriptor erweitern.
    4. Module sind flach organisiert. Sie können weder hierarchisch angeordnet werden (wie man das beispielsweise von Maven-Parent-POMs kennt) noch sind sie gruppierbar. Abhängigkeiten kann man jedoch über die beschriebenen Aggregator-Module bündeln.
    5. Die module-info wird wie eine Klasse behandelt (konzeptionell fragwürdig) und in das .class-Binärformat kompiliert. Liegt eine Bibliothek als Modul nur kompiliert vor, so benötigt man separate Auslese-Tools wie jar oder jdeps, um den Modul-Deskriptor anzuzeigen. In Modular-JAR-Dateien ist die module-info außerdem teilweise redundant zur Manifest-Datei.
    6. Der Zugriffsschutz ist restriktiv: Was nicht exportiert ist, ist außerhalb nicht sichtbar ("Opt in"). Da es leider für Exports keine Package-Wildcards gibt, kann das zu viel Tipparbeit, Synchronisierungsproblemen bei Refactoring und langen, schwer lesbaren module-info-Dateien führen. Schön wäre, stattdessen oder alternativ ein "Opt out" anzubieten – also per Default alles zu exportieren, was nicht explizit eingeschränkt wird.
    7. Ein häufiger Kritikpunkt ist die fehlende Unterstützung für Modul-Versionen. Die Spezifikation nennt Versionierung ausdrücklich als Non-Requirement [10] – das wird als Aufgabe der Build-Werkzeuge angesehen. Eine Modul-Version kann nur als zusätzliches Meta-Attribut mitgegeben werden, das von der Java-Plattform nicht ausgewertet wird.
    8. Insbesondere ist es ohne weitere Jigsaw-Strukturen nicht möglich, mehrere Versionen eines Moduls gleichzeitig zu verwenden: Ein solcher Versuch scheitert schon am Split Package. Für diese Funktionalität benötigt man das Jigsaw-Konstrukt der sogenannten Laufzeit-Layer.

    Im September 2017 ist nun Java 9 erschienen. Leider wurden so einige Wünsche an das neue Modulsystem nicht oder noch nicht umgesetzt, was auf den Mailinglisten und in der Expert-Group auch zum Teil stark kritisiert wurde. Werden alte und neue Softwareprojekte umsteigen? Welche Erfahrungen werden wir mit der Umstellung echter, großer Anwendungen machen? Wie wirkt sich das Java-Modulsystem auf Architektur und Komponentenschnitt aus? Es bleibt spannend!

    Quellen
    1. JDK9 und Roadmap
    2. Project Jigsaw Homepage und Java Specification Request, JSR 376
    3. Liste der Java Enhancement Proposals (für Jigsaw: JEP 200, 201, 220, 260, 261, 282)
    4. Mailinglisten jigsaw-dev, jpms-spec-observers, jpms-spec-experts, jpms-spec-comments, jdk9-dev
    5. Diskussionen zu Jigsaw
    6. Download des JDK 9 with Project Jigsaw
    7. JDK Modularization
    8. Jens Schauder: "Packageabhängigkeiten managen mit Degraph", Artikel auf JAXenter, 26.08.2016
    9. Maven
    10. Jigsaw Specification
    11. Github: Jigsaw Example-Suite und DepVis-Tool zur Visualisierung von Modul-Graphen (auf Basis von GraphViz)

    Autoren

    Dr. Rüdiger Grammes

    Dr. Rüdiger Grammes ist seit November 2011 als Principal bei der Accso – Accelerated Solutions GmbH und dort als Softwarearchitekt in verschiedenen Projekten unterwegs.
    >> Weiterlesen

    Martin Lehmann

    Seit Ende der 90er Jahre wirkt Martin Lehmann als Softwareentwickler und -architekt in der Softwareentwicklung in diversen Projekten der Individualentwicklung für Kunden verschiedener Branchen.
    >> Weiterlesen

    Dr. Kristine Schaal

    Dr. Kristine Schaal arbeitet seit fast 20 Jahren in der Softwareentwicklung und ist in Projekten der Individualentwicklung für Kunden verschiedener Branchen unterwegs, technisch überwiegend im Java-Umfeld.
    >> Weiterlesen
    Das könnte Sie auch interessieren
    Kommentare (0)

    Neuen Kommentar schreiben