Viele Programmbibliotheken und Frameworks in Java unterstützen mehrere Java Versionen. Das führt dazu, dass neue Sprachfeatures und neue Funktionen der Plattform-API in diesen Projekten nur zögerlich umgesetzt werden, um die Abwärtskompatibilität mit alten Java Versionen nicht zu untergraben. Insbesondere im Hinblick auf die größeren Änderungen an der Plattform-API im Zuge von Java 9 ergibt sich das Problem, dass viele Programmbibliotheken entweder das Modulsystem von Java 9 umgesetzt haben und damit voll zu Java 9 konform sind, oder nicht. Als Resultat werden dann mehrere JAR-Artefakte für die unterschiedlichen Java Versionen veröffentlicht. An dieser Stelle schafft das Multi-Release JAR-File in Java 9 (JEP 238) Abhilfe.
Seit Java 9 ist es nun möglich, unterschiedliche Versionen derselben Java-Klasse für unterschiedliche Java Versionen in der gleichen Jar vorzuhalten. Bei der Entwicklung einer Programmbibliothek können also für die Java 9 Nutzer der Bibliothek spezielle Funktionen vorbehalten werden und spezielle Sprachfeatures verwendet werden, ohne dass die Java 8 Nutzer davon etwas mitbekommen oder die Klassen auch nur sehen. Beim Ausführen des Programms landen dann ausschließlich die Klassen im Klassenpfad, die für die verwendete JVM-Version vorgesehen sind. Diese Funktionalität funktioniert auch für Resource-Dateien.
Um ein Multi-Release JAR als solches zu kennzeichnen, muss in der MANIFEST.MF-Datei innerhalb der JAR-Datei zunächst das Attribut Multi-Release: true
gesetzt werden. Dies ist ein Hinweis an die JVM, die die JAR ausführen soll und wird von JVMs mit einer Java Version von 8 oder niedriger einfach ignoriert.
Ein Multi-Release JAR erstellen
Zunächst erstellen wir drei Ordner für die drei Java Versionen (8, 9, 10), die unser kleines Testprogramm unterstützen soll. Diese Ordner nennen wir src8
, src9
und src10
.
Unser Testprogramm besteht aus den 2 Klassen Main.java
und Example.java
:
public class Main { public static void main(String[] args) { new Example().doStuff(); } }
public class Example { public void doStuff() { System.out.prinln("Beispiel für Java 8"); } }
Die Main
-Klasse erzeugt dabei eine Instanz der Example
-Klasse und ruft die Methode doStuff
auf. Diese gibt anschließend den Text “Java 8 Example” auf der Konsole aus. Um die Snippets und insbesondere die später folgenden Konsolenbefehle kurz zu halten, werden diese beiden Klassen im Default-Package belassen. Die Main
-Klasse soll für alle Versionen identisch sein, nur die Example
-Klasse soll für die Java Versionen 8, 9 und 10 unterschiedliche Implementierungen erhalten.
Die beiden oben erstellten Java-Dateien legen wir nun unter src8/
ab.
Nun zur Version der Example.java
für Java 9:
public class Example { public void doStuff() { System.out.prinln("Beispiel für Java 9"); } }
Diese sieht sehr ähnlich wie die für Java 8 aus, nur wird hier “Java 9 Example” statt “Java 8 Example” ausgegeben. Diese Datei legen wir unter src9/Example.java
ab.
Schließlich die Example.java
für Java 10:
public class Example { public void doStuff() { System.out.prinln("Beispiel für Java 10"); } }
Abgelegt wird diese unter src10/Example.java
.
Unsere Dateistruktur sollte nun wie folgt aussehen:
./
├── src8/
│ . ├── Main.java
| . └── Example.java
├── src9/
| . └── Example.java
└── src10/
. . └── Example.java
Diese Java-Dateien müssen nun alle für ihre jeweilige Version kompiliert werden. Dafür gibt es seit Java 9 das Compiler-Flag --release
, welches auch gleich die korrekte Plattform-API verwendet.
Hier die Kommandozeilenbefehle, um alle Dateien mit dem Java 10 Compiler korrekt zu kompilieren:
$ javac --release 8 src8/*.java $ javac --release 9 src9/*.java $ javac --release 10 src10/*.java
Im nächsten Schritt werden diese nun zu einer gemeinsamen JAR-Dateien zusammengefügt. Dies geschieht mit folgendem Befehl:
$ jar cfe multi.jar Main -C src8 Main.class -C src8 Example.class --release 9 -C src9 Example.class --release 10 -C src10 Example.class
Ein sehr langer Befehl. Zunächst werden die Dateien Main.class
und Example.class
aus dem src8
-Ordner als Default-Klassen in die Datei multi.jar
verpackt. Anschließend wird mittels der Flag --release 9
die Datei Example.class
aus dem Ordner src9
für Java Version 9 vorgemerkt. Analog dazu wird mittels --release 10
die Example.class
-Datei aus dem src10
-Ordner für Java Version 10 vorgemerkt.
Anschließend kann die Datei multi.jar
mit dem Befehl java -jar multi.jar
gestartet werden. Hier das Resultat für die Java 10 JVM:

Multi Release Jar ausführen unter Java Version 10
Java 9 JVM:

Multi Release Jar ausführen unter Java Version 9
Java 8 JVM:

Multi Release Jar ausführen unter Java Version 8
Multi-Release JAR im Detail
Das Ganze scheint also irgendwie zu funktionieren. Aber wie genau funktioniert es? Dazu schauen wir uns mal den Inhalt der JAR-Datei an. Diese lässt sich mit einem beliebigen Zip-Tool öffnen. Heraus kommt die folgende Dateistruktur:
multi.jar/
├── META-INF/
│ . ├── versions
│ . | . ├── 10
│ . | . | . └── Example.class
| . | . └── 9
│ . | . . . └── Example.class
| . └── MANIFEST.MF
├── Main.class
└── Example.class
Die Dateien Main.class
und Example.class
im obersten Ordner enthalten hierbei den Java-8-Bytecode, der mit dem ersten der drei javac
-Aufrufe generiert wurde. Die Versionen der Datei Example.class
für Java 9 und Java 10 sind respektive unter META-INF/versions/9/Example.class
, beziehungsweise META-INF/version/10/Example.class
zu finden. Genau diese Class-Dateien werden dann auch von einer Java 9 oder 10 JVM statt der Example.class
im obersten Ordner verwendet. Die Java 8 JVM ignoriert einfach den versions
-Ordner unter META-INF/
und verwendet die Example.class
aus dem obersten Ordner. Somit kommt es zu keiner Inkompatibilität der hier erstellten JAR-Dateien mit alten JVMs.
Multi-Release JAR im Quellcode
Diese Multi-Release JARs sind auch im Quellcode etwas anders zu handhaben, als normale JAR-Dateien. Wenn beispielsweise eine Klasse oder eine Ressource zur Laufzeit aus einer JAR-Datei geladen werden soll, muss nun auf die korrekte Version verwiesen werden. Statt
jar:file:/multi.jar!/Example.class
wird als Resourcenpfad im UrlClassLoader
nun eben
jar:file:/multi.jar!/META-INF/versions/10/Example.class
angegeben, wenn die Java 10 Version verwendet werden soll.
Zusammenfassung
Mit dem neuen Multi-Release JAR lassen sich mit geringem Aufwand gleich mehrere Java- und Java-Plattform-API-Versionen unterstützen, ohne dass diese interferieren würden. Dadurch können neue Sprachfeatures oder verbesserte Funktionen der Plattform-API an einigen Stellen im Code direkt verwendet werden, ohne den gesamten Code des Projektes auf die neue Java-Version migrieren zu müssen.

