Von Java 8 migrieren: welcher Aufwand anfällt und was zuerst drankommt
Die Anwendung soll zwischen den Schritten lauffähig bleiben. Das geht, wenn du das Build-Werkzeug zuerst anhebst, die Bibliotheken in Wellen nachziehst und den Wechsel von javax auf jakarta als eigenen, klar abgegrenzten Schnitt behandelst.
KI-generiertDieses Bild wurde mit KI erzeugt · Yves Hoppe / KI / cmt
Alles auf einmal umstellen und dann monatelang nichts ausliefern können
Der übliche erste Anlauf sieht so aus: Ein Branch, darin wird die Java-Version in der Build-Datei hochgesetzt, danach werden alle Fehler abgearbeitet, die daraufhin erscheinen. Nach zwei Wochen übersetzt das Projekt wieder, nach sechs Wochen laufen die Tests, und in der Zwischenzeit hat der Hauptzweig sich so weit weiterbewegt, dass das Zusammenführen ein eigenes Projekt wird. Am Ende steht ein sehr großer Umbau, den niemand mehr sinnvoll prüfen kann, und ein Team, das ihn ungern ein zweites Mal anfasst.
Der Grund dafür ist, dass drei unabhängige Umbauten in einem Schritt zusammenfallen. Der erste betrifft die Werkzeuge: Ältere Maven-Plugins und Gradle-Fassungen laufen auf neueren JDKs nicht, und ihre Aktualisierung ändert wiederum Verhalten im Build. Der zweite betrifft die Bibliotheken: Praktisch jede Abhängigkeit braucht eine neuere Fassung, und einige davon haben zwischen ihren Versionen fachliche Änderungen. Der dritte betrifft den Namensraum: Wenn eure Anwendung Servlet, JPA, JAX-RS, Bean Validation oder CDI benutzt, heißen diese Pakete ab Jakarta EE 9 nicht mehr javax, sondern jakarta.
Jeder dieser drei Umbauten lässt sich einzeln durchführen, prüfen und ausliefern. Zusammengeworfen ergeben sie ein Bündel, bei dem nach einem Testfehler niemand mehr sagen kann, welcher der drei ihn verursacht hat. Genau daran scheitern die meisten Anläufe, und nicht an fehlendem Wissen über die neue Java-Version.
KI-generiertDieses Bild wurde mit KI erzeugt · Yves Hoppe / KI / cmt
Der Ablauf in sechs Schritten
Jeder Schritt endet mit einem Zustand, der übersetzt, testet und ausgeliefert werden kann. Das ist die eigentliche Absicht hinter dieser Reihenfolge: Ihr könnt jederzeit anhalten, etwas anderes dazwischenschieben und später weitermachen, ohne einen halbfertigen Umbau zu verwalten.
- 1
Das Build-Werkzeug anheben, ohne den Code anzufassen
Zuerst kommt der Unterbau dran, und zwar noch auf Java 8. Bei Gradle heißt das, den Wrapper auf eine Fassung zu ziehen, die später auch auf der Ziel-Version laufen kann: Für Java 17 braucht ihr mindestens Gradle 7.3, für Java 21 mindestens 8.5, für Java 25 mindestens 9.1.0. Bei Maven bedeutet es, die Plugins für Compiler, Surefire, Shade und Assembly auf aktuelle Fassungen zu heben. Diese Aktualisierung ändert erfahrungsgemäß schon Verhalten im Build, etwa bei der Testerkennung, und genau deshalb gehört sie in einen eigenen Schritt.
Geschafft, wenn: Der Build läuft mit neuen Werkzeugen und altem Code durch, die Auslieferung ist unverändert.
- 2
Auf einem neuen JDK bauen und weiter für Java 8 übersetzen
Jetzt läuft der Build selbst auf dem Ziel-JDK, das erzeugte Ergebnis bleibt aber für Java 8 gültig. In Maven setzt du dafür maven.compiler.release, in Gradle die release-Eigenschaft der Compile-Aufgabe oder eine Toolchain. Der Unterschied zu den alten Optionen für Quell- und Zielversion ist wichtig: --release prüft zusätzlich, dass ihr nur APIs benutzt, die es in Java 8 schon gab. Damit fangt ihr das versehentliche Abrutschen in neuere Methoden ab, das später zu Fehlern erst zur Laufzeit führt.
Geschafft, wenn: Der CI-Server läuft auf dem neuen JDK, das Artefakt ist weiterhin auf Java 8 lauffähig.
- 3
Fehlende Module wieder einhängen
Beim ersten Lauf gegen ein JDK ab 11 fehlen die Klassen, die mit den Java-EE- und CORBA-Modulen entfernt wurden. Betroffen sind vor allem JAXB, JAX-WS, die Activation Framework und die Common Annotations. Nimm sie als reguläre Abhängigkeiten in den Build auf, und zwar in der jakarta-Fassung, wenn ihr ohnehin dorthin wollt, sonst zunächst in der javax-Fassung. Parallel dazu arbeitest du die Liste aus jdeps ab: Für jeden Zugriff auf ein internes JDK-Paket gibt es entweder eine neuere Bibliotheksversion oder eine Startoption, die den Zugriff wieder öffnet. Die Bibliotheksversion ist immer die bessere Antwort, weil die Startoption bei jedem weiteren Sprung erneut zur Debatte steht.
Geschafft, wenn: Der Übersetzungslauf ist frei von fehlenden Klassen, die Startoptionen sind dokumentiert statt verstreut.
- 4
Bibliotheken in Wellen hochziehen
Jetzt kommen die Abhängigkeiten dran, und zwar in Gruppen statt einzeln oder alle auf einmal. Eine sinnvolle Gruppierung ist: erst das Test-Framework samt Erweiterungen, dann Logging, dann Serialisierung, dann Persistenz, dann das Web-Framework. Jede Welle ist ein eigener Pull Request mit eigenem Testlauf. Wo eine Bibliothek eine fachliche Änderung mitbringt, siehst du das in einem überschaubaren Umfang und nicht als eine von zweihundert Meldungen. Für wiederkehrende Anpassungen lohnt sich OpenRewrite, dessen Rezepte für den Weg auf Java 17, 21 und 25 die üblichen API-Ersetzungen automatisch vornehmen und sich vorher als reiner Trockenlauf ansehen lassen.
Geschafft, wenn: Alle Abhängigkeiten sind auf Versionen, die auf der Ziel-Version freigegeben sind, jede Welle ist einzeln ausgeliefert.
- 5
javax auf jakarta umstellen, in einem Schnitt
Dieser Schritt ist der einzige, der sich nicht in Wellen teilen lässt. Jakarta EE 9 hat den Namensraum vollständig gewechselt, und Java-EE- und Jakarta-EE-APIs lassen sich in einem Projekt nicht mischen. Euer Code, alle Abhängigkeiten und der Application Server müssen gemeinsam auf die jakarta-Seite. Nimm dafür ein Werkzeug, das den Unterschied zwischen JDK-Paketen und Jakarta-Paketen kennt, etwa das entsprechende OpenRewrite-Rezept oder das Migrationswerkzeug aus dem Tomcat-Projekt, das auch fertige Archive umschreiben kann. Eine Textersetzung über alle javax-Vorkommen bricht zuverlässig an javax.sql, javax.crypto und javax.naming, die nie umgezogen sind.
Geschafft, wenn: Die Anwendung läuft auf einem Application Server oder einem Framework in der jakarta-Fassung, der Namensraum ist im ganzen Projekt einheitlich.
- 6
Das Ziel anheben und den Betrieb nachziehen
Erst jetzt wird release im Build von 8 auf die Ziel-Version gestellt. Der Code darf ab hier neuere Sprachmittel benutzen, muss es aber nicht. Wichtiger ist der Betrieb: Prüft die Startoptionen, weil einige aus der Java-8-Zeit nicht mehr existieren, stellt die Heap-Konfiguration in Containern auf die prozentualen Optionen um und schaut euch die Werte für den Garbage Collector neu an, denn seit JDK 9 ist G1 die Vorgabe und nicht mehr der Parallel Collector. Lasst die Anwendung eine Weile unter echter Last laufen, bevor die alte Umgebung abgebaut wird.
Geschafft, wenn: Die Anwendung übersetzt und läuft auf der Ziel-Version, die Startoptionen und Speicherwerte sind an die neue Laufzeitumgebung angepasst.
Die Reihenfolge, in der nichts stehen bleibt
- 01 Build und Plugins anheben, während der Code noch auf Java 8 übersetzt wird.
- 02 Auf einem neuen JDK bauen, das Ziel aber über --release weiter auf 8 halten.
- 03 Bibliotheken in Wellen hochziehen, jede Welle als eigene Auslieferung.
- 04 Erst danach javax auf jakarta umstellen, in einem Schnitt und mit Werkzeug.
- 05 Zuletzt das Ziel im Build anheben und den Betrieb auf die neuen Flags einstellen.
Woran du den Aufwand vorher abschätzt
Bevor der erste Schritt startet, lohnen sich fünf Messungen. Sie kosten zusammen etwa einen Tag und verwandeln eine Schätzung aus dem Bauch in eine Zahl, die du im Fahrplan vertreten kannst.
Zählen, ob überhaupt Jakarta-EE-Pakete im Spiel sind
Such im Quelltext nach Importen aus javax.servlet, javax.persistence, javax.ws.rs, javax.validation, javax.enterprise und javax.jms. Findest du nichts davon, entfällt der teuerste Einzelschritt komplett. Wichtig ist die Abgrenzung: javax.swing, javax.sql, javax.crypto, javax.naming und javax.xml.parsers gehören zum JDK und sind nie umgezogen. Eine blinde Textersetzung über alle javax-Vorkommen zerstört genau diese Stellen.
Die Abhängigkeiten auf entfernte Module prüfen
Mit JDK 11 sind die Java-EE- und CORBA-Module aus dem JDK entfernt worden, darunter JAXB, JAX-WS, die JavaBeans Activation Framework und die Common Annotations. Wer sie weiter braucht, nimmt sie als gewöhnliche Abhängigkeit in den Build auf. Das ist keine große Sache, aber es ist der häufigste Grund dafür, dass ein Projekt beim ersten Übersetzungsversuch mit fehlenden Klassen abbricht.
Die Zugriffe auf JDK-Interna sichtbar machen
jdeps liegt im JDK und zeigt dir, welche Pakete eures Codes und eurer Abhängigkeiten in interne JDK-Bereiche greifen. Seit JDK 16 sind diese Bereiche stark gekapselt, ein Zugriff braucht seither ausdrückliche Startoptionen oder eine neuere Bibliotheksversion. Die Liste aus jdeps ist die Arbeitsliste für diesen Teil, und sie ist meist kürzer, als man befürchtet.
Den Bestand an veralteten APIs erheben
jdeprscan prüft den fertigen Bytecode gegen die Liste der als veraltet markierten APIs einer Ziel-Version. Der Aufruf gegen ein Auslieferungsartefakt braucht Sekunden und liefert eine Liste, die sich nach Aufwand sortieren lässt. Das ist deutlich verlässlicher als eine Suche im Quelltext, weil auch die Abhängigkeiten mit erfasst werden.
Die Werkzeuge im Betrieb abfragen, bevor ein Datum zugesagt wird
Application Server, APM-Agenten, Profiler und Container-Basisabbilder haben eigene Freigabelisten für Java-Versionen. Diese Kette bestimmt regelmäßig das Tempo der ganzen Migration. Frag sie ab, solange der Fahrplan noch verhandelbar ist, und plan die Aktualisierung der Agenten als eigenen Schritt vor der Laufzeitumstellung.
KI-generiertDieses Bild wurde mit KI erzeugt · Yves Hoppe / KI / cmt
Warum der Namensraumwechsel kein Suchen und Ersetzen ist
Als Oracle die Verwaltung von Java EE an die Eclipse Foundation übergeben hat, blieb die Marke javax bei Oracle. Die Spezifikationen mussten deshalb umziehen, und Jakarta EE 9 hat diesen Umzug in einem Schritt vollzogen: Alle Pakete der Plattform heißen seither jakarta statt javax, und sonst hat sich in dieser Version bewusst nichts geändert. Der Zuschnitt war Absicht, damit Werkzeughersteller, Anbieter von Application Servern und Anwendungsteams genau eine Sache gleichzeitig anfassen mussten.
Die Falle liegt darin, dass javax nicht gleich javax ist. Umgezogen sind nur die Pakete der Enterprise-Plattform, also Servlet, JPA, JAX-RS, CDI, Bean Validation, JMS, JAXB und ihre Verwandten. Im JDK selbst gibt es weiterhin javax.swing für Oberflächen, javax.sql für JDBC-Datenquellen, javax.crypto für Verschlüsselung, javax.naming für JNDI und javax.xml.parsers für XML. Ein reguläres Suchen und Ersetzen über den Quelltext trifft diese Pakete mit und erzeugt Fehler, die erst beim Übersetzen und teilweise erst zur Laufzeit auffallen.
Deshalb gehört dieser Schritt in ein Werkzeug, das den Unterschied kennt. OpenRewrite hat dafür ein eigenes Rezept, das nicht nur Importe umschreibt, sondern auch Abhängigkeiten in Maven und Gradle auf die passenden jakarta-Artefakte umstellt. Das Tomcat-Projekt liefert ein Migrationswerkzeug, das fertige JAR- und WAR-Dateien umschreibt, was hilft, wenn eine Abhängigkeit keine jakarta-Fassung hat und ihr die Quellen nicht ändern könnt. Beide Wege sind besser als jede Regel, die jemand von Hand aufschreibt.
Was mit Java EE, Jakarta EE und Spring zusammenhängt und was nicht
Für Projekte auf Spring stellt sich die Frage in einer anderen Form. Spring Boot 3.0 hat die Mindestversion auf Java 17 gehoben und benutzt die Jakarta-EE-9-APIs, also den jakarta-Namensraum. Es gibt keinen Weg, der auf Java 8 oder 11 bleibt und trotzdem nach Spring Boot 3 führt. Wer aus Spring Boot 2.7 kommt, muss deshalb beide Schnitte machen, und die Reihenfolge oben gilt genauso: erst Werkzeuge, dann Bibliotheken, dann der Namensraum, dann die Laufzeitversion.
Für die aktuelle Generation liegt die Latte noch etwas anders. Spring Boot 4.0 ist im November 2025 erschienen, setzt auf Spring Framework 7 auf und zielt auf Jakarta EE 11. Als Mindestversion bleibt Java 17 bestehen. Die freie Unterstützung für Spring Boot 3.5 ist im Juni 2026 ausgelaufen, kommerzieller Support für diese Reihe läuft nach dem veröffentlichten Zeitplan deutlich länger. Wer heute migriert, sollte deshalb prüfen, ob das Ziel wirklich die 3er-Reihe sein soll oder gleich die 4er.
Auf der Jakarta-EE-Seite sieht der Stand so aus: Jakarta EE 11 ist 2025 erschienen und setzt Java SE 17 als Mindestversion voraus, mit ausdrücklicher Unterstützung für Java 21 und dessen Nebenläufigkeitsmerkmale. Wer heute von Java EE 8 kommt, hat damit einen klaren Zielpunkt, muss aber den Sprung über den Namensraum von Jakarta EE 9 nehmen, weil es daran vorbei keinen Weg gibt.
Die Stellen, an denen die Anwendung nach der Migration anders läuft
Der erste Kandidat ist die Zeichenkodierung. Seit JDK 18 ist UTF-8 die vorgegebene Kodierung der Standard-APIs. Vorher galt stillschweigend die Kodierung des Betriebssystems, und unter Windows war das häufig nicht UTF-8. Anwendungen, die Dateien ohne ausdrückliche Kodierung lesen oder schreiben, verhalten sich danach anders, und zwar meist erst bei Umlauten im Produktivbestand. Der Schutz davor ist einfach: Überall dort, wo ein Reader, Writer oder eine Umwandlung zwischen Bytes und Zeichenketten steht, die Kodierung ausdrücklich angeben.
Der zweite ist der Garbage Collector. Java 8 hat im Regelfall den Parallel Collector benutzt, seit Java 9 ist G1 die Vorgabe. G1 optimiert auf kurze Pausen statt auf maximalen Durchsatz, was für die meisten Anwendungen die bessere Wahl ist, sich in Messwerten aber deutlich zeigt. Wer feste Heap-Größen aus der Java-8-Zeit übernimmt und dazu Optionen setzt, die sich auf den alten Collector beziehen, bekommt ein Verhalten, das niemand beabsichtigt hat. Nimm die Gelegenheit und setz die Speicherkonfiguration neu auf, statt sie zu übertragen.
Der dritte betrifft den Betrieb in Containern. Ein JDK ab Version 10 erkennt die Speichergrenze des Containers von selbst und leitet daraus die maximale Heap-Größe ab, standardmäßig ein Viertel des verfügbaren Speichers. Wer aus der Java-8-Zeit eine feste Heap-Größe mitschleppt, verschenkt entweder Speicher oder überzieht die Grenze und wird vom Container-Betriebssystem abgeräumt. Die prozentualen Optionen sind hier die richtige Antwort, weil sie sich mit der Container-Konfiguration mitbewegen.
Wie lange das Ganze dauert und wovon das abhängt
Eine belastbare Aussage lässt sich erst nach der Bestandsaufnahme treffen, aber die Treiber sind immer dieselben drei. Der erste ist die Frage, ob Jakarta-EE-APIs im Spiel sind. Eine Anwendung ohne Servlet, JPA und CDI überspringt den teuersten Schritt komplett. Der zweite ist die Anzahl der Abhängigkeiten, die keine gepflegte neuere Fassung mehr haben. Für jede davon braucht ihr eine Entscheidung: ersetzen, selbst pflegen oder das Artefakt mit einem Werkzeug umschreiben. Der dritte ist der Testbestand, denn ohne Tests ist die Migration kein Umbau, sondern ein Versuch.
Was den Aufwand verlässlich in die Höhe treibt, ist der Versuch, die Migration mit einer fachlichen Änderung zu verbinden. Das klingt sparsam und ist es nie: Sobald ein Testfehler auftaucht, gibt es zwei mögliche Ursachen statt einer, und die Suche danach kostet mehr als der zweite Durchlauf gekostet hätte. Halte die Migration frei von Fachlichkeit, auch wenn eine Stelle im Code offensichtlich verbesserungswürdig ist. Schreib sie auf und mach sie danach.
Was den Aufwand senkt, ist ein CI-Server, der beide Versionen parallel baut. Solange ihr auf Java 8 ausliefert, kann ein zweiter Lauf gegen das Ziel-JDK bereits die Fehler zeigen, die euch später erwarten, ohne dass jemand darauf reagieren muss. Dieser Lauf darf rot sein. Sein Wert liegt darin, dass die Liste der offenen Punkte jeden Tag aktuell ist statt einmal am Anfang erhoben und danach veraltet.
Dazu passende Kurse
Wenn das Wissen über die alte Anwendung im Team ungleich verteilt ist, holen Java-Trainings für Bestandsanwendungen alle auf denselben Stand, bevor der erste Umbau startet.
Soll die Anwendung nach der Migration in Containern laufen, findest du bei cmt Kurse für den Weg in den Container-Betrieb .
Wie sicher bist du beim Thema wirklich?
Lesen fühlt sich schnell nach Können an. Ein kurzer Test zeigt dir, was davon schon sitzt und wo sich ein Kurs lohnt. Kostenlos, ohne Anmeldung, mit einer Erklärung zu jeder Antwort.
Wo du genau das übst
Sehr umfangreicher Überblick über Zusammenhänge und Anwendungsbereiche. Sehr zu empfehlen.
Effektives, gemeinsames Erarbeiten der Aufgaben mit vielen praktischen Tipps.
Sehr guter Trainer und gute Aufbereitung der Inhalte und auch eingehen auf Fragen der Beteiligten.
Häufige Fragen
Müssen wir wirklich über Jakarta EE 9 gehen, oder können wir das überspringen?
Was machen wir mit einer Bibliothek, von der es keine jakarta-Fassung gibt?
Können wir die Migration Modul für Modul machen?
Lohnt sich ein automatisches Migrationswerkzeug oder machen wir es besser von Hand?
Sollen wir gleich auf Java 25 gehen oder erst auf 17?
Deine Ansprechpartner
Du bist dir nicht sicher, welcher Kurs oder welches Level zu dir passt? Wir beraten dich persönlich und kostenlos.
Yves Hoppe
Weiterbildung & Beratung
Hilft dir, aus dem Programmierung-Programm den passenden Kurs für deinen Stand zu finden.
Norbert Jansen
Beratung & Inhouse
Plant mit dir Inhouse-Trainings, die auf eure Abläufe und euren Datenbestand zugeschnitten sind.
Den Umbau an einer echten Anwendung üben
Bei cmt gehst du im Kurs zur Ablösung alter Java-EE-Anwendungen genau diese Schritte an einem laufenden Beispiel durch, vom Build über die Bibliotheken bis zum Namensraum.
Passt dazu
- Java 17, 21 oder 25: welche LTS-Version euer Projekt tragen sollte
- Oracle JDK oder OpenJDK: welche Java-Distribution ihr einsetzen dürft
- Spring Boot oder Quarkus: was Startzeit und Speicher im Betrieb kosten
- Maven oder Gradle: welches Build-Werkzeug euer Java-Projekt braucht