Den neuen Java-Release-Zyklus beherrschen

Java ganz konkret: Was der neue JDK-Release-Zyklus für Entwickler bedeutet

Sven Ruppert

© Shutterstock / Maksim Kabakou

 

Seit einiger Zeit ist bekannt, dass Oracle die Zyklen, in denen das JDK veröffentlicht wird, geändert hat. Nun ist es soweit, dass die Auswirkungen auch für die meisten Projekte spürbar werden. Was also ist zu beachten?

In dieser Serie werden wir uns mit den praktischen Auswirkungen der neuen Release-Zyklen des JDK auseinander setzen. Allerdings behandeln wir das Thema ausschließlich aus der Sicht der Entwickler. Sicherlich gibt es noch das ein oder andere auf der rechtlichen Seite zu klären, und es lohnt sich, auch diesen Aspekt für die eigenen Projekte zu beleuchten. Hier wollen wir uns aber lediglich auf die praktischen Dinge konzentrieren.

Die Geschichte

Viele Jahre lang wurden die JDK-Releases, wenn man von den Hauptversionen spricht, lediglich alle paar Jahre veröffentlicht. Was auf der einen Seite zu der Annahme führte, dass Java eine sich nur träge weiterentwickelnde Sprache sei, führte auf der anderen Seite zu einer für die Industrie
sehr gut passenden Stabilität.

Diese Stabilität ist, meiner Meinung nach, auch einer der Gründe gewesen, dass sich ein so reichhaltiges Open-Source-Ökosystem um die Sprache herum entwickeln konnte. Für Firmen, die Produkte für Java entwickelten, ist es ebenfalls sehr von Vorteil gewesen, konnte man sich doch auf die neuen Funktionen konzentrieren, anstatt immer neuen Versionen hinterher laufen zu müssen.

Viele Open-Source-Projekte haben eine große Userbasis, aber leider eine recht überschaubare Anzahl an aktiven Mitgliedern, die in regelmäßigen Abständen Beiträge leisten. Aufgrund der langan Zeiträume zwischen neuen Java-Hauptversionen konnte man sich auch hier voll und ganz auf die Umsetzung von Funktionalitäten und Qualität konzentrieren.

Die Welt änderte sich, als immer mehr neue Sprachen entwickelt wurden, um einige Punkte, die an der Sprache Java bemängelt wurden, auszugleichen. Die Vielzahl der Sprachen führte teilweise doch zu einem babylonischen Wirrwar (Mir ist bewusst, dass ich mir mit dieser Aussage nicht nur Beifall einfange).

Nachdem einige Zeit eine Art Konsolidierung stattgefunden hatte, wurden aus viele Sprachen, die von der Community ins Rennen geschickt worden sind, einzelne Elemente in die Sprache Java übernommen. Sicherlich nicht so konsequent und mit der Geschwindigkeit, wie es sich der ein oder andere Entwickler wünschte, aber doch schnell genug für einen Wandel und langsam genug, dass die Industrie es verdauen konnte.

Die Gegenwart

Aber nun haben sich einige Dinge geändert. Die gesamte Entwicklung des JDK liegt mittlerweile in der Händen der Community und findet im OpenJDK statt. Immer mehr ehemals kommerzielle Erweiterungen werden von Oracle an die Community übergeben. Und der Release-Zyklus wurde drastisch beschleunigt: Es wird nun alle sechs Monate eine neue JDK-Version geben.

Begonnen hat dieses Verfahren mit der Version 9, Version 11 ist vor kurzem erschienen. In jeder Version sind recht große Änderungen enthalten, angefangen bei der Einführung neuer syntaktischer Elemente bis hin zum Entfernen ganzer Pakete. So ist in JDK 11 zum Beispiel kein Corba mehr enthalten. Viele wird das nicht sonderlich stören, aber bei der Entfernung von JavaFX aus dem JDK war der Aufschrei teils doch recht groß. Vorausgesehen hatten diesen Schritt jedenfalls die wenigsten.

Wir sind nun also an einem Punkt angekommen, an dem sich die Sprache Java und damit auch das JDK in einer sehr hohen Geschwindigkeit verändert. Aber werfen wir noch einmal kurz einen Blick auf die Versionen selbst.

JDK-Versionen und das LTS

Java 8 wird noch recht lange weiterentwickelt werden. Kommerziellen Support gibt es dafür noch bis ca. 2025. Des Weiteren ist das JDK 8 auch das letzte, das ohne dem neuen Modulsystem Jigsaw auskommt. Die JDK-Versionen 9 und 10 verlieren ihren Public Support indes an dem Datum, an dem die jeweils nächste Version final auf den Markt kommt.

Erst die Version 11 ist wieder eine Version, für die Oracle Long Term Support (LTS) anbietet. Danach ist erst wieder mit der Version 17 eine weitere LTS-Version geplant. Doch auch hier sollte man nicht vergessen, dass die frei verfügbaren Versionen nur sechs Monate zum Download bereitstehen sollen. Das LTS bezieht sich lediglich auf den kommerziellen Aspekt.

Dieser Release-Zyklus rief nun auch andere Firmen auf den Plan. Da die gesamte Basisentwicklung auf dem OpenJDK basiert, hat zum Beispiel auch die Firma Azul begonnen, eine angepasste JDK-Version anzubieten, hier allerdings mit einem etwas anderen Release-Plan. Laut Hersteller werden alle Versionen, also auch die Version 9 und 10, von Azul mit Support und Weiterentwicklung versehen.

Ganz neu auf dem Markt ist auch die GraalVM. Diese basiert auf der Sprachversion Java 8. In welchen Abständen welche Sprachversionen unterstützt werden, steht derzeitig noch nicht fest. Wir wissen also noch nicht, ob es eine GraalVM für Java 10 oder 11 geben wird.

Im Ergebnis sind wir also in einer Situation, in der nicht nur eine Menge an verschiedenen JDK-Versionen im Umlauf sind, sondern auch noch verschiedene Anbieter von JDK-Versionen zur Verfügung stehen. Wie kann man damit umgehen?

Die einfache Lösung

Die einfachste Lösung ist es, sich bei einem Projekt für ein JDK zu entscheiden und dieses bis zum Ende der Verwendung beizubehalten. Allerdings kann das nicht bei allen Projekten so gehandhabt werden. Wenn das Projekt beispielsweise frei im Internet zum Einsatz kommt, sind mindestens Sicherheitsupdates notwendig. Doch diese wird es nach Ablauf der freien Support-Phase nicht kostenlos geben.

Ebenfalls recht einfach kann es sein, wenn in Zeitabständen, die für das Projekt verträglich sind, die JDK-Version zu einem defnierten Zeitpunkt geändert wird. Hier handelt man sich allerdings die typischen Migrationsprobleme ein, da dieses Vorgehen dazu verleitet, Updates lediglich in recht großen zeitlichen Abständen durchzuführen.

Continuous Update

Ein anderer Weg kann es sein, dass man bei der Entwicklung ständig am Ball bleibt. Das bedeutet nicht, dass immer das neueste JDK zum Einsatz kommt. Allerdings wird ständig gegen die aktiven JDKs getestet. Beispielsweise könnte die JDK-Version 8 das tatsächlich verwendete Umfeld sein, die Tests würden allerdings schon gegen das aktuelle JDK 11 und die sich noch in der Entwicklung befindlichen JDK-Versionen ausgeführt werden. Die ersten Builds für das JDK 12 gibt es ja auch schon.

Das hat verschiedene Vorteile. Auf diese Art und Weise erfolgt zum Beispiel immer ein Test, ob einzelne Teile in einer der nächsten JDK-Versionen als Deprecated markiert werden. Da mittlerweile solche Elemente auch aus dem JDK entfernt werden, kann man so testen, ob die eigenen Sourcen damit zurecht kommen werden, und natürlich werden damit auch alle Abhängigkeiten diesem Test unterzogen. Wenn auch nur indirekt, erhält man sofort einen Überblick, welche Abhängigkeiten zu Problemen führen werden. Je früher man in einem Projekt diese Information einsetzen kann, desto besser lässt sich damit umgehen.

Lesen Sie auch: Java 11 ist da! Die neuen Features auf einen Blick

Wir wollen uns diese Update-Strategie einmal in einem konkreten Beispiel anschauen. Dabei nehmen wir uns den einfachsten Fall vor. Es soll lediglich darum gehen zu überprüfen, ob eine Anwendung mit einem bestimmten JDK übersetzt und gebaut werden kann. Wir lassen im Beispiel bewusst die Durchführung der jUnit-Tests außen vor.

Docker ist dein Freund

Wenn man sein Projekt mittels verschiedener JDKs übersetzen möchte, müssen alle diese JDKs auf der Plattform zur Verfügung stehen. Meist handelt man sich damit ein wenig manuelle Arbeit ein. Allerdings gibt es aus dem Open-Source-Bereich sehr hilfreiche Werkzeuge, die sich genau mit dieser Problematik auseinandersetzen.

Das Werkzeug, das bei mir zum Einsatz kommt, nennt sich Jabba und ist auf GitHub zu finden. Die Nutzung ist recht einfach: Auf der Kommandozeile kann man das Werkzeug installieren und später auch verwenden. Die genauen Parameter und Möglichkeiten können auf der Webseite zu diesem Projekt nachgelesen werden.

Das Demo-Projekt

Kommen wir zu dem Beispielprojekt, das wir hier in diesem Artikel verwenden wollen. Dieses ist sehr einfach und besteht lediglich aus einem Button. Wird dieser betätigt, wird ein Label erzeugt, in dem die aktuelle Zeitmarke steht und unter dem Button hinzugefügt wird. Als Basis verwenden wir den Meecrowave als Container.

Das Projekt basiert auf Vaadin 8 und befindet sich komplett auf GitHub unter: https://github.com/Nano-Vaadin-Demos/nano-vaadin-meecrowave-V08. Wer Vaadin 10 als Basis verwenden möchte, nimmt: https://github.com/Nano-Vaadin-Demos/nano-vaadin-meecrowave-V10.

Wer nun das Projekt per git clone auf den eigenen Rechner geladen hat, kann dort im Verzeichnis mit einem mvn clean install das Projekt in das lokale Umfeld übersetzen. Voraussetzung hierfür ist natürlich ein installiertes Maven und ein JDK 8.

Wer die Anwendung einmal starten möchte, kann die main-Methode in der Klasse HelloWorld dazu verwenden. Nach weniger als einer Sekunde (auf meinem Laptop 300ms) kann, wenn alle notwendigen Abhängigkeiten lokal vorhanden sind, mittels Browser darauf zugegriffen werden. Da der Port bei jedem Start neu gewählt wird, muss man den aktuell verwendeten Port den Logmeldungen auf der Kommandozeile entnehmen.

Docker-basiertes Kompilieren

Ein lokal lauffähiges Docker vorausgesetzt, kann nun der erste Versuch unternommen werden, den Übersetzungsprozess in einen Docker-Container auszulagern. Das hier verwendete Basis-Image ist auf Docker Hub unter dem Namen maven:3.5-jdk-8 zu finden.

Um den Übersetzungsprozess zu starten, kann man den nachfolgenden Dockerbefehl benutzen:

bash
docker run -it --rm --name compileJDK08 \
-v "$(pwd)":/usr/src/mymaven \
-w /usr/src/mymaven \
maven:3.5-jdk-8 \
mvn clean install

Was hier passiert, ist relativ einfach. Falls das Maven-Image nicht lokal vorhanden ist, wird es aus der offiziellen Docker-Registry geladen. Hier ist es die Version mit Maven und einem JDK 8, was man an dem Tag 3.5-jdk-8 erkennt. Basierend auf dem Image wird ein Container erzeugt und gestartet,  der den Namen compileJDK08 bekommt. Das lokale Verzeichnis wird in dem Container unter /usr/src/mymaven eingebunden und als Arbeitsverzeichnis definiert. Genau an dieser Stelle wird dann innerhalb des Containers ein mvn clean install ausgeführt.

Das Ergebnis ist unter target zu finden, so als hätte man dieses Maven-Kommando lokal ausgeführt. Es kann also nach dem Durchlauf das Ergebnis und die Logfiles überprüft werden.

Wenn man nun dieses Kommando ausführt, wird man alle Aktionen auf der Kommandozeile verfolgen können. Auffällig ist auf jeden Fall, dass alle Abhängigkeiten mittels Maven aus dem Internet geladen werden. Das kann natürlich einige Zeit dauern. Nun kann man auf die Idee kommen, dass dies ja eigentlich nicht notwendig ist, und das lokale Maven Repository ebenfalls mit einbinden. Sicherlich ist das möglich, allerdings nicht immer sinnvoll. Auf dieses Thema kommen wir später noch einmal zu sprechen. In unserem Beispiel werden wir das lokale Repository nicht mit einbinden.

Als nächstes wollen wir den Versuch unternehmen, das Projekt mit einem JDK 10 zu übesetzen. Hierzu muss man lediglich ein anderes Image angeben: anstelle des 3.5-jdk-8 nun ein 3.5-jdk-10:

bash
docker run -it --rm --name compileJDK10 \
-v "$(pwd)":/usr/src/mymaven \
-w /usr/src/mymaven \
maven:3.5-jdk-10 \
mvn clean install

Schon haben wir das Projekt gegen zwei verschiedene JDK-Versionen gebaut, ohne diese auf dem lokalen System vorhalten zu müssen.

Die Anforderungen

Kommen wir zu den Anforderungen. Bisher hatten wir einfach die vorhandenen Docker Images verwendet, die offiziell angeboten werden. Hierbei stellen sich aber gleich mehrere Fragen:

  • Welche Maven-Version wurde verwendet?
  • Welches JDK wurde genutzt?
  • Welche Build-Nummer hat das zum Einsatz kommende JDK?
  • Sind alle Images gleichförmig aufgebaut?
  • Wie kann man den Build-Prozess beschleunigen?
  • Wie schnell stehen neue JDK-Build-Nummern zur Verfügung?

Ich versuche, diese Fragen in ihrer Gesamtheit zu beleuchten. Als erstes muss man wissen, dass die Images nicht alle exakt gleich aufgebaut worden sind. Das bedeutet, dass es schon zu Fehlern gekommen ist, die in den Unterschieden der Images lagen. Das kann einem schon einmal eine sehr nervige Stunde kosten.

Des Weiteren wird wohl immer das OpenJDK verwendet. Allerdings ist das nicht zwingend vorgeschrieben, müsste also bei jedem neuen Image geprüft werden. Demnach ist es auch nicht immer möglich, die gewünschte JDK-Build-Nummer zu erhalten. Der Zeitraum von der Veröffentlichung einer neuen JDK-Version, sei es die Hauptversion oder ein bestimmter Build, ist ebenfalls eher Glückssache.

Alles zusammen bedeutet, dass man sich damit auseinandersetzen sollte, diese Images selber zu bauen. Aus Sicherheitsgründen ist es ohnehin anzuraten, nicht einfach blind den öffentlichen Registries zu vertrauen.

Docker yourself

Wie können wir uns aber selbst diese Images aufbauen? Beginnen wir mit der manuellen Methode. Als Basis nehmen wir in diesem Beispiel das Docker-Image mit dem Namen buildpack-deps:buster-curl. Natürlich kann man auch ein Debian nehmen und curl von Hand installieren.

Die nachfolgenden Schritte sind nun recht geradlinig. Zuerst wird das Maven-Paket heruntergeladen, meist in Form einer Datei mit der Endung tar.gz. Sobald die Datei geladen worden ist, wird diese in das Verzeichnis /usr/share/maven entpackt. Nun fehlt noch der symbolische Link, damit mvn unter /usr/bin/mvn erreichbar ist. Jetzt kann noch ein wenig aufgräumt werden, damit keine unnötigen Dateien in dem Image vorhanden sind.

Alles zusammen in ein Dockerfile verpackt sieht dann wie folgt aus:

Dockerfile
FROM buildpack-deps:buster-curl
MAINTAINER sven.ruppert@gmail.com

ARG MAVEN_VERSION=3.5.3
ARG USER_HOME_DIR="/root"
ARG BASE_URL=http://www-eu.apache.org/dist/maven/maven-3/${MAVEN_VERSION}/binaries

RUN mkdir -p /usr/share/maven /usr/share/maven/ref \
&& curl -o /tmp/apache-maven.tar.gz ${BASE_URL}/apache-maven-${MAVEN_VERSION}-bin.tar.gz \
&& tar -xzf /tmp/apache-maven.tar.gz -C /usr/share/maven --strip-components=1 \
&& rm -f /tmp/apache-maven.tar.gz \
&& ln -s /usr/share/maven/bin/mvn /usr/bin/mvn
ENV MAVEN_HOME /usr/share/maven
ENV MAVEN_CONFIG "$USER_HOME_DIR/.m2"
CMD ["mvn"]

Dieses Image ist die Grundlage für die jeweiligen JDK-Images. Es muss demnach nur noch basierend auf dem Image ein JDK installiert werden. Dazu wird das oben erwähnte Open-Source-Werkzeug Jabba verwendet. Wenn das JDK installiert ist, werden lediglich alle benötigten Pfade und Umgebungsvariablen gesetzt, und fertig sind wir:

Dockerfile
FROM svenruppert/maven-3.5-no-jdk:maven-3.5.3
MAINTAINER sven.ruppert@gmail.com

ARG USER_HOME_DIR="/root"

RUN curl -sL https://github.com/shyiko/jabba/raw/master/install.sh | \
JABBA_COMMAND="install adopt@1.8.172-11 -o /jdk" bash

ENV JAVA_HOME /jdk
ENV PATH $JAVA_HOME/bin:$PATH
ENV MAVEN_HOME /usr/share/maven
ENV MAVEN_CONFIG "$USER_HOME_DIR/.m2"

RUN java -version
RUN mvn -version

CMD ["mvn"]

Damit haben wir die volle Kontrolle darüber, welche Maven-Version und welches JDK zum Einsatz kommen wird. Ebenfalls hilfreich ist dieses Vorgehen, um auch später noch JDKs zur Verfügung zu haben, die zum Beispiel nicht mehr zum Download angeboten werden. Als Beispiel sei hier das Oracle JDK 9 genannt, das genau an dem Tag nicht mehr zum Download zur Verfügung stand, als das JDK 10 final veröffentlicht wurde.

Selbstverständlich kann man sich auch genauso die Basisimages für die Runtimes zusammenstellen. Hier wurde schon eine Menge Vorarbeit geleistet, ein Beispiel ist unter der URL https://github.com/vaadin-developer/vaadin-dev-environment auf GitHub zu finden. In dem Verzeich images befinden sich die jeweiliegen Dockerfiles. Ebenfalls auf Docker Hub bereit stehen die Images  unter der Organisation svenruppert.

Damit ändert sich der Aufruf lediglich um die Angabe des Images. In folgendem Beispiel wird das JDK 10 von Azul verwendet:

bash
docker run -it --rm --name compileJDK10 \
-v "$(pwd)":/usr/src/mymaven \
-w /usr/src/mymaven \
svenruppert/maven-3.5-jdk-zulu-10 \
mvn clean install

Möchte man eine bestimmte JDK-Build-Nummer verwenden, so erweitert man die Angabe des Images lediglich um die Build-Nummer, wenn dafür ein Image erstellt wurde und es dementsprechend mit einem Tag versehen worden ist. In dem nachfolgenden Fall ist es die Buildnummer 1.10.0-1 des Zulu JDKs:

bash
docker run -it --rm --name compileJDK10 \
-v "$(pwd)":/usr/src/mymaven \
-w /usr/src/mymaven \
svenruppert/maven-3.5-jdk-zulu-10:1.10.0-1 \
mvn clean install

Fazit

Wir sind nun in der Lage, das Projekt manuell gegen eine Matrix von JDK-Versionen und -Herstellern zu testen. Bisher haben wir lediglich ein einfaches mvn clean install durchgeführt. Da wir jedoch auch Webanwendungen testen möchten, kommen wir um ein wenig weitere Infrastruktur nicht herum. Ebenfalls ist der Build-Prozess noch nicht schnell genug, da Maven uns immer wieder mit langen Downloadzeiten außer Gefecht setzt.

Diese Punkte wollen wir in den nächsten Teilen dieser Serie angehen. Für die ungeduldigen Leser sei hier schon einmal auf die Sourcen auf GitHub verwiesen: https://github.com/vaadin-developer/vaadin-dev-environment. Wer Fragen und Anmerkungen hat, kann sich gerne per Twitter (@SvenRuppert) oder direkt per Mail an mich wenden.

Happy Coding!

Lesen Sie auch:

Java 11, LTS und der ganze Rest: Bleibt Java kostenlos?

Verwandte Themen:

Geschrieben von
Sven Ruppert
Sven Ruppert
Sven Ruppert arbeitet seit 1996 mit Java und ist Developer Advocate bei Vaadin. In seiner Freizeit spricht er auf internationalen und nationalen Konferenzen, schreibt für IT-Magazine und für Tech-Portale. Twitter: @SvenRuppert
Kommentare

Hinterlasse einen Kommentar

Hinterlasse den ersten Kommentar!

avatar
400
  Subscribe  
Benachrichtige mich zu: