Durch gravierende Änderungen bei den Java-Versionen ab Java 9, hat sich vor allem die Entwicklung klassischer Java-Desktop-Anwendungen stark verändert. Die Problematik ist sehr vielschichtig. Doch mit Java-Version-14 gibt es jetzt endlich anwenderfreundliche Lösungen für die Auslieferung von Java-Desktop-Applikationen.
Im Jahr 2014 war die Java-Welt noch in Ordnung. Drei Jahre zuvor kam nach langer Zeit mit Java 7 endlich wieder eine neue Version. Dann wurde mit Java 8 eine Version mit vorher nie dagewesenen Neuerungen vorgestellt. Lambdas und Streams sollten die Art wie wir Java programmieren für immer verändern. Java 9 führte im Jahr 2017 ein Modulsystem mit dem Codenamen Jigsaw ein. Dieses Modulsystem war sehr umstritten und führte dazu, dass der Release verschoben werden musste. Dieses Modulsystem ist wohl der Grund dafür, warum viele Entwickler sich geweigert haben auf Version 9 umzusteigen. Java 9 war gleichzeitig auch der Startschuss für eine neue Release-Strategie von Oracle. Von nun an sollte jedes halbe Jahr eine neue Hauptversion erscheinen. Und mit jeder neuen Version wurden weitere starke Einschnitte in das Java-System vorgenommen, so dass sich in relativ kurzer Zeit sehr viele Änderungen ergaben, die mit der Abwärtskompatibilität gebrochen haben.
Auswirkung auf Server-Anwendungen
Bis Java 8 beinhaltete die Java-Laufzeitumgebung auch einige Komponenten, die für den Betrieb eines Servers notwendig sind. Zu den Einschnitten, die mit Java 9 begannen, gehört unter anderem diese Komponenten zu entfernen, weswegen vor allem Nutzer solcher Server mit einigen Schwierigkeiten zu rechnen haben, wenn sie ihre Java-Version aktualisieren wollen. Viele Anbieter von Server-Software haben sich lange Zeit geweigert, Versionen über Java 8 zu unterstützen. Je nachdem in welcher Umgebung der Anwender die Software ausführt, sind diese Komponenten vorhanden oder eben nicht. Das ist nicht das was mit Java ursprünglich geplant war, doch es ist ein Teil einer durchaus notwendig gewordenen Aufräumaktion. Da Oracle den Support für Java 8 nun nach und nach einstellt, werden sich alle Entwickler, die bisher noch mit dem Umstieg gewartet haben, früher oder später mit den neueren Java-Versionen auseinandersetzen müssen.
Auswirkung auf Desktop-Anwendungen
Mit JavaFX kam 2008 ein neues GUI-Framework, welches das inzwischen veraltete Swing ablösen sollte. Doch mit Java 11 wurde JavaFX aus der Java-Laufzeitumgebung entfernt, was für ein Desktop-System besonders unglücklich ist. Java basierte immer auf dem Konzept, dass in der Java-Laufzeitumgebung bereits alle plattformspezifischen Bibliotheken vorhanden sind, die eine Anwendung braucht. Da man sich aber jetzt nicht mehr darauf verlassen kann, dass JavaFX in der vom Anwender installierten Laufzeitumgebung bereits vorhanden ist, müssen Entwickler, die eine FX-Desktop-Anwendung ausliefern wollen, die JavaFX-Bibliotheken selbst mitliefern. Denn als sehr systemnahe Grafikbibliothek, was eine der größten Stärken von JavaFX gegenüber dem früheren Swing ist, ist JavaFX für jede Plattform anders. Das heißt, Entwickler einer Desktop-Anwendung müssen jetzt für jede Plattform eine eigene Version bauen, auf der die Anwendung laufen soll. Das aber widerspricht dem ursprünglichen Gedanken Write-once-run-everywhere, was heißt, dass ein Java-Programm auf allen von Java unterstützten Plattformen läuft, ohne dass dafür zusätzliche Anpassungen nötig sind. Wer jetzt jedoch eine solche plattformunabhängige JavaFX-Anwendung will, muss zu seiner Anwendung die JavaFX-Bibliotheken für alle Zielplattformen selbst mit dazu packen. Das Paket wird allerdings dadurch schnell mehrere hundert Megabyte groß, was keine optimale Lösung darstellt.
Eine mögliche Lösung
Mit Java 9 kam mit jlink eine Neuerung dazu, die damals noch kaum Beachtung gefunden hat, aber nun in Verbindung mit JavaFX sehr interessant ist. jlink ermöglicht nämlich, eine Java-Anwendung mit genau den Teilen der Lauzeitumgebung zusammenzupacken, welche diese Anwendung benötigt. Dadurch lassen sich Anwendung auch auf Systemen starten, auf denen keine Java-Laufzeitumgebung vorhanden ist, oder bei denen man nicht weiß, welche Java-Version installiert ist. So deployte Anwendungen startet zudem auch noch schneller, da nicht die gesamte Java-Laufzeitumgebung geladen werden muss, sondern nur die Teile, die tatsächlich benötigt werden. jlink kann jedoch nur dann richtig funktionieren, wenn die Anwendung vollständig das neue Modulsystem aus Version 9 einsetzt. Nur damit kann automatisch erkannt werden, welche Teile der Java-Laufzeitumgebung von der Anwendung benötigt werden.
Das Modulsystem
Durch das Modulsystem soll vor allem möglich werden, die Sichtbarkeiten der eigenen Bibliotheken besser zu kontrollieren. Ein Beispiel dafür sind Packages mit Klassen, die nur für die eigene Verwendung bestimmt sind. In dem neuen Modulsystem muss explizit angeben werden, welche Packages nach außen hin sichtbar sind und von anderen Anwendungen verwendet werden können. Weil klar definiert ist, welche Packages sich in welcher Bibliothek befinden, können Klassen zur Laufzeit schneller gefunden und geladen werden, wodurch sich die Geschwindigkeit der gesamten Anwendung erhöht. Um das Modulsystem im eigenen Projekt zu benutzen, muss im Root-Ordner des Quellcodes eine Datei mit dem Namen modul-info.java erzeugt werden. In einem Maven-Projekt wäre das: src/main/java. Dieser kann für ein einfaches JavaFX-Projekt zum Beispiel wie in (Listing 1) aussehen.
(Listing 1)
In Zeile 1 wird der Name dieses Moduls festgelegt. Zeile 2 besagt, dass dieses Modul ein Modul von JavaFX benötigt. In Zeile 3 wird festgelegt, welche Packages dieses Modul zur Verfügung stellt. Gerade in JavaFX-Projekten ist es wichtig das Package mit der Hauptklasse zu exportieren, da das JavaFX-System sonst nicht auf diese zugreifen kann und es dadurch bereits beim Starten zu einer Fehlermeldung kommen würde.
Das Problem verschiedener Versionen der gleichen Bibliothek in einer Anwendung, wird durch das Modulsystem nicht gelöst, was ein Teil der Kritik am Muldulsystem war. Mittlerweile ist es so, dass viele Bibliotheken, die man in der eigenen Anwendung verwendet, wiederum andere Bibliotheken benötigen. Es kann schnell passieren, dass über diese sogenannten transitiven Abhängigkeiten verschiedene Versionen der gleichen Bibliothek benötigt werden. Das Modulsystem wäre eine Möglichkeit gewesen dieses Problem zu lösen. Stattdessen gibt es einige Dinge bei der Verwendung zu beachten. Vieles, was vorher einfach so funktioniert hat, muss nun richtig konfiguriert werden.
Immerhin besitzt das Modulsystem einen Modus zur Abwärtskompatibilität. Somit können Bibliotheken verwendet werden, die das neue Modulsystem noch nicht nutzen. Auf diesen Kompatibilitätsmodus muss komplett verzichten werden, wenn man jlink einsetzen möchte. Gerade in größeren Projekten kommt es schnell vor, dass viele verschiedene Bibliotheken zum Einsatz kommen. Das schließt eine Verwendung von jlink praktisch aus, weil nur wenige Libraries das neue Modulsystem anwenden. Um hier Abhilfe zu schaffen, wurde in Java 9 das Tool jdeps integriert. Es analysiert die Abhängigkeiten des eigenen Programms und seiner Bibliotheken. So können die Informationen für das Modulsystem nachträglich generiert werden.
Multi-Version-Bibliotheken
Mit jdeps und jlink gibt es jedoch weitere Hürden, um eine Anwendung mit eigener Laufzeitumgebung ausliefern zu können. Mit Java 9 wurden sogenannte Multi-Version-Bibliotheken eingeführt. Es war schon immer so, dass sich der Umfang der Java-Laufzeitumgebung von Version zu Version leicht erweitert wurde. Multi-Version-Bibliotheken ermöglichen es, Teile des Programms auszutauschen, falls es auf einer anderen Umgebung ausgeführt wird. Das ist ein durchaus nützliches Feature, verhindert aber den Einsatz von jdeps mit einer niedrigeren Version als Java 12. Denn jdeps ist erst mit Java 12 in der Lage herkömmliche und Multi-Version-Bibliotheken miteinander zu vermischen. Wer also mit einer niedrigeren Java-Version als 12 Multi-Version-Bibliothek verwendet, kann jdeps nicht einsetzen.
Eine Multi-Version-Bibliothek zeichnet sich dadurch aus, dass im META-INF Ordner ein Unterordner mit dem Namen versions liegt. In diesem Ordner kann dann für jede Version, für die Teile der Bibliothek ersetzt werden, jeweils ein Ordner gelegt werden dessen Name einfach nur aus der Versionsnummer besteht. Für Änderungen, die für Java-Version 10 und später gültig werden, würde der Ordner also META-INF/versions/10 heißen. Dieser Ordner fungiert dann als ein weiteres Wurzelverzeichnis der Bibliothek. Wird zum Beispiel die Klasse treemap/TreeMap.class ersetzt, hat die neue Datei den vollständigen Namen META-INF/versions/10/treemap/TreeMap.class. In diesem Beispiel sollte die eigene Bibliothek mindestens mit Java 10 ausgeführt werden.
Installer für Java-Programme
Mit Java 8 war es möglich, eine JAR-Datei zu packen und diese dem Anwender zur Verfügung zu stellen. JAR-Dateien können bekanntlich per Doppelklick direkt ausgeführt werden. Die mit jlink erstellten Images sind dagegen komplexe Ordnerstrukturen, in denen der Benutzer erst die startbare Datei finden muss. Das ist zwar durch eine Batch-Datei oder etwas ähnliches lösbar, aber es ist schade, dass Java bis zu diesem Zeitpunkt keine benutzerfreundlichere Lösung dafür zur Verfügung gestellt hat. Erst mit Java 14 kam das Tool jpackage zum JDK hinzu. Dieses Tool ist ein Ersatz für das zuvor gestrichenen Tool JavaPackage, um eigenständig laufende Programme zu erzeugen. Das ist mit Java 14 endlich wieder möglich, auch wenn diese Pakete nun nicht mehr auf beliebigen Plattformen, sondern immer nur auf einer bestimmten Zielplattform laufen. Dafür kann jpackage aber einen Installer für die jeweilige Plattform erstellen, den der Anwender anklickt, um die Anwendung wie gewohnt zu installieren und anschließend zu starten. Benutzer bekommen damit jetzt die für die jeweilige Plattform vertraute und damit bestmögliche Customer-Experience.
Fazit:
Das eigene Projekt setzt am besten vollständig das Modulsystem ein. Sämtliche Bibliotheken müssen mit Hilfe von jdeps analysiert werden, um herauszufinden welche Teile der Java-Laufzeitumgebung benötigt werden. Mit jlink kann ein Image geschaffen werden, welches die nötigen Teile der Java-Laufzeitumgebung enthält. Diese Anwendung kann somit auch auf einem System ausgeführt werden, welches keine oder die falsche Java-Laufzeitumgebung hat. Für eine Standard-Installationsprozedur wird das Image anschließend mit jpackage verpackt. Zumindest für Desktop-Entwickler ist spätestens jetzt die Zeit gekommen den Schritt von Java 8 vorwärts zu gehen, denn mit Java 14 gibt es nun endlich eine Möglichkeit den Benutzern wieder die gleiche oder sogar eine bessere Experience zu bieten als früher.
Der gesamte Prozess ist jedoch aufwändig. Denn gerade, wenn man ein größeres Projekt hat, wird man wahrscheinlich ein Build-System wie Maven verwenden, welches sich um die ganzen Bibliotheken kümmert. Dort werden Bibliotheken schnell in einer unübersichtlichen Anzahl vorhanden sein. Da Maven ohnehin sämtliche Informationen darüber hat welche Bibliotheken benötigt werden, wäre ein Maven-Plugin hilfreich, das den gesamten Prozess abbildet und ausführt. Der Autor hat dazu auf GitHub ein entsprechendes Open-Source-Projekt gestartet.
Sven Reinck programmiert seit seinem sechsten Lebensjahr. Da war es nicht verwunderlich, dass er später Informatik studiert hat. Im Studium lernte er die Sprache Java, welche ab diesem Zeitpunkt seine bevorzugte Programmiersprache war. 2017 entdeckte er dann den Nachfolger Kotlin, den er seitdem immer mehr schätzen gelernt hat. Jetzt führt er mit seinem Coaching nicht nur Schulungen zu Kotlin durch, sondern bietet auch eine längerfristige Begleitung, damit die Einführung von Kotlin in Firmen reibungslos funktioniert.