dark

Java 9 Migration

#JAVAPRO #JCON2017 #CoreJava #Java9

Java 9 enthält mehr grundlegende Änderungen als jedes bisherige Release. Die enorme Leistung, die Java- Plattform zu modularisieren, hat zu vielen internen Änderungen geführt. Diese betreffen nahezu jede bestehende Anwendung. In diesem Artikel lernen Sie, wie Sie Ihre eigenen Anwendungen auf Java-9-Kompatibilität prüfen können, um sie erfolgreich zu portieren.

Da es bei dieser Portierung eine Reihe unterschiedlicher Probleme geben kann, gehen wir dabei Schritt für Schritt vor, um so jeweils immer nur eine bestimmte Klasse von Problemen zu lösen. Damit die Migration auch sofort in der Praxis getestet werden kann, benutzen wir ein kleines Beispielprojekt, das auf Github veröffentlicht ist.

Die Beispielanwendung

Die Beispielanwendung ist ein Maven-Projekt mit mehreren Sub-Modulen. (Abb. 1) zeigt die Abhängigkeiten unter den einzelnen Artefakten. Die gepunktete Linie vom hello Artefakt zu swinggreeter repräsentiert eine indirekte Service-Abhängigkeit. Die Main-Klasse steckt im Modul hello. Wie das Projekt im Detail funktioniert ist zunächst für die Portierung nicht wichtig. Das sehen wir uns nur dann genauer an, wenn ein Problem bei der Migration auftritt.

Das modulare JDK9. (Abb. 1)
Der Aufbau der Beispielanwendung. (Abb. 1)

 

1. Schritt: Einfach mit Java 9 testen

Wird das Projekt unter Java 8 kompiliert und gestartet, so wird ein kleiner Dialog angezeigt (Abb. 2). Ein Programm, das mit Java 8 entwickelt wurde, und keine internen Java-APIs verwendet, sollte – laut Aussage der Entwickler – auch unter Java 9 eigentlich ganz normal starten.

Die Beispielanwendung zeigt einen einfachen Swing-Dialog (Abb. 2)
Die Beispielanwendung zeigt einen einfachen Swing-Dialog (Abb. 2)

Zunächst müssen die Umgebungsvariablen gesetzt werden, damit alles von der Kommandozeile aus funktionieren kann.

Gezeigt wird hier die Vorgehensweise unter OS X, für andere Unix-/Linux-System müssen lediglich die Pfade entsprechend angepasst werden. Für Windows wird die Umgebungsvariable mit set statt mit export gesetzt:

> export JAVA9_HOME=“/Library/Java/JavaVirtualMachines/jdk-9.jdk/Contents/Home“
> export JAVA_HOME=“/Library/Java/JavaVirtualMachines/jdk1.8.0.jdk/Contents/Home“
> cd legacyproject
> mvn clean install

Das Programm wird von der Kommandozeile aus gestartet – mit allen JARs auf dem Classpath. Später modularisieren wir die Anwendung und packen die Module auf den Modulepath. Der Maven-Build kopiert alle Artefakte in das Verzeichnis libs. Damit ergibt sich folgender Aufruf:

> $JAVA9_HOME/bin/java -classpath „./libs/
*“ de.eppleton.hello.HelloWorld
Exception in thread „main“ java.lang.NoSuchMethodError: javax.
swing.JButton.getPeer()Ljava/awt/peer/ComponentPeer;
at de.eppleton.swinggreeter.SwingGreeterService.greet
(SwingGreeterService.java:12)
at de.eppleton.greetings.Greeter.greet(Greeter.java:11)
at de.eppleton.hello.HelloWorld.main(HelloWorld.java:11)

Offenbar kann die Anwendung mit Java 9 auf diese Art nicht gestartet werden. Der problematische Aufruf ist im swinggreeter:

if (jButton.getPeer() != null) {
System.out.println(„hallo“);
}

Die Methode getPeer wurde im Rahmen der Modularisierung entfernt. Der Name des Package java.awt.peer legt zwar nahe, dass Peer Teil der Java-SE-API war, dem ist aber nicht so. In Java 9 wurde das Package verkapselt und die Methode getPeer aus Component entfernt. Der Nulltest kann durch den Aufruf der
Methode isDisplayable ersetzen, welcher zum selben Ergebnis führt. Anschließend kompilieren wir die Anwendung noch einmal mit Java 8 und starten sie neu.

if (jButton.isDisplayable()) {
System.out.println(„hallo“);
}

Wir haben damit die erste Klasse von Problemen kennengelernt, nämlich die Änderung der Java-API als Resultat der Verkapselung interner APIs. Eine ganze Reihe von internen APIs stehen dadurch nicht mehr zur Verfügung. Im Zuge der Entwicklung von Java 9 wurde jedoch analysiert, welche internen APIs tatsächlich in vielen Projekten eingesetzt werden, und es wurden entsprechend offizielle APIs als Ersatz geschaffen.

2. Kompilieren mit Java 9

Nachdem wir die Bytecode-Kompatibilität erreicht haben, können wir nun im zweiten Schritt testen, ob wir unser Projekt unter Java 9 weiterentwickeln können. Dazu kompilieren wir es ab jetzt unter Java 9. Dazu ändern wir source und target Version im pom.xml des Parent-Projekts:

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>9</maven.compiler.source>
<maven.compiler.target>9</maven.compiler.target>
</properties>

Dann müssen wir noch die JAVA_HOME Umgebungsvariable auf die JDK-9-Installation verweisen lassen und können danach den Build starten:

> export JAVA_HOME=“/Library/Java/JavaVirtualMachines/jdk-9.jdk/Contents/Home“
> mvn clean install

Wir erhalten folgende aussagekräftige Fehlermeldung:

[ERROR] Failed to execute goal org.apache.maven.plugins:
maven-compiler-plugin:3.1:compile (default-compile) on project
hello: Compilation failure
[ERROR] /Users/antonepple/Entwicklung/Eppleton/trainings/
Java9Migration/projects/legacyproject_porting/hello/src/main/
java/de/eppleton/hello/HelloWorld.java:[8,9] as of release 9, ‚_‘
is a keyword, and may not be used as an identifier

Der Unterstrich ist von Java 9 an als Identifier verboten, da er eventuell einmal als Keyword verwendet wird. Wir können den Fehler ganz leicht beheben, indem wir dem betroffenen Feld einen anderen Namen geben.

// int _ = 1;
int idx = 1;

Danach lässt sich die Anwendung mit Java 9 kompilieren und startet ganz normal. Die zweite Klasse von Problemen „neue Keywords“ dürfte allerdings deutlich weniger Änderungen erfordern als die Einführung von assert und enum als Keywords in Java 5.

3. Schrittweise Modularisierung mit dem Unnamed-Module

Unsere Anwendung läuft nun unter Java 9. Das ist schon mal ermutigend. Wir nutzen jedoch den ganz normalen Classpath und profitieren nicht von den Neuerungen des Modulsystems. Wir können zum Beispiel so keine angepasste Runtime bauen, da das Modulsystem keine Informationen über die Abhängigkeiten unserer JARs hat.

Alle Klassen, die vom Classpath geladen werden, betrachtet die Java-Virtual-Machine (JVM) in Java 9 als Teil eines Unnamed-Module. Dieses Modul darf Klassen aus allen „echten“ Modulen lesen, die als Named-Modules bezeichnet werden. Ein Named-Module darf hingegen keine Abhängigkeit auf dieses Unnamed-Module setzen.

(Abb. 3) zeigt das Verhältnis zwischen Unnamed-Module und Named-Module. Die Module auf dem Classpath (jar1 bis jar6) gehören zum Unnamed-Module und dürfen aus allen Named-Modules lesen. Das sind die Anwendungsmodule (de.eppleton.*) und die Java-APIs. Umgekehrt darf keines dieser Named-Module auf das Unnamed-Module, und damit den Classpath, zugreifen.

Das Unnamed-Module kann aus allen Modulen lesen. (Abb. 3)
Das Unnamed-Module kann aus allen Modulen lesen. (Abb. 3)

Die Modularisierung klappt am einfachsten, wenn wir dabei Schritt für Schritt vorgehen. Man spricht hier von einem Bottom-up Vorgehen. Am Anfang müssen wir ein Modul für die Modularisierung auswählen. Dabei hilft das Tool jdeps, das uns für alle JARs die Abhängigkeiten anzeigen kann:

> jdeps libs/*
[…]
hello-1.0-SNAPSHOT.jar -> libs/datamodel-1.0-SNAPSHOT.jar
hello-1.0-SNAPSHOT.jar -> libs/greetingcomposer-1.0-SNAPSHOT.jar
hello-1.0-SNAPSHOT.jar -> libs/greetings-1.0-SNAPSHOT.jar
hello-1.0-SNAPSHOT.jar -> java.base
de.eppleton.hello -> de.eppleton.datamodel
datamodel-1.0-SNAPSHOT.jar
de.eppleton.hello -> de.eppleton.greetingcomposer
greetingcomposer-1.0-SNAPSHOT.jar
de.eppleton.hello -> de.eppleton.greetings greetings-1.0-SNAPSHOT.jar
de.eppleton.hello -> java.lang
java.base
[…]

Hier ist beispielhaft die Ausgabe für das hello-1.0-SNAPSHOT.jar dargestellt. Es hat Abhängigkeiten auf weitere JARs, die noch nicht modularisiert sind. Das macht es zu einem schlechten Kandidaten für die Modularisierung. Denn als Named-Module darf es nicht mehr auf Klassen des Unnamed-Module zugreifen, in welchem die anderen Klassen liegen. Wir sollten uns also ein Modul aussuchen, das keine Abhängigkeiten aufs Unnamed-Module hat. Da gibt es momentan nur das datamodel.

Bei einem Maven-Projekt kann man das passende Modul auch ganz einfach über den Abhängigkeitsgraphen identifizieren. Viele IDEs können anhand des POM (Project-Object-Model) die Abhängigkeiten visualisieren. (Abb. 4) zeigt den Graphen für unser Beispiel in der NetBeans IDE. Module, die keine ausgehenden Kanten zum Unnamed-Module haben, sind ideale Kandidaten. Sobald man mit dieser untersten Schicht fertig ist, kümmert man sich dann um die Module in der darüberliegenden Schicht.

Der Maven-Abhängigkeitsgraph (Netbeans) ist ideal um Module für den Bottom-Up Ansatz zu identifizieren. (Abb. 4)
Der Maven-Abhängigkeitsgraph (Netbeans) ist ideal um Module für den Bottom-Up
Ansatz zu identifizieren. (Abb. 4)

4. Umwandlung in ein Modul

Um ein JAR in ein Modul zu verwandeln müssen wir lediglich eine besondere module-info Klasse im Wurzelverzeichnis des Quellcodes anlegen und die entsprechenden Informationen eintragen. Legen wir also in unserem datamodel Projekt die Datei ./src/main/java/module-info.java an:

module de.eppleton.datamodel{
exports de.eppleton.datamodel;
}

Der Name des Moduls wird per Konvention wie bei Packages nach dem Reverse-Domain-Muster gewählt, üblicherweise entspricht er dem Namen des Wurzel-Package. Da andere Module auf die Typen unseres Moduls zugreifen sollen, exportieren wir das Package de.eppleton.datamodel. Das fertige JAR-File kopieren wir nun nicht mehr ins libs Verzeichnis, sondern nach modules. Dazu ändern wir im pom.xml das Ausgabeverzeichnis der Copy-Task:

<outputDirectory>../modules</outputDirectory>

Das Argument -p ist das Kürzel für den Modulepath. Mit –add-module fügen wir ein Wurzel-Modul hinzu; alle Modul-Abhängigkeiten dieses Wurzel-Moduls findet Java dann automatisch. Als nächstes Modul wählen wir nach dem Maven-Abhängigkeitsgraphen entweder greetings oder greetingcomposer. Nehmen wir uns zunächst das Projekt greetingcomposer vor. Wir erzeugen erneut eine module-info.java und setzen mit requires eine Abhängigkeit auf unser erstes Modul. Mit exports legen wir noch fest, welche Packages von anderen Modulen gelesen werden dürfen:

module de.eppleton.greetingcomposer {
exports de.eppleton.greetingcomposer;
requires de.eppleton.datamodel;
}

Nach dem selben Modell machen wir weiter und erzeugen die module-info für greetings:

module de.eppleton.greetings {
requires de.eppleton.datamodel;
exports de.eppleton.greetings;
exports de.eppleton.greetings.spi;
}

Danach kümmern wir uns um swinggreeter:

module de.eppleton.swinggreeter {
requires de.eppleton.greetings;
requires de.eppleton.datamodel;
}

Wenn wir nun den Build starten, gibt uns der Compiler eine hilfreiche Fehlermeldung:

-------------------------------------------------------------
COMPILATION ERROR :
------------------------------------------------------------
de/eppleton/swinggreeter/SwingGreeterService.java:[4,13] package
javax.swing is not visible
(package javax.swing is declared in module java.desktop, but
module de.eppleton.swinggreeter does not read it)
de/eppleton/swinggreeter/SwingGreeterService.java:[6,13] package
javax.swing is not visible
(package javax.swing is declared in module java.desktop, but
module de.eppleton.swinggreeter does not read it)
2 errors

Offenbar wird in unserem Projekt auf das Package javax.swing zugegriffen, das im Modul java.desktop liegt. java.desktop ist zwar Teil von Java SE, aber wir müssen trotzdem unsere Abhängigkeit darauf erklären. Dadurch können wir später beim Bau einer angepassten Runtime ausschließlich die Module dazu packen, die wirklich benötigt werden. Nur für die Klassen des Moduls java.base ist die Abhängigkeit implizit. Das ist immer mit dabei. Wenn wir uns den Output des jdeps Tools genauer ansehen, sehen wir, dass diese Abhängigkeit dort bereits aufgeführt wird:

swinggreeter-1.0-SNAPSHOT.jar -> java.base
swinggreeter-1.0-SNAPSHOT.jar -> java.desktop

Am einfachsten ist es daher jdeps zu verwenden, um gleich von Anfang an alle Abhängigkeiten zu identifizieren. Wenn wir requires java.desktop; ergänzen, lässt sich das Projekt wieder kompilieren. Als letztes Modul passen wir hello an und setzen die Abhängigkeiten, die wir diesmal mit jdeps ermittelt haben:

module de.eppleton.hello {
requires de.eppleton.greetings;
requires de.eppleton.datamodel;
requires de.eppleton.greetingcomposer;
}

5. Service-Abhängigkeiten auflösen

Unsere Beispielanwendung nutzt den ServiceLoader Ansatz, den es seit Java 6 gibt, um die Implementierung eines Greeter Service zu finden:

public class Greeter {
public static void greet(Message message) {
ServiceLoader<GreeterService> load = ServiceLoader.load(GreeterService.class);
if(load.iterator().hasNext()){
load.iterator().next().greet(message);
}
}
}

Die passende Implementierung wird dabei über eine Datei im META-INF/services Verzeichnis registriert. In unserem Fall ist das der SwingGreeterService und die Registrierung steckt in der Datei META-INF/services/de.eppleton.greetings.spi.GreeterService.

Im Java 9 Modulsystem wird der ServiceLoader Ansatz ebenfalls genutzt, die Registrierung erfolgt aber über die module-info. Sowohl der Konsument des Service (de.eppleton.greetings), als auch der Anbieter (de.eppleton.swinggreeter) müssen diese Serviceabhängigkeit bekannt geben. Der Konsument
verwendet dazu das Schlüsselwort uses:

module de.eppleton.greetings {
exports de.eppleton.greetings;
exports de.eppleton.greetings.spi;
uses de.eppleton.greetings.spi.GreeterService;
}

Der Anbieter verwendet das Schlüsselwort-Paar provides with. Mit provides wird der implementierte Service identifiziert, nach with folgt die konkrete Implementierung:

module de.eppleton.swinggreeter {
requires de.eppleton.greetings;
requires java.desktop;
provides de.eppleton.greetings.spi.GreeterService
with de.eppleton.swinggreeter.SwingGreeterService;
}

Damit haben wir unser Projekt komplett modularisiert. Wir benötigen nun den Classpath überhaupt nicht mehr und können die Anwendung starten:

java --module-path modules
--add-modules de.eppleton.hello,de.eppleton.swinggreeter
-m de.eppleton.hello/de.eppleton.hello.HelloWorld

Wie bereits erwähnt findet die JVM alle benötigten Module anhand der Abhängigkeiten von Wurzel-Modulen, die mit –add-modules angegeben werden. Da zwischen dem hello Modul und dem swinggreeter nur eine Service-Abhängigkeit und keine direkte Abhängigkeit besteht, ist der swinggreeter nicht im Abhängigkeitsgraphen vom hello Modul. Deshalb muss der swinggreeter ebenfalls als Wurzel-Modul auf dem Modulpfad angegeben werden. Ansonsten würde der Service nicht geladen werden. Damit haben unsere kleine Beispielanwendung erfolgreich nach Java 9 migriert.

6. Automatic-Modules

In unserer Beispielanwendung nutzen wir keine Third-Party-Bibliotheken. Wenn Sie aber in Ihrer Anwendung solche Abhängigkeiten haben, sind diese häufig noch nicht als Java 9 Module verfügbar. Aber auch dafür gibt es eine Lösung. Sie können das zugehörige JAR einfach auf den Modul-Pfad legen, die JVM behandelt es dann als Automatic-Module. Der Name des Moduls ergibt sich aus dem Namen des JAR-Archivs. Danach können Sie in den entsprechenden Modulen eine Abhängigkeit auf dieses Modul anhand des Namens setzen. Automatic-Modules wurden genau für diesen Zweck entworfen.

Probleme aus der Praxis

In unserem Beispielprojekt sieht das alles relativ einfach aus. Aber das ist natürlich ein konstruierter Fall. In der Praxis treten durchaus auch andere Probleme auf. In Spring-Boot gab es zum Beispiel Schwierigkeiten mit ClassCastExceptions, die durch das Casten eines ClassLoaders zum URLClassLoader verursacht wurden. Der URLClassLoader ist zwar nach wie vor Teil der öffentlichen API, wird jedoch an vielen Stellen durch den AppClassLoader ersetzt. Ein Problem, das leider erst zur Laufzeit auftritt. Der Code ist also gültig, aber die Annahmen über die Implementierung innerhalb der Java-Core-Libraries sind es nicht mehr.

Wie bei einem Framework wie bei Spring-Boot zu erwarten gibt es auch einige Probleme mit der Benutzung interner APIs. So griff bislang die Klasse org.springframework.boot.configuration processor.fieldvalues.javac.Trees auf die interne Klasse com.sun.tools.javac.api.JavacTrees zu, um Default-Werte für Felder in einem AnnotationProcessor auszulesen. Diese Klasse ist in Java 9 verkapselt und daher nicht zugänglich.

Das interessante an diesem spezifischen Problem ist, dass Spring-Boot die verbotene Klasse gar nicht direkt referenziert, sondern über Reflection die Instanz eines exportierten Interfaces (com.sun.source.util.Trees) sucht. Danach werden die Methoden der Klasse per Reflection mit Hilfe der Klasse ReflectionWrapper ausgelesen und aufgerufen. Erst dieser Aufruf löste das Problem aus, denn dadurch werden Instanzmethoden der verkapselten Klasse JavacTree aufgerufen. Das Problem wurde schließlich dadurch behoben, dass die Instanz vor dem Zugriff in das erlaubte Interface gecastet wird:

final class Trees extends ReflectionWrapper {

private Trees(Object instance) {
super(Class.forName(„com.sun.source.util.Trees“),instance);
}
//…
ReflectionWrapper(Class type, Object instance) {
this.type = type;
this.instance = type.cast(instance);
}

Dies sind nur zwei Beispiele für Probleme, die auftreten können, selbst wenn man die Regeln für die Migration so gut wie möglich befolgt. Die Praxis ist doch immer ein wenig komplexer als erwartet.

Fazit

Wir haben unser kleines Testprojekt erfolgreich nach Java 9 portiert. Natürlich dreht man in einem echten Projekt ein paar zusätzliche Runden, aber prinzipiell ist das Vorgehen dasselbe. Mit Unnamed-Module und Automatic-Modules ist es möglich dabei Schritt für Schritt vorzugehen. In den meisten Fällen ist es für den Endanwender gar nicht so schwer nach Java 9 zu migrieren. Wer sich bisher an die Vorgaben gehalten hat, nur offizielle APIs zu verwenden, ist von einigen Überraschungen abgesehen relativ sicher.
Am meisten Aufwand haben Entwickler, die bislang viel mit internen APIs gearbeitet haben. Hier ist nicht immer ein eindeutiger Migrationspfad vorgegeben. Das ist vor allem bei komplexen Anwendungen und Application-Frameworks ein Problem. Man sollte sich also sicherheitshalber auf einige Überraschungen einstellen und sich ein wenig mehr Zeit für die Migration reservieren als bei früheren Releases. Dann klappt auch die Migration auf Java 9.

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

Total
0
Shares
Previous Post

Wer hat Angst vor Java 9?

Next Post

JAVAPRO startet Bildungsoffensive200 Nachwuchstickets für die JCON 2017

Related Posts