Multi-Release-JARs - schlecht oder gut?


Von einem Übersetzer: Wir arbeiten aktiv an der Übertragung der Plattform auf Java 11-Rails und überlegen, wie Java-Bibliotheken (wie YARG ) unter Berücksichtigung der Funktionen von Java 8/11 effizient entwickelt werden können, damit Sie keine separaten Zweige und Releases erstellen müssen. Eine mögliche Lösung ist eine Multi-Release-JAR, aber nicht alles ist reibungslos.


Java 9 enthält eine neue Java-Laufzeitoption namens Multi-Release-JARs. Dies ist möglicherweise eine der umstrittensten Innovationen auf der Plattform. TL; DR: Wir finden, dass dies eine krumme Lösung für ein ernstes Problem ist . In diesem Beitrag erklären wir, warum wir so denken, und erklären Ihnen auch, wie Sie eine solche JAR erstellen, wenn Sie dies wirklich möchten.


Multi-Release-JARs oder MR-JARs sind eine neue Funktion der in JDK 9 eingeführten Java-Plattform. Im Folgenden werden die mit der Verwendung dieser Technologie verbundenen erheblichen Risiken und das Erstellen von Multi-Release-JARs mit Gradle ausführlich erläutert. wenn du noch willst


Tatsächlich ist eine JAR mit mehreren Versionen ein Java-Archiv, das mehrere Varianten derselben Klasse für die Arbeit mit verschiedenen Versionen der Laufzeit enthält. Wenn Sie beispielsweise in JDK 8 arbeiten, verwendet die Java-Umgebung die Klassenversion für JDK 8, und wenn in Java 9 die Version für Java 9 verwendet wird. Wenn die Version für eine zukünftige Version von Java 10 erstellt wurde, verwendet die Laufzeit diese Version anstelle der Version für Java 9 oder die Standardversion (Java 8).


Unter der Katze verstehen wir das Gerät des neuen JAR-Formats und finden heraus, ob dies alles ist.


Wann werden JARs mit mehreren Versionen verwendet?


  • Optimierte Laufzeit. Dies ist eine Lösung für das Problem, mit dem viele Entwickler konfrontiert sind: Bei der Entwicklung einer Anwendung ist nicht bekannt, in welcher Umgebung sie ausgeführt wird. Für einige Versionen der Laufzeit können Sie jedoch generische Versionen derselben Klasse einbetten. Angenommen, Sie möchten die Versionsnummer von Java anzeigen, in der die Anwendung ausgeführt wird. Für Java 9 können Sie die Runtime.getVersion-Methode verwenden. Dies ist jedoch eine neue Methode, die nur in Java 9+ verfügbar ist. Wenn Sie andere Laufzeiten benötigen, z. B. Java 8, müssen Sie die Eigenschaft java.version analysieren. Als Ergebnis haben Sie 2 verschiedene Implementierungen einer Funktion.
  • Widersprüchliche APIs: Die Konfliktlösung zwischen APIs ist ebenfalls ein häufiges Problem. Beispielsweise müssen Sie zwei Laufzeiten unterstützen, aber in einer davon ist die API veraltet. Es gibt zwei gängige Lösungen für dieses Problem:


    1. Das erste ist Reflexion. Sie können beispielsweise die VersionProvider-Schnittstelle angeben, dann 2 bestimmte Klassen Java8VersionProvider und Java9VersionProvider und die entsprechende Klasse in die Laufzeit laden (es ist lustig, dass Sie die Versionsnummer analysieren müssen, um zwischen zwei Klassen zu wählen!). Eine der Optionen für diese Lösung besteht darin, eine einzelne Klasse mit verschiedenen Methoden zu erstellen, die mithilfe der Reflexion aufgerufen werden.
    2. Eine fortgeschrittenere Lösung besteht darin, wenn möglich Methodenhandles zu verwenden. Höchstwahrscheinlich wird Ihnen die Reflexion hemmend und unangenehm erscheinen und im Allgemeinen so, wie sie ist.


Bekannte Alternativen zu Multi-Release-JARs


Der zweite Weg, einfacher und verständlicher, besteht darin, zwei verschiedene Archive für verschiedene Laufzeiten zu erstellen. Theoretisch erstellen Sie zwei Implementierungen derselben Klasse in der IDE, und das Kompilieren, Testen und korrekte Packen in zwei verschiedene Artefakte ist Aufgabe des Build-Systems. Dies ist ein Ansatz, der seit vielen Jahren in Guave oder Spock verwendet wird. Es wird aber auch für Sprachen wie Scala benötigt. Und das alles, weil es so viele Optionen für den Compiler und die Laufzeit gibt, dass die Binärkompatibilität fast unerreichbar wird.


Es gibt jedoch noch viele andere Gründe, separate JAR-Archive zu verwenden:


  • JAR ist nur eine Art zu packen.

Es ist ein Assembly-Artefakt, das Klassen enthält, aber das ist noch nicht alles: Ressourcen sind in der Regel auch im Archiv enthalten. Verpackung hat wie Ressourcenhandhabung einen Preis. Das Gradle-Team möchte die Build-Qualität verbessern und die Wartezeit für den Entwickler reduzieren, um Ergebnisse, Tests und den Build-Prozess im Allgemeinen zusammenzustellen. Wenn das Archiv zu früh im Prozess erscheint, wird ein unnötiger Synchronisationspunkt erstellt. Zum Kompilieren von API-abhängigen Klassen werden beispielsweise nur .class-Dateien benötigt. Weder JAR-Archive noch Ressourcen im JAR werden benötigt. Ebenso werden nur Grade-Dateien und -Ressourcen benötigt, um Gradle-Tests auszuführen. Zum Testen muss kein Glas erstellt werden. Es wird nur von einem externen Benutzer benötigt (d. H. Beim Veröffentlichen). Wenn die Erstellung eines Artefakts jedoch obligatorisch wird, können einige Aufgaben nicht parallel ausgeführt werden, und der gesamte Montageprozess wird gesperrt. Wenn dies für kleine Projekte nicht kritisch ist, ist dies für große Unternehmensprojekte der Hauptverlangsamungsfaktor.


  • Es ist viel wichtiger, dass ein JAR-Archiv als Artefakt keine Informationen über Abhängigkeiten enthalten kann.

Es ist unwahrscheinlich, dass die Abhängigkeiten der einzelnen Klassen in der Java 9- und Java 8-Laufzeit gleich sind. Ja, in unserem einfachen Beispiel ist dies der Fall, aber bei größeren Projekten ist dies nicht der Fall: Normalerweise importiert der Benutzer den Backport der Bibliothek für Java 9-Funktionen und verwendet ihn zum Implementieren der Version der Java 8-Klasse. Wenn Sie jedoch beide Versionen in einem Archiv in einem Artefakt packen Es wird Elemente mit unterschiedlichen Abhängigkeitsbäumen geben. Dies bedeutet, dass Sie bei der Arbeit mit Java 9 Abhängigkeiten haben, die niemals benötigt werden. Darüber hinaus wird der Klassenpfad verschmutzt, was zu wahrscheinlichen Konflikten für Bibliotheksbenutzer führt.


Und schließlich können Sie in einem Projekt JARs für verschiedene Zwecke erstellen:


  • für API
  • für Java 8
  • für Java 9
  • mit nativer Bindung
  • usw.

Die falsche Verwendung des Abhängigkeitsklassifikators führt zu Konflikten im Zusammenhang mit der gemeinsamen Nutzung desselben Mechanismus. Normalerweise werden Quellen oder Javadocs als Klassifizierer installiert, aber in Wirklichkeit haben sie keine Abhängigkeiten.


  • Wir möchten keine Inkonsistenzen erzeugen, der Erstellungsprozess sollte nicht davon abhängen, wie Sie die Klassen erhalten. Mit anderen Worten, die Verwendung von Multi-Release-Jars hat einen Nebeneffekt: Das Aufrufen aus dem JAR-Archiv und das Aufrufen aus dem Klassenverzeichnis sind jetzt völlig verschiedene Dinge. Sie haben einen großen Unterschied in der Semantik!
  • Abhängig davon, mit welchem ​​Tool Sie die JAR erstellen, kann es zu inkompatiblen JAR-Archiven kommen! Das einzige Tool, das garantiert, dass beim Packen von zwei Klassenoptionen in einem Archiv eine einzige offene API vorhanden ist - das JAR- Dienstprogramm selbst. Dies erfordert nicht unbedingt Montagewerkzeuge oder sogar Benutzer. JAR ist im Wesentlichen ein „Umschlag“, der einem ZIP-Archiv ähnelt. Je nachdem, wie Sie es sammeln, erhalten Sie unterschiedliche Verhaltensweisen oder Sie sammeln möglicherweise ein falsches Artefakt (und Sie werden es nicht bemerken).

Effizientere Möglichkeiten zur Verwaltung einzelner JAR-Archive


Der Hauptgrund dafür, dass Entwickler keine separaten Archive verwenden, ist, dass das Sammeln und Verwenden unpraktisch ist. Schuld daran sind die Build-Tools, die vor Gradle damit überhaupt nicht fertig wurden. Insbesondere diejenigen, die diese Methode in Maven verwendeten, konnten sich nur auf die schwache Klassifikatorfunktion verlassen , um zusätzliche Artefakte zu veröffentlichen. Der Klassifikator hilft jedoch in dieser schwierigen Situation nicht. Sie werden für verschiedene Zwecke verwendet, von der Veröffentlichung von Quellcodes, Dokumentation, Javadocs bis zur Implementierung von Bibliotheksoptionen (guava-jdk5, guava-jdk7, ...) oder verschiedenen Anwendungsfällen (api, fat jar, ...). In der Praxis kann nicht gezeigt werden, dass sich der Klassifizierer- Abhängigkeitsbaum vom Abhängigkeitsbaum des Hauptprojekts unterscheidet. Mit anderen Worten, das POM-Format ist grundsätzlich kaputt, weil Es zeigt, wie die Komponente zusammengesetzt ist und welche Artefakte sie liefert. Angenommen, Sie müssen zwei verschiedene JAR-Archive implementieren: Classic und Fat JAR, die alle Abhängigkeiten enthalten. Maven entscheidet, dass 2 Artefakte identische Abhängigkeitsbäume haben, auch wenn dies offensichtlich falsch ist! In diesem Fall ist dies mehr als offensichtlich, aber die Situation ist dieselbe wie bei JARs mit mehreren Versionen!


Die Lösung besteht darin, die Optionen korrekt zu behandeln. Gradle kann dies tun, indem Abhängigkeiten basierend auf Optionen verwaltet werden. Diese Funktion ist derzeit für die Entwicklung unter Android verfügbar, wir arbeiten jedoch auch an der Version für Java und native Anwendungen!


Das variantenbasierte Abhängigkeitsmanagement basiert auf der Tatsache, dass Module und Artefakte völlig unterschiedliche Dinge sind. Der gleiche Code kann zu unterschiedlichen Laufzeiten unter Berücksichtigung unterschiedlicher Anforderungen einwandfrei funktionieren. Für diejenigen, die mit nativer Kompilierung arbeiten, ist dies seit langem offensichtlich: Wir kompilieren für i386 und amd64 und können in keiner Weise die Abhängigkeiten der i386- Bibliothek mit arm64 beeinträchtigen ! Im Kontext von Java bedeutet dies, dass Sie für Java 8 eine Version des JAR-Archivs „Java 8“ erstellen müssen, in der das Klassenformat Java 8 entspricht. Dieses Artefakt enthält Metadaten mit Informationen zu den zu verwendenden Abhängigkeiten. Für Java 8 oder 9 werden Abhängigkeiten ausgewählt, die der Version entsprechen. So einfach ist das (der Grund ist nicht, dass die Laufzeit nur ein Optionsfeld ist, Sie können mehrere kombinieren).


Natürlich hatte dies aufgrund der übermäßigen Komplexität noch niemand getan: Maven würde offenbar niemals zulassen, dass eine so komplizierte Operation durchgeführt wird. Aber mit Gradle ist das möglich. Das Gradle-Team arbeitet derzeit an einem neuen Metadatenformat, das den Benutzern mitteilt, welche Option verwendet werden soll. Einfach ausgedrückt muss ein Build-Tool das Kompilieren, Testen, Verpacken und Verarbeiten solcher Module übernehmen. Das Projekt sollte beispielsweise in Java 8- und Java 9-Laufzeiten funktionieren. Idealerweise müssen Sie zwei Versionen der Bibliothek implementieren. Dies bedeutet, dass es 2 verschiedene Compiler gibt (um die Verwendung der Java 9-API bei der Arbeit in Java 8 zu vermeiden), 2 Klassenverzeichnisse und letztendlich 2 verschiedene JAR-Archive. Und höchstwahrscheinlich müssen auch zwei Laufzeiten getestet werden. Oder Sie implementieren 2 Archive, entscheiden sich jedoch dafür, das Verhalten der Java 8-Version in der Java 9-Laufzeit zu testen (da dies beim Start passieren kann!).


Dieses Schema wurde noch nicht umgesetzt, aber das Gradle-Team hat erhebliche Fortschritte in dieser Richtung erzielt.


So erstellen Sie mit Gradle JAR mit mehreren Versionen


Aber was soll ich tun, wenn diese Funktion noch nicht bereit ist? Entspannen Sie sich, korrekte Artefakte werden auf die gleiche Weise erstellt. Bevor die obige Funktion im Java-Ökosystem angezeigt wird, gibt es zwei Möglichkeiten:


  • guter alter Weg mit Reflection oder verschiedenen JAR-Archiven;
  • Verwenden Sie Multi-Release-JARs (beachten Sie, dass dies eine schlechte Lösung sein kann, selbst wenn es gute Anwendungsfälle gibt).

Was auch immer Sie wählen, verschiedene Archive oder JARs mit mehreren Versionen, das Schema ist das gleiche. Multi-Release-JARs sind im Wesentlichen die falsche Verpackung: Sie sollten eine Option sein, aber nicht das Ziel. Technisch gesehen ist das Quelllayout für einzelne und externe JARs gleich. In diesem Repository wird beschrieben, wie Sie mit Gradle eine JAR mit mehreren Versionen erstellen. Das Wesentliche des Prozesses wird nachstehend kurz beschrieben.


Zunächst sollten Sie sich immer an eine schlechte Angewohnheit der Entwickler erinnern: Sie führen Gradle (oder Maven) mit derselben Java-Version aus, auf der Artefakte gestartet werden sollen. Darüber hinaus wird manchmal eine spätere Version verwendet, um Gradle zu starten, und die Kompilierung erfolgt mit einer früheren API-Ebene. Es gibt jedoch keinen besonderen Grund dafür. In Gradle ist eine Ross-Kompilierung möglich. Sie können die Position des JDK beschreiben und die Kompilierung als separaten Prozess ausführen, um die Komponente mit diesem JDK zu kompilieren. Der beste Weg, um verschiedene JDKs zu konfigurieren, besteht darin, den Pfad zum JDK über Umgebungsvariablen zu konfigurieren, wie in dieser Datei beschrieben . Dann müssen Sie Gradle nur so konfigurieren, dass das richtige JDK verwendet wird, basierend auf der Kompatibilität mit der Quell- / Zielplattform . Es ist anzumerken, dass ab JDK 9 frühere Versionen von JDK nicht für die Cross-Kompilierung benötigt werden. Dies macht eine neue Funktion, -release. Gradle verwendet diese Funktion und konfiguriert den Compiler nach Bedarf.


Ein weiterer wichtiger Punkt ist der Bezeichnungsquellensatz. Quellensatz ist ein Satz von Quelldateien, die zusammen kompiliert werden müssen. Eine JAR wird durch Kompilieren eines oder mehrerer Quellensätze erhalten. Für jeden Satz erstellt Gradle automatisch eine entsprechende benutzerdefinierte Kompilierungsaufgabe. Dies bedeutet, dass sich Quellen für Java 8 und Java 9 in verschiedenen Gruppen befinden. Genau so funktioniert es im Quellensatz für Java 9 , in dem es eine Version unserer Klasse geben wird. Dies funktioniert wirklich und Sie müssen kein separates Projekt erstellen, wie in Maven. Am wichtigsten ist jedoch, dass Sie mit dieser Methode die Zusammenstellung des Sets optimieren können.


Eine der Schwierigkeiten bei unterschiedlichen Versionen einer Klasse besteht darin, dass der Klassencode selten unabhängig vom Rest des Codes ist (er hat Abhängigkeiten von Klassen, die nicht im Hauptsatz enthalten sind). Beispielsweise kann die API Klassen verwenden, die keine speziellen Quellen zur Unterstützung von Java 9 benötigen. Gleichzeitig möchte ich nicht alle diese allgemeinen Klassen neu kompilieren und ihre Versionen für Java 9 packen. Sie sind allgemeine Klassen, daher müssen sie getrennt von existieren Klassen für ein bestimmtes JDK. Wir richten es hier ein : Fügen Sie eine Abhängigkeit zwischen dem Quellensatz für Java 9 und dem Hauptsatz hinzu, damit beim Kompilieren der Version für Java 9 alle allgemeinen Klassen im Kompilierungsklassenpfad verbleiben.


Der nächste Schritt ist einfach : Sie müssen Gradle erklären, dass der Hauptquellensatz mit der Java 8-API-Ebene und der Satz für Java 9 mit der Java 9-Ebene kompiliert wird.


All dies hilft Ihnen bei der Verwendung der beiden zuvor genannten Ansätze: Implementierung separater JAR-Archive oder JARs mit mehreren Versionen. Da der Beitrag zu diesem Thema verfasst ist, sehen wir uns ein Beispiel an, wie Gradle dazu gebracht wird, eine JAR mit mehreren Versionen zu erstellen:


jar { into('META-INF/versions/9') { from sourceSets.java9.output } manifest.attributes( 'Multi-Release': 'true' ) } 

Dieser Block beschreibt: Packen von Klassen für Java 9 im Verzeichnis META-INF / version / 9 , das für MR-JARs verwendet wird, und Festlegen des Multi-Release-Labels im Manifest.


Und fertig, Ihr erstes MR JAR ist fertig!


Leider ist die Arbeit daran noch nicht abgeschlossen. Wenn Sie mit Gradle gearbeitet haben, wissen Sie, dass Sie die Anwendung bei Verwendung des Anwendungs-Plugins direkt über die Ausführungsaufgabe ausführen können . Aufgrund der Tatsache, dass Gradle normalerweise versucht, den Arbeitsaufwand zu minimieren, muss die Ausführungsaufgabe sowohl die Klassenverzeichnisse als auch die Verzeichnisse der verarbeiteten Ressourcen verwenden. Bei Multi-Release-JARs ist dies ein Problem, da JARs sofort benötigt werden! Anstatt das Plugin zu verwenden, müssen Sie daher Ihre eigene Aufgabe erstellen. Dies ist ein Argument gegen die Verwendung von JARs mit mehreren Versionen.


Und zu guter Letzt haben wir erwähnt, dass wir 2 Versionen der Klasse testen müssen. Zu diesem Zweck können Sie nur VM in einem separaten Prozess verwenden, da es für die Java-Laufzeit kein Äquivalent zum Marker -release gibt . Die Idee ist, dass nur ein Test geschrieben werden muss, der jedoch zweimal ausgeführt wird: in Java 8 und Java 9. Nur so kann sichergestellt werden, dass die laufzeitspezifischen Klassen ordnungsgemäß funktionieren. Standardmäßig erstellt Gradle eine Testaufgabe und verwendet Klassenverzeichnisse auf dieselbe Weise anstelle der JAR. Daher werden wir zwei Dinge tun: Erstellen Sie eine Testaufgabe für Java 9 und konfigurieren Sie beide Aufgaben so, dass sie die JAR und die angegebenen Java-Laufzeiten verwenden. Die Implementierung sieht folgendermaßen aus:


 test { dependsOn jar def jdkHome = System.getenv("JAVA_8") classpath = files(jar.archivePath, classpath) - sourceSets.main.output executable = file("$jdkHome/bin/java") doFirst { println "$name runs test using JDK 8" } } task testJava9(type: Test) { dependsOn jar def jdkHome = System.getenv("JAVA_9") classpath = files(jar.archivePath, classpath) - sourceSets.main.output executable = file("$jdkHome/bin/java") doFirst { println classpath.asPath println "$name runs test using JDK 9" } } check.dependsOn(testJava9) 

Wenn die Aufgabe gestartet wird, überprüfen Sie , ob Gradle jeden Quellensatz mit dem gewünschten JDK kompiliert, eine JAR mit mehreren Versionen erstellt und die Tests mit dieser JAR auf beiden JDKs ausführt. Zukünftige Versionen von Gradle werden Ihnen dabei helfen, dies deklarativer zu tun.


Fazit


Zusammenfassend. Sie haben gelernt, dass Multi-Release-JARs ein Versuch sind, das eigentliche Problem zu lösen, mit dem viele Bibliotheksentwickler konfrontiert sind. Diese Lösung des Problems scheint jedoch falsch zu sein. Richtiges Abhängigkeitsmanagement, Verknüpfung von Artefakten und Optionen, Sorge um die Leistung (die Fähigkeit, so viele Aufgaben wie möglich parallel auszuführen) - all dies macht MR JAR zu einer Lösung für die Armen. Dieses Problem kann mithilfe von Optionen korrekt gelöst werden. Während sich das Abhängigkeitsmanagement mit Optionen von Gradle in der Entwicklung befindet, sind JARs mit mehreren Releases in einfachen Fällen recht praktisch. In diesem Fall hilft Ihnen dieser Beitrag zu verstehen, wie dies zu tun ist und wie sich die Philosophie von Gradle von Maven unterscheidet (Quellensatz vs. Projekt).


Schließlich bestreiten wir nicht, dass es Fälle gibt, in denen JARs mit mehreren Releases sinnvoll sind: Wenn beispielsweise nicht bekannt ist, in welcher Umgebung die Anwendung ausgeführt wird (keine Bibliothek), ist dies eher eine Ausnahme. In diesem Beitrag haben wir die Hauptprobleme beschrieben, mit denen Bibliotheksentwickler konfrontiert sind, und wie JARs mit mehreren Versionen versuchen, sie zu lösen. Die korrekte Modellierung von Abhängigkeiten als Optionen verbessert die Leistung (durch fein abgestimmte Parallelität) und reduziert die Wartungskosten (Vermeidung unvorhergesehener Komplexität) im Vergleich zu JARs mit mehreren Versionen. In Ihrer Situation werden möglicherweise auch MR-JARs benötigt, daher hat Gradle dies bereits erledigt. Schauen Sie sich dieses Beispielprojekt an und probieren Sie es selbst aus.

Source: https://habr.com/ru/post/de428868/


All Articles