Ich habe eine kleine StreamEx- Bibliothek, die die Java 8 Stream-API erweitert. Ich sammle die Bibliothek traditionell über Maven und bin größtenteils mit allem zufrieden. Ich wollte jedoch experimentieren.
Einige Dinge in der Bibliothek sollten in verschiedenen Java-Versionen unterschiedlich funktionieren. Das auffälligste Beispiel sind die neuen Stream-API-Methoden wie takeWhile
, die nur in Java 9 takeWhile
waren. Meine Bibliothek bietet eine Implementierung dieser Methoden auch in Java 8, aber wenn Sie die Stream-API selbst erweitern, takeWhile
Sie einigen Einschränkungen, die ich hier nicht erwähne. Ich wünschte, Java 9+ Benutzer hätten Zugriff auf eine Standardimplementierung.
Damit das Projekt mit Java 8 weiter kompiliert werden kann, erfolgt dies normalerweise mithilfe von Reflection-Tools: Wir finden heraus, ob die Standardbibliothek eine entsprechende Methode enthält, und rufen sie in diesem Fall auf. Andernfalls verwenden wir unsere Implementierung. Ich habe mich jedoch für die MethodHandle-API entschieden , da sie weniger Aufrufaufwand deklariert. Sie können MethodHandle
im Voraus MethodHandle
und in einem statischen Feld speichern:
MethodHandles.Lookup lookup = MethodHandles.publicLookup(); MethodType type = MethodType.methodType(Stream.class, Predicate.class); MethodHandle method = null; try { method = lookup.findVirtual(Stream.class, "takeWhile", type); } catch (NoSuchMethodException | IllegalAccessException e) {
Und dann benutze es:
if (method != null) { return (Stream<T>)method.invokeExact(stream, predicate); } else {
Das ist alles gut, aber es sieht hässlich aus. Und am wichtigsten ist, dass Sie an jedem Punkt, an dem eine Variation der Implementierungen möglich ist, solche Bedingungen schreiben müssen. Ein etwas alternativer Ansatz besteht darin, die Strategien von Java 8 und Java 9 als Implementierung derselben Schnittstelle zu trennen. Um die Größe der Bibliothek zu speichern, implementieren Sie einfach alles für Java 8 in einer separaten, nicht endgültigen Klasse und ersetzen Sie Java 9 durch einen Erben. Es wurde ungefähr so gemacht:
Dann können Sie an Verwendungspunkten einfach return Internals.VER_SPEC.takeWhile(stream, predicate)
schreiben. Die ganze Magie mit Methodenhandles ist jetzt nur in der Java9Specific
Klasse. Dieser Ansatz speicherte übrigens die Bibliothek für Android-Benutzer, die sich zuvor darüber beschwert hatten, dass dies im Prinzip nicht funktioniert. Die virtuelle Android-Maschine ist kein Java, sie implementiert nicht einmal die Java 7-Spezifikation. Insbesondere gibt es keine Methoden mit einer polymorphen Signatur wie invokeExact
, und das Vorhandensein dieses Aufrufs im Bytecode bricht alles. Jetzt werden diese Aufrufe in eine Klasse gestellt, die niemals initialisiert wird.
All dies ist jedoch immer noch hässlich. Eine schöne Lösung (zumindest theoretisch) ist die Verwendung des Multi Release Jar, das mit Java 9 ( JEP-238 ) geliefert wurde . Dazu muss ein Teil der Klassen unter Java 9 kompiliert und die Klassendateien in META-INF/versions/9
in der Jar-Datei abgelegt werden. Außerdem müssen Sie dem Manifest die Zeile Multi-Release: true
hinzufügen. Dann ignoriert Java 8 dies alles erfolgreich und Java 9 und neuere laden neue Klassen anstelle von Klassen mit denselben Namen, die sich an der üblichen Stelle befinden.
Das erste Mal habe ich dies vor mehr als zwei Jahren versucht, kurz vor der Veröffentlichung von Java 9. Es war sehr schwierig und ich habe aufgehört. Es war schon schwierig, das Projekt mit dem Java 9-Compiler zu kompilieren: Viele Maven-Plugins brachen einfach aufgrund geänderter interner APIs, geänderter java.version
Zeichenfolgenformate oder etwas anderem.
Ein neuer Versuch in diesem Jahr war erfolgreicher. Plugins wurden größtenteils aktualisiert und funktionieren im neuen Java recht gut. Im ersten Schritt habe ich die gesamte Assembly in Java 11 übersetzt. Dazu musste ich neben der Aktualisierung der Plugin-Versionen Folgendes tun:
Der nächste Schritt war die Anpassung der Tests. Da das Bibliotheksverhalten in Java 8 und Java 9 offensichtlich unterschiedlich ist, wäre es logisch, Tests für beide Versionen auszuführen. Jetzt führen wir alles unter Java 11 aus, sodass Java 8-spezifischer Code nicht getestet wird. Dies ist ein ziemlich großer und nicht trivialer Code. Um dies zu beheben, habe ich einen künstlichen Stift hergestellt:
static final VersionSpecific VER_SPEC = System.getProperty("java.version", "").compareTo("1.9") > 0 && !Boolean.getBoolean("one.util.streamex.emulateJava8") ? new Java9Specific() : new VersionSpecific();
-Done.util.streamex.emulateJava8=true
jetzt einfach -Done.util.streamex.emulateJava8=true
wenn Sie die Tests -Done.util.streamex.emulateJava8=true
maven-surefire-plugin
zu testen, was normalerweise in Java 8 maven-surefire-plugin
argLine = -Done.util.streamex.emulateJava8=true
Konfiguration des maven-surefire-plugin
mit argLine = -Done.util.streamex.emulateJava8=true
einen neuen <execution>
-Block argLine = -Done.util.streamex.emulateJava8=true
, und die Tests bestehen zweimal.
Ich möchte jedoch die Gesamtabdeckungstests berücksichtigen. Ich benutze JaCoCo, und wenn Sie ihm nichts sagen, überschreibt der zweite Lauf einfach die Ergebnisse des ersten. Wie funktioniert JaCoCo? Zuerst wird das Prepare prepare-agent
Ziel ausgeführt, das die argLine Maven-Eigenschaft -javaagent:blah-blah/.m2/org/jacoco/org.jacoco.agent/0.8.4/org.jacoco.agent-0.8.4-runtime.jar=destfile=blah-blah/myproject/target/jacoco.exec
etwas wie -javaagent:blah-blah/.m2/org/jacoco/org.jacoco.agent/0.8.4/org.jacoco.agent-0.8.4-runtime.jar=destfile=blah-blah/myproject/target/jacoco.exec
. Ich möchte, dass zwei verschiedene Exec-Dateien gebildet werden. Sie können es auf diese Weise hacken. Fügen Sie destFile=${project.build.directory}
Konfiguration destFile=${project.build.directory}
prepare-agent
. Rau aber effektiv. Jetzt argLine
mit argLine
blah-blah/myproject/target
. Ja, dies ist überhaupt keine Datei, sondern ein Verzeichnis. Wir können den Dateinamen jedoch bereits zu Beginn der Tests ersetzen. Wir kehren zum maven-surefire-plugin
und setzen argLine = @{argLine}/jacoco_java8.exec -Done.util.streamex.emulateJava8=true
für den Java 8-Lauf und argLine = @{argLine}/jacoco_java11.exec
für den Java 11-Lauf. Dann ist es einfach, diese beiden Dateien mithilfe des merge
zu kombinieren, das auch das JaCoCo-Plugin bereitstellt, und wir erhalten die Gesamtabdeckung.
Update: godin in den Kommentaren sagte, dass es unnötig war, Sie können in eine Datei schreiben, und es wird automatisch das Ergebnis mit der vorherigen kleben. Jetzt bin ich mir nicht sicher, warum dieses Szenario anfangs bei mir nicht funktioniert hat.
Nun, wir sind gut vorbereitet, auf das Multi-Release-Glas umzusteigen. Ich habe eine Reihe von Empfehlungen dazu gefunden. Das erste schlug die Verwendung eines multimodularen Maven-Projekts vor. Ich habe keine Lust dazu: Dies ist eine große Komplikation der Projektstruktur: Es gibt zum Beispiel fünf pom.xml. Für ein paar Dateien herumzuspielen, die in Java 9 kompiliert werden müssen, scheint übertrieben. Ein anderer schlug vor, die Kompilierung über das maven-antrun-plugin
. Hier habe ich beschlossen, nur als letzten Ausweg zu suchen. Es ist klar, dass jedes Problem in Maven mit Ant gelöst werden kann, aber das ist irgendwie ziemlich ungeschickt. Schließlich sah ich eine Empfehlung zur Verwendung eines Multi-Release-Jar-Maven-Plugin-Plugins eines Drittanbieters. Es klang schon lecker und richtig.
Das Plugin empfiehlt, Quellcodes, die für neue Java-Versionen spezifisch sind, in Verzeichnissen wie src/main/java-mr/9
, was ich auch getan habe. Ich habe mich immer noch entschlossen, Kollisionen in den Klassennamen maximal zu vermeiden. Daher habe ich als einzige Klasse (sogar die Schnittstelle), die sowohl in Java 8 als auch in Java 9 vorhanden ist, Folgendes:
Die alte Konstante zog an einen neuen Ort, aber nichts anderes änderte sich wirklich. Erst jetzt ist die Java9Specific
Klasse viel einfacher geworden: Alle Squats mit MethodHandle wurden erfolgreich durch direkte Methodenaufrufe ersetzt.
Das Plugin verspricht Folgendes:
- Ersetzen Sie das Standard-
maven-compiler-plugin
und kompilieren Sie in zwei Schritten mit einer anderen Zielversion. - Ersetzen Sie das Standard
maven-jar-plugin
das maven-jar-plugin
und maven-jar-plugin
Kompilierungsergebnis mit den richtigen Pfaden. - Fügen Sie die Zeile
Multi-Release: true
zu MANIFEST.MF
.
Damit es funktioniert, waren einige Schritte erforderlich.
Ändern Sie die Verpackung von jar
zu multi-release-jar
.
Build-Erweiterung hinzufügen:
<build> <extensions> <extension> <groupId>pw.krejci</groupId> <artifactId>multi-release-jar-maven-plugin</artifactId> <version>0.1.5</version> </extension> </extensions> </build>
Kopieren Sie die Konfiguration vom maven-compiler-plugin
. Ich hatte nur die Standardversion im Sinne von <source>1.8</source>
und <arg>-Xlint:all</arg>
Ich dachte, dass das maven-compiler-plugin
jetzt entfernt werden kann, aber es stellte sich heraus, dass das neue Plugin die Kompilierung von Tests nicht ersetzt. -Xlint:all
die Java-Version auf den Standardwert (1.5!) -Xlint:all
Und das Argument -Xlint:all
verschwand. Also musste ich gehen.
Um die Quelle und das Ziel für die beiden Plugins nicht zu duplizieren, habe ich herausgefunden, dass beide die Eigenschaften von maven.compiler.source
und maven.compiler.target
. Ich habe sie installiert und die Versionen aus den Plugin-Einstellungen entfernt. Es stellte sich jedoch plötzlich heraus, dass das maven-javadoc-plugin
die source
aus den Einstellungen des maven-compiler-plugin
verwendet, um die URL des Standard-JavaDoc herauszufinden, die verknüpft werden muss, wenn auf Standardmethoden verwiesen wird. Und jetzt respektiert er maven.compiler.source
. Daher musste ich <source>${maven.compiler.source}</source>
an die Einstellungen des maven-compiler-plugin
. Glücklicherweise waren keine weiteren Änderungen erforderlich, um JavaDoc zu generieren. Es kann sehr gut aus Java 8-Quellen generiert werden, da das gesamte Karussell mit Versionen die Bibliotheks-API nicht beeinflusst.
Das maven-bundle-plugin
, was meine Bibliothek in ein OSGi-Artefakt verwandelt hat. Er weigerte sich einfach, mit packaging = multi-release-jar
. Im Prinzip mochte ich ihn nie. Er schreibt eine Reihe zusätzlicher Zeilen in das Manifest, verdirbt gleichzeitig die Sortierreihenfolge und fügt mehr Müll hinzu. Glücklicherweise stellte sich heraus, dass es nicht schwierig ist, alles loszuwerden, indem Sie alles, was Sie brauchen, von Hand schreiben. Nur natürlich nicht im maven-jar-plugin
, sondern im neuen. Die gesamte Konfiguration des multi-release-jar
Plugins wurde schließlich so (ich habe einige Eigenschaften wie project.package
selbst definiert):
<plugin> <groupId>pw.krejci</groupId> <artifactId>multi-release-jar-maven-plugin</artifactId> <version>0.1.5</version> <configuration> <compilerArgs><arg>-Xlint:all</arg></compilerArgs> <archive> <manifestEntries> <Automatic-Module-Name>${project.package}</Automatic-Module-Name> <Bundle-Name>${project.name}</Bundle-Name> <Bundle-Description>${project.description}</Bundle-Description> <Bundle-License>${license.url}</Bundle-License> <Bundle-ManifestVersion>2</Bundle-ManifestVersion> <Bundle-SymbolicName>${project.package}</Bundle-SymbolicName> <Bundle-Version>${project.version}</Bundle-Version> <Export-Package>${project.package};version="${project.version}"</Export-Package> </manifestEntries> </archive> </configuration> </plugin>
Tests. Wir haben nicht mehr one.util.streamex.emulateJava8
, aber Sie können den gleichen Effekt erzielen, indem Sie die Klassenpfadtests one.util.streamex.emulateJava8
. Jetzt ist das Gegenteil der Fall: Standardmäßig arbeitet die Bibliothek im Java 8-Modus, und für Java 9 müssen Sie Folgendes schreiben:
<classesDirectory>${basedir}/target/classes-9</classesDirectory> <additionalClasspathElements>${project.build.outputDirectory}</additionalClasspathElements> <argLine>@{argLine}/jacoco_java9.exec</argLine>
Ein wichtiger Punkt: classes-9
sollten gewöhnlichen Klassendateien vorausgehen, daher mussten wir die üblichen in additionalClasspathElements
Klassenpfad-Elemente übertragen, die anschließend hinzugefügt werden.
Quellen. Ich werde Source-JAR haben, und es wäre schön, Java 9-Quellen darin zu packen, damit beispielsweise der Debugger in der IDE sie korrekt anzeigen kann. Ich VerSpec
mir keine großen Sorgen um doppelte VerSpec
, da es eine Zeile gibt, die nur während der Initialisierung ausgeführt wird. Es ist in Java9Specific.java
, nur die Version von Java 8 zu Java9Specific.java
. Es wäre jedoch schön, Java9Specific.java
als Java9Specific.java
anzuhängen. Dies kann durch manuelles Hinzufügen eines zusätzlichen Quellverzeichnisses erfolgen:
<plugin> <groupId>org.codehaus.mojo</groupId> <artifactId>build-helper-maven-plugin</artifactId> <version>3.0.0</version> <executions> <execution> <phase>test</phase> <goals><goal>add-source</goal></goals> <configuration> <sources> <source>src/main/java-mr/9</source> </sources> </configuration> </execution> </executions> </plugin>
Nachdem ich das Artefakt gesammelt hatte, verband ich es mit einem Testprojekt und überprüfte es im IntelliJ IDEA-Debugger. Alles funktioniert wunderbar: Abhängig von der Version der virtuellen Maschine, mit der das Testprojekt ausgeführt wird, landen wir beim Debuggen in einer anderen Quelle.
Es wäre cool, dies mit dem Multi-Release-Jar-Plugin selbst erledigen zu lassen, also habe ich einen solchen Vorschlag gemacht.
JaCoCo. Es stellte sich heraus, dass es für ihn am schwierigsten war, und ich konnte nicht ohne fremde Hilfe auskommen. Tatsache ist, dass das Plug-in perfekt generierte Exec-Dateien für Java-8 und Java-9 normalerweise in eine Datei geklebt hat. Beim Generieren von Berichten in XML und HTML haben sie die Quellen aus Java-9 jedoch hartnäckig ignoriert. Beim Stöbern in der Quelle habe ich festgestellt, dass nur ein Bericht für die in project.getBuild().getOutputDirectory()
gefundenen project.getBuild().getOutputDirectory()
. Dieses Verzeichnis kann natürlich ersetzt werden, aber tatsächlich habe ich zwei davon: classes
und classes-9
. Theoretisch können Sie alle Klassen in ein Verzeichnis kopieren, das outputDirectory ändern und JaCoCo starten und dann das outputDirectory
zurück ändern, um die JAR-Assembly nicht zu beschädigen. Das klingt aber völlig hässlich. Im Allgemeinen habe ich beschlossen, die Lösung für dieses Problem in meinem Projekt vorerst zu verschieben, aber ich habe den Jungs von JaCoCo geschrieben, dass es schön wäre, mehrere Verzeichnisse mit Klassendateien angeben zu können.
Zu meiner Überraschung kam nur wenige Stunden später einer der Entwickler von JaCoCo godin zu meinem Projekt und brachte eine Pull-Anfrage , die das Problem löst. Wie entscheidet es? Natürlich mit Ant! Es stellte sich heraus, dass das Ant-Plugin für JaCoCo weiter fortgeschritten ist und einen zusammenfassenden Bericht für mehrere Quellverzeichnisse und Klassendateien erstellen kann. Er brauchte nicht einmal einen separaten merge
, da er mehrere Exec-Dateien gleichzeitig füttern konnte. Im Allgemeinen konnte Ant nicht vermieden werden, so sei es. Die Hauptsache, die funktionierte, und pom.xml wuchs nur um sechs Zeilen.

Ich habe sogar in Herzen getwittert:
Also habe ich ein sehr funktionierendes Projekt bekommen, das ein wunderschönes Multi-Release-Glas baut. Gleichzeitig stieg der Prozentsatz der Abdeckung sogar an, da ich alle Arten von catch (NoSuchMethodException | IllegalAccessException e)
, die in Java 9 nicht erreichbar waren. Leider wird diese Projektstruktur von IntelliJ IDEA nicht unterstützt, sodass ich den POM-Import abbrechen und das Projekt in der IDE manuell konfigurieren musste . Ich hoffe, dass es auch in Zukunft eine Standardlösung geben wird, die automatisch von allen Plugins und Tools unterstützt wird.