Unterstützung verschiedener Versionen von Haupt- (und Test-) Quellensätzen für verschiedene Java-Versionen (6, 7, 8)

8

Ich habe ein Bibliotheksprojekt in Java. Ich möchte mehrere Versionen des Projekts implementieren, testen und wahrscheinlich freigeben, die mit verschiedenen Java-Versionen verwendet werden sollen: 6, 7, 8.

Der einfachste Weg besteht darin, ein Projekt zu kopieren und mehrere Quellbäume zu unterstützen, aber ich möchte dies vermeiden, weil es langwierig und fehleranfällig ist.

Eine andere Möglichkeit besteht darin, das "Basis" -Projekt und mehrere Java-Version-spezifische Projekte abhängig zu machen. Versionen unterscheiden sich sehr geringfügig, aber ich möchte nicht, was dieses technische Entwicklungsproblem in der Klassenhierarchie widerspiegelt.

Also ich suche

  • eine Art Precompiler
  • und / oder Standard Maven Optionen
  • und / oder Maven Plugins

, die helfen könnte, mehrere Java-Version-spezifische Versionen der Bibliothek aus einer einzelnen Quellstruktur zu unterstützen, und transparent für die Lib-Benutzer.

    
leventov 04.10.2014, 17:24
quelle

8 Antworten

8

Sie können ein jar für jede Java-Version (6, 7, 8) aus einer einzelnen pom.xml-Datei generieren.

Alle relevanten Arbeiten finden im maven-compiler-plugin statt: kompilieren mojo.

Der Trick besteht darin, das mojo dreimal auszuführen, wobei jedes Mal die resultierende Datei in eine andere outputFileName . Dies führt dazu, dass der Compiler mehrere Male ausgeführt wird, wobei jedes Mal unterschiedliche Versionen verwendet und eine entsprechend benannte Datei ausgegeben wird.

%Vor%

Ich hoffe, das hilft.

BEARBEITEN

RE: zusätzliche Anforderung, dass jeder Lauf gegen verschiedene Quellen kompiliert.

Siehe Änderungen im Pom-Snippet oben.

Jede Ausführung kann eine eigene Abhängigkeiten Bibliotheksliste definieren.

Also hängt JDK6-Build von ABC.jar ab, aber JDK7 hängt von XYZ.jar ab.

    
333kenshin 10.10.2014 09:45
quelle
4

Sie können einige Quellverzeichnisse bedingt hinzufügen, indem Sie für jede Java-Version separate Profile erstellen. Dann können Sie maven mit dem Profilnamen ausführen, der definiert, welche Quellen für welche Version Sie verwenden möchten. In diesem Fall bleiben alle gemeinsamen Quellen in src/main/java , während von der Java-Version abhängige Dateien in /src/main/java-X.X directories abgelegt sind.

%Vor%

Sie können dies wahrscheinlich noch dynamischer machen, indem Sie hardcoded java-X.X durch eine Eigenschaft ersetzen, die Sie zusammen mit dem Profil an maven übergeben. Das wäre etwa so:

%Vor%

Und später, wenn Sie es ausführen, übergeben Sie einfach mvn -Pconditional-java -Dmy.java.version=1.6 .

Dies erfordert, dass Sie von der Java-Version abhängige Dateien in separaten Verzeichnissen ablegen. Wenn Sie in Ihrer IDE gegen eine bestimmte Version von Java entwickeln, markieren Sie einfach das für Ihre Java-Version relevante Verzeichnis als Quellordner (weil IDEs standardmäßig nur src / main / java als Quellverzeichnis erkennen).

Genauso wie Sie die Compiler-Ebene an maven compiler plugin übergeben können:

%Vor%     
walkeros 15.10.2014 14:49
quelle
3

Warum sammeln Sie nicht die Methoden, die in jeder Java-Version anders sein sollen, wickeln Sie sie in ein "Dienstprogramm" -Projekt ein, erstellen Sie Ihre eigene API, die Ihr Hauptcode aufrufen kann, und geben Sie bei der Verteilung das entsprechende Dienstprogramm an Jar du willst?

etwas wie:

util-api.jar (Methoden, die Ihr Hauptprojekt aufruft)

util-1.6.jar (welche Implementierung auch immer gilt, auch "keine Operation", wenn nichts zu tun ist)

Ich habe das schon oft mit ähnlichen Problemen wie jetzt gemacht.

    
Luis Matos 10.10.2014 12:04
quelle
3

Die Leute, die an Hibernate arbeiten, rangen mit diesem Problem einige Zeit herum, bevor sie sich entschließen, zu einem Gradle basierend auf Build.

Sehen Sie sich Gradle: warum? für weitere Informationen an.

    
Steve C 16.10.2014 05:33
quelle
3

Ein Weg ist in Code eingebettet: Vom log4j2-Quellcode:

%Vor%

Sie können auch Ссылка verwenden und Variablen basierend auf der Java-Version festlegen, um den Code zu ändern. Allerdings würde dies an so wenigen Orten tun, wie es schwierig sein wird zu debuggen. Oder zumindest ein Protokoll drucken, bevor der dynamische Code ausgeführt wird, damit Sie wissen, welche Version / echte Zeile das Problem hat.

    
tgkprog 05.10.2014 07:54
quelle
2

Für so etwas würde ich

  • verschiebt JVM-versionsabhängige Dateien in ihr eigenes Paket
  • Alternativ verwenden Sie eine Namenskonvention, um JVM-versionsspezifische Dateien zu identifizieren
  • Verwenden Sie anstelle von Maven Ant zum Kompilieren. Ant erleichtert das Ausschließen von Quelldateien basierend auf Name oder Speicherort. Sie können auch einfach mehrere Ziele mit Ant.
  • erstellen
  • Wenn die Bibliothek für alle JVM-Versionen dieselbe API verwendet, würde ich Schnittstellen für die JVM-spezifischen Klassen erstellen und dann die entsprechenden Implementierungen zur Laufzeit in Abhängigkeit von der JVM-Version instanziieren.
Tim Jansen 15.10.2014 14:59
quelle
1

Was Sie tun müssen, ist eine Basis-API zu definieren und dann die Funktionen hinzuzufügen, die den neuen Versionen von Java als zusätzliche Methoden zur Verfügung stehen (die von Ihren ursprünglichen API-Methoden aufgerufen werden können). Dadurch kann die grundlegende Integrität Ihrer API von der alten zur neuen Implementierung beibehalten werden.

Zum Beispiel:

%Vor%

ist Ihre ursprüngliche API-Methode, wobei Consumer Ihr org.mylib.Consumer ist. Sie können in Java 8 zwei Dinge damit tun. Sie können Ihre alte Implementierung beibehalten und eine neue Methode als Komfortmethode hinzufügen, oder Sie können Ihre neue Implementierung hinzufügen und ein hinzufügen Defender-Methode , die den neuen aufruft. In beiden Fällen bleibt Legacy-Code, der Ihre API implementiert, gültig.

Convenience-Methode Beispiel:

%Vor%

Defender-Methode Beispiel:

%Vor%

Offensichtlich können Sie für die Java 7-Version die Standardmethoden auf der Benutzeroberfläche nicht definieren, daher müssen Sie diese Methoden stattdessen in abstrakte Basisklassen implementieren, aber das Konzept ist ziemlich gleich. Theoretisch würden Sie in Java 8 den Defender nicht benötigen, da der Aufruf genau gleich sein kann, da jede gültige Instanz Ihres Consumers eine gültige Instanz von j.u.f.C ist.

Java 8 bedeutet zum ersten Mal, dass es vollkommen zulässig ist, einer vorhandenen Schnittstelle neue Methoden hinzuzufügen. Sie sollten jedoch zwischen Java 6 und 7 keine unterschiedlichen API-Signaturen haben.

Vermeiden Sie für Java 6 und 7 einfach die folgenden Dinge in Ihrem Quellcode und Sie können beide kompilieren:

  • Der Diamantoperator & lt; & gt; Es ist praktisch, aber nicht zu verwenden bedeutet eine Codebasis für J6 und J7
  • switch-Anweisungen (In fast allen Fällen sollten Sie wahrscheinlich mehrere Klassen und eine Factory für sie haben, wenn Sie eine switch-Anweisung haben.)
  • Versuche mit Ressourcen. Es ist eines meiner Lieblingsfeatures von J7, aber letztendlich sind Blöcke immer noch perfekt gültig.
  • mehrfacher Ausnahmefehler
  • Der J7 Path api - die alte Datei-API ist besser für die Legacy-Kodierung
  • API zum Abzweigen und Verknüpfen

Ich bin mir nicht wirklich bewusst, dass Code für J6 geschrieben wurde, der nicht auf einer J8 JVM ausgeführt wird, wenn er für J8 kompiliert wird. Aber ich nehme an, dass Sie wahrscheinlich die neueren Versionen der Bibliothek nutzen möchten, um die J8-Verbesserungen wie Streams zu nutzen und Lambda.

Sie möchten vielleicht auch den Java Services Provider api von Java 7 aus betrachten. Sie könnten Ihre API in einem Basismodul definieren und die Implementierungen als JAR-Dateien für Java Services-Plugins installieren, die von der JVM so lange erkannt werden wie sie im Klassenpfad Ihrer Anwendung sind. Dann können Sie einfach neue Service-Plugin-Jars definieren, sobald neue Funktionen verfügbar werden. Natürlich hilft das Ihrer Java-6-Implementierung nicht, aber Sie könnten Ihre J6-API als Basis verwenden, die Add-On-Funktion für Services für Ihre J7-API hinzufügen und einfach weitere Services hinzufügen und ersetzen, wenn die JVM aktualisiert wird. p>     

Steve K 17.10.2014 02:54
quelle
1

Ich habe hier als Antwort auf Leventovs letzte Bemerkung über eine explizite Besetzung eine andere Antwort hinzugefügt und biete diesen Rat an. Es ist vielleicht eine schlechte Übung, aber ich fand es sehr nützlich für die Definition meiner eigenen Schnittstellen in einigen Fällen, wo ich etwas Pre-or-Post-Verarbeitung einbringen oder meine eigene Abstraktionsebene über der von jemand anderem legen wollte , haben wir einen solchen für Hibernate's Work-Klassen definiert, sodass, wenn sich die Hibernate-API in der Zukunft ändert, unsere Implementierungen nicht - nur die Standardmethode in unserem Interface), und es das Leben mit den beiden Versionen von die gleiche Schnittstelle.

Bedenken Sie Folgendes: Das Schöne an der funktionalen Schnittstelle ist, dass es nur eine abstrakte Methode gibt, sodass Sie ein Lambda als Implementierung dieser Methode übergeben können.

Aber wenn Sie eine Schnittstelle erweitern, die immer noch dieselbe Funktionalität haben soll (ein Lambda übergeben und in allen Fällen funktionieren), kommt eine andere Schönheit von Java 8 ins Spiel: Defender-Methoden. Nichts sagt irgendwo, dass Sie die SAME-Methode abstrakt lassen müssen, wie es die Elternklasse getan hat. Sie können eine Schnittstelle als eine Art Interzeptor-Schnittstelle erweitern. So können Sie Ihren MyConsumer wie folgt definieren:

%Vor%

Unsere Schnittstelle implementiert anstelle von accept (T) als abstrakte Methode accept (T) und definiert eine abstrakte Methode getConsumer (). Dies bewirkt, dass das Lambda einen MyConsumer instanziiert, der sich von dem unterscheidet, der zum Instantiieren eines j.u.f.Consumer erforderlich ist, und entfernt den Klassenkonflikt des Compilers. Dann können Sie Ihre Klasse definieren, die Iterable implementiert, um stattdessen Ihre eigene benutzerdefinierte Schnittstelle zu implementieren, die Iterable erweitert.

%Vor%

Es hat immer noch eine forEach-Methode, die es dem iterablen Iterface erlaubt, aber Ihr gesamter Code kann in allen Versionen Ihrer API jedes () anstelle von forEach () aufrufen. Auf diese Weise können Sie Ihren Code auch teilweise zukunftssicher machen - wenn die zugrundeliegende Java-API jahrelang veraltet wäre, könnten Sie Ihre Standardmethode () ändern, um die Dinge auf die neue Art und Weise zu tun, aber an jeder anderen Stelle alles Ihr vorhandener Implementierungscode ist weiterhin funktional korrekt .

Wenn Sie also api.each aufrufen, müssen Sie anstelle einer expliziten Umwandlung einfach das Lambda für die andere Methode übergeben ... In MyConsumer gibt die Methode einen Verbraucher zurück, also ist Ihr Lambda wirklich einfach, Sie fügen einfach hinzu der Lambda-Zero-Arg-Konstruktor zu Ihrer vorherigen Aussage. Die Methode accept () in Consumer verwendet ein Argument und gibt eine void zurück. Wenn Sie Java also ohne Argumente definieren, weiß Java, dass es eine Schnittstelle mit einer abstrakten Methode ohne Argumente benötigt und dieses Lambda Instantiiert MyConsumer.

%Vor%

während dieser instanziiert j.u.f.Consumer

%Vor%

Da die ursprüngliche abstrakte Methode (accept) vorhanden und implementiert ist, ist sie immer noch eine gültige Instanz von Consumer und wird in allen Fällen funktionieren, aber da wir den 0-arg-Konstruktor aufgerufen haben, haben wir ihn explizit zu einer Instanz gemacht unserer benutzerdefinierten Schnittstelle. Auf diese Weise erfüllen wir immer noch den Schnittstellenvertrag von Consumer, aber wir können die Signatur unserer Schnittstelle von Consumer unterscheiden.

    
Steve K 20.10.2014 01:45
quelle