Multi-Release JAR-File

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

Multi Release Jar ausführen unter Java Version 10

Java 9 JVM:

Multi Release Jar ausführen unter Java Version 9

Multi Release Jar ausführen unter Java Version 9

Java 8 JVM:

Multi Release Jar ausführen unter Java Version 8

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.

Short URL for this post: https://wp.me/p4nxik-352
This entry was posted in Java Basics and tagged , , . Bookmark the permalink.

Leave a Reply