Der Spickzettel für die Umsetzung: was beim Übertragen einer Jakarta-EE-Anwendung auf Quarkus und in den Container an welcher Stelle nachzusehen ist. Die Entscheidungen davor — Weg, Station, Pilot, Koexistenz — stehen auf dem ersten Blatt: Jakarta-EE-Modernisierung planen. Maßgeblich ist die Quarkus-Dokumentation; hier steht die Auswahl, die beim Umstieg vom Application Server tatsächlich gebraucht wird.
Application Server im Container
wildfly-glow scan ./zaehlwerk.war
wildfly-glow provision ./zaehlwerk.war \
--provision DOCKER_IMAGE
Der Scan meldet auch, welche Fähigkeit er im Deployment gefunden, aber keinem Layer zugeordnet hat. Wer den Zustand nicht herausnimmt, betreibt einen Container, der sich wie ein Server verhält:
| Zustand | Heute | Im Container |
|---|---|---|
| Sitzung | Serverspeicher | Token oder externer Speicher |
| Hochgeladene Datei | Serververzeichnis | Objektspeicher |
| Timer | Serverinstanz | eine Instanz oder externe Steuerung |
| Cache | Serverspeicher | akzeptiert kalt oder externer Cache |
Modul: Moderne Application Server im Container
Entwicklungsschleife in Quarkus
| Werkzeug | Aufruf | Aufgabe |
|---|---|---|
| Entwicklungsmodus | quarkus dev | Live Reload beim nächsten Aufruf |
| Fortlaufende Tests | im Dev-Modus, Taste r | betroffene Tests je Änderung |
| Dev UI | /q/dev-ui | Konfiguration und Erweiterungen einsehen |
| Dev Services | automatisch | fehlende Infrastruktur als Container |
Dev Services starten nur, solange die zugehörige Konfiguration fehlt — gesetzte Werte schalten sie ab. Im Migrationsprojekt ist die Dev UI das nützlichste Stück: Sie zeigt, welche Konfiguration wirklich greift, wenn ein übertragenes Modul sich anders verhält als im Server.
Modul: Quarkus als Modernisierungsoption
Health-Prüfungen und Rollout
| Prüfung | Beantwortet | Folge bei Fehlschlag |
|---|---|---|
| Startup | Ist der Start abgeschlossen | mehr Zeit, dann Neustart |
| Liveness | Läuft der Prozess noch sinnvoll | Neustart des Containers |
| Readiness | Kann er jetzt Anfragen annehmen | kein Verkehr, kein Neustart |
Eine Liveness-Prüfung, die die Datenbank abfragt, startet den Container neu,
wenn die Datenbank hakt. Die Pfade liegen unter q/health und sind einzeln
abrufbar. Ein Rolling Update setzt voraus, dass zwei Versionen gleichzeitig
laufen können — das Datenmodell muss beide vertragen, und der Rückweg besteht
aus einem alten Image, nicht aus einem neuen Build.
Module: Moderne Application Server im Container · Containerisierung und Plattformbetrieb
Jakarta REST in Quarkus
| Erweiterung | Zweck | Hinweis |
|---|---|---|
| quarkus-rest | Jakarta REST | Basis jeder Ressource |
| quarkus-rest-jackson | JSON über Jackson | eine JSON-Bindung wählen |
| quarkus-rest-jsonb | JSON über JSON-B | näher am bisherigen Verhalten |
| quarkus-rest-client | Aufrufe nach außen | typisierte Client-Schnittstellen |
Wer bisher JSON-B nutzte, bleibt zunächst dabei — der Wechsel auf Jackson ändert Ausgaben.
@Path("/ableseauftraege")
@Produces(MediaType.APPLICATION_JSON)
public class AbleseauftragResource {
@Inject AbleseauftragService service;
@GET @Path("/{id}")
public Ableseauftrag lesen(@RestPath long id) {
return service.finden(id);
}
}
@ServerExceptionMapper
public Response zuFehler(ZaehlerUnbekannt e) {
return Response.status(404)
.entity(new Fehler("zaehler_unbekannt")).build();
}
@RegisterRestClient(configKey = "abrechnung")
public interface AbrechnungClient {
@POST Buchung buchen(Buchungswunsch w);
}
Die Client-Adresse steht als quarkus.rest-client.abrechnung.url in der
Konfiguration, nicht im Code. Die Fehlerabbildung kommt zuerst dran: Aufrufer
werten Fehlerschlüssel aus, und der Application Server hat unbehandelte Fehler
bisher oft in eigene Seiten verwandelt. Das Schema liegt unter /q/openapi, die
Oberfläche unter /q/swagger-ui.
Modul: Jakarta REST und HTTP-Schnittstellen migrieren
Wo Antworten abweichen
| Stelle | Typische Abweichung | Wirkung beim Aufrufer |
|---|---|---|
| Feldnamen | Groß- und Kleinschreibung | Feld wird nicht gefunden |
| Nullwerte | Feld fehlt statt null | Auswertung bricht ab |
| Datum | Zeitzone oder Format | falscher Abrechnungstag |
| Statuscode | 500 statt 404 | Wiederholung statt Fehlermeldung |
Jede dieser Abweichungen ist ein Befund, keine Geschmacksfrage. Gesondert zu prüfen ist, was bisher der Server beisteuerte: Zeichenkodierung, Standardsprache, Fehlerseiten, Größenbegrenzungen, Zeitlimits — und der Kontextpfad, der alle absoluten Adressen ändert.
Modul: Jakarta REST und HTTP-Schnittstellen migrieren
CDI-Scopes und Bean-Ermittlung
| Scope | Erzeugung | Hinweis |
|---|---|---|
| ApplicationScoped | beim ersten Methodenaufruf | Standard für zustandslose Dienste |
| Singleton | beim Injizieren | kein Proxy, kein spätes Erzeugen |
| RequestScoped | je Anfrage | Kontext muss aktiv sein |
| Dependent | je Injektionsstelle | Lebensdauer des Nutzers |
SessionScoped steht nur mit der Undertow-Erweiterung zur Verfügung — nicht
selbstverständlich wie im Server. Die Bean-Ermittlung passiert im Build:
Unbenutzte Beans werden entfernt, was per Reflection gesuchte Klassen trifft;
wer eine Bean erhalten muss, markiert sie als @Unremovable.
Modul: CDI, EJB und Transaktionen modernisieren
EJB-Funktionen ersetzen
@ApplicationScoped
public class AbleselaufStarter {
@Inject AbleseauftragService service;
@Scheduled(cron = "{zaehlwerk.ablesung.cron}")
@Transactional
void naechtlicherLauf() {
service.faelligeAuftraegeAnlegen();
}
}
Für geclusterten Betrieb, in dem nur eine Instanz laufen darf, ist die Quartz-Erweiterung vorgesehen — sonst läuft der Timer in jeder Instanz. Ohne automatische Entsprechung bleiben der Nebenläufigkeitsschutz einer Singleton Bean und der Sicherheitskontext des Servers, der jetzt aus dem Token kommt.
Modul: CDI, EJB und Transaktionen modernisieren
Transaktionsgrenzen
| Typ | Verhalten | Typischer Einsatz |
|---|---|---|
| REQUIRED | nutzt vorhandene, sonst neue | Standardfall |
| REQUIRES_NEW | immer neue, alte ruht | Protokolleintrag trotz Rollback |
| MANDATORY | Fehler ohne laufende | interner Baustein |
| NEVER | Fehler mit laufender | reiner Lesepfad |
Das Standard-Zeitlimit liegt bei 60 Sekunden, konfigurierbar über
quarkus.transaction-manager. Programmatische Steuerung bietet die
QuarkusTransaction-API statt UserTransaction. Verteilte Transaktionen über
mehrere Ressourcen werden nicht unterstützt, und ein Selbstaufruf innerhalb der
Klasse umgeht die Annotation.
Modul: CDI, EJB und Transaktionen modernisieren
Persistence: wo die Einstellungen jetzt stehen
| Einstellung | Bisher | Jetzt |
|---|---|---|
| Zugangsdaten | Serverkonfiguration | Umgebungsvariable oder Secret |
| Poolgröße | Serverkonsole | quarkus.datasource.jdbc.max-size |
| Dialekt | persistence.xml | aus db-kind und db-version |
| Schemaverhalten | persistence.xml | schema-management.strategy |
quarkus.datasource.db-kind=postgresql
quarkus.datasource.jdbc.url=jdbc:postgresql://db:5432/zaehlwerk
quarkus.datasource.jdbc.max-size=16
quarkus.hibernate-orm.schema-management.strategy=validate
Poolgröße mal Instanzenzahl ergibt die Last auf der Datenbank — der Wert eines
einzelnen Servers passt nicht mehr. Für die Schemastrategie gilt:
drop-and-create in Tests, validate in Abnahme und Produktion, update
nirgends produktiv, none für Sonderfälle. Während der Koexistenz bleibt das
Datenmodell unverändert, sonst ist der Vergleich alt gegen neu wertlos.
Modul: Persistence und Datenzugriff
Messaging: Bausteine und Kanäle
| Bisher | Bedeutung | Danach |
|---|---|---|
| Queue | ein Verbraucher je Nachricht | Kanal auf Broker-Ziel |
| Topic | mehrere Verbraucher | Kanal mit Broadcast |
| Message-Driven Bean | Verbraucher im Server | Methode mit Incoming |
| Bestätigung in Transaktion | alles oder nichts | ausdrückliche Strategie |
Die letzte Zeile ist die schwierigste — die selbstverständliche Klammer des
Servers fällt weg. Die Zustellgarantie gehört deshalb zuerst geklärt, vor der
Wahl der Erweiterung: quarkus-messaging-kafka (Ereignisströme),
-amqp (klassische Ziele, Ablösung von JMS), -rabbitmq (Routing über
Exchanges), -pulsar (Ströme mit Mandantentrennung).
@Incoming("ablesung-eingang")
@Blocking
@Transactional
public void empfangen(AblesungMeldung meldung) {
service.uebernehmen(meldung);
}
Der Kanal wird über mp.messaging.incoming.ablesung-eingang mit Connector und
Ziel verbunden. Ohne @Blocking läuft synchroner Code auf dem Event Loop und
bremst alle anderen Kanäle.
Modul: Messaging und asynchrone Integration
Sicherheit: vom Realm zum Token
| Ort | Inhalt | Nach der Migration |
|---|---|---|
| Realm des Servers | Benutzer und Rollen | Identity Provider |
| web.xml | Security Constraints | Annotationen an Methoden |
| Annotationen | RolesAllowed | bleiben unverändert |
| Serverkonsole | Rollenzuordnung | Anspruch im Token |
Die dritte Zeile ist der Grund, warum diese Migration weniger Code kostet als befürchtet.
quarkus.oidc.auth-server-url=https://login.example/realms/netz
quarkus.oidc.client-id=zaehlwerk-ablesung
quarkus.oidc.application-type=service
quarkus.oidc.roles.role-claim-path=realm_access/roles
Drei Einstellungen stehen beim Umstieg häufig falsch: application-type
(falscher Anmeldefluss, erkennbar an einer Weiterleitung statt 401),
role-claim-path (keine Rollen, erkennbar an 403 trotz gültigem Token — der
häufigste Fehler) und die Zielgruppe des Tokens (fremde Token werden
akzeptiert, weil die Prüfung im Test fehlt).
Modul: Authentifizierung und Autorisierung
Tests: Mittel und Reihenfolge
| Mittel | Zweck | Hinweis |
|---|---|---|
| QuarkusTest | Anwendung im Test starten | schnell, im selben Prozess |
| QuarkusIntegrationTest | gebautes Artefakt prüfen | auch nativ und im Container |
| TestTransaction | Änderungen zurückrollen | hält die Testdatenbank sauber |
| InjectMock | Bean durch Mock ersetzen | nur wo echte Infrastruktur fehlt |
Integrationstests und normale Tests laufen nicht gleichzeitig — sie gehören in verschiedene Phasen. Die Reihenfolge der Testarbeit ergibt sich aus dem Risiko, nicht aus einer Abdeckungszahl: hohe Geldwirkung und niedrige Abdeckung zuerst, gut abgedeckte Stammdatenpflege zuletzt.
@QuarkusTest
class AbleseauftragResourceTest {
@Test
void bekannterAuftragWirdGeliefert() {
given()
.when().get("/ableseauftraege/4711")
.then().statusCode(200)
.body("zaehlernummer", is("Z-4711"));
}
}
REST Assured bekommt die Testadresse automatisch; der Testport liegt standardmäßig auf 8081.
Modul: Migration durch Tests absichern
Observability
| Erweiterung | Signal | Hinweis |
|---|---|---|
| quarkus-opentelemetry | Traces, Logs | empfohlener Weg für Traces |
| quarkus-micrometer | Metriken | breite Instrumentierung |
| quarkus-micrometer-opentelemetry | alle drei | kombiniert beide Wege |
| quarkus-smallrye-health | Health | Pfade unter q/health |
quarkus.otel.service.name=zaehlwerk-ablesung
quarkus.otel.exporter.otlp.endpoint=http://collector:4317
quarkus.otel.traces.sampler=parentbased_always_on
Ohne gesetzten Dienstnamen laufen alle Spannen unter einer gemeinsamen
Bezeichnung. Eigene Spannen (@WithSpan("tarif-ermitteln")) lohnen dort, wo
Fachlogik Zeit kostet — nicht an jeder Methode. Die vier Kennzahlen für den
Vergleich: Antwortzeit im 95. Perzentil, Fehlerrate je Endpunkt, Durchsatz je
Minute, Dauer des fachlichen Laufs. Alle vier vor der Umstellung erheben —
ohne Grundlinie ist jede spätere Zahl nur eine Zahl.
Modul: Observability für modernisierte Anwendungen
Container-Images und Plattform
./mvnw package \
-Dquarkus.container-image.build=true \
-Dquarkus.container-image.group=netzbetrieb \
-Dquarkus.container-image.name=zaehlwerk-ablesung
ls target/kubernetes/
Die erzeugten Manifeste gehören ins Repository — sie beschreiben den Sollzustand,
und Handeingriffe im Cluster gehen beim nächsten Rollout verloren. Grenzen und
Secrets stehen ebenfalls in der Konfiguration
(quarkus.kubernetes.env.secrets, …resources.limits.memory).
| Thema | Kubernetes | OpenShift |
|---|---|---|
| Zugang von außen | Ingress | Route |
| Image-Bau | außerhalb des Clusters | auch im Cluster möglich |
| Erweiterung | quarkus-kubernetes | quarkus-openshift |
| Rechte | oft weit gefasst | strengere Voreinstellung |
Die letzte Zeile trifft Migrationen häufig: ein Image, das als Wurzelbenutzer laufen will, startet dort nicht.
Modul: Containerisierung und Plattformbetrieb
JVM oder Native Image
| Merkmal | JVM-Image | Native-Image |
|---|---|---|
| Bauzeit | Minuten | deutlich länger |
| Startzeit | Sekundenbruchteile bis Sekunden | sehr kurz |
| Speicherbedarf | höher | niedriger |
| Fehlerbilder | vertraut | erst zur Laufzeit sichtbar |
Die Bauzeit gehört in die Rechnung — sie zahlt das Entwicklungsteam täglich.
| Lastprofil | Passende Variante | Grund |
|---|---|---|
| viele kurze Instanzen | nativ | Start und Speicher zählen |
| dauerhafte Last | JVM | Optimierung im Betrieb |
| seltener Batch-Lauf | beides möglich | Startzeit fällt kaum ins Gewicht |
| unklares Profil | JVM | weniger Unbekannte im Betrieb |
Der JVM-Betrieb im Container ist eine vollwertige Zielform, kein Zwischenschritt.
./mvnw package -Dnative
./mvnw package -Dnative -Dquarkus.native.container-build=true
./mvnw verify -Dnative
Vier Fundstellen brechen nativ, und alle vier zeigen sich erst beim Ausführen, nie beim Build: Reflection auf eigene Klassen (Abhilfe: Annotation zur Anmeldung), Fremdbibliotheken mit Reflection (Konfigurationsdatei), Ressourcen im Archiv (Aufnahme über Muster), dynamische Proxys (ausdrückliche Anmeldung). Deshalb ist der Integrationstest gegen das gebaute Artefakt hier Pflicht, nicht Kür.
Modul: JVM oder Native Image
Typische Fallen
- Jackson und JSON-B liegen gleichzeitig im Projekt und erzeugen andere Ausgaben; Datumsfelder wechseln dabei still das Format.
- Ein blockierender Aufruf hängt den Event Loop, weil
@Blockingfehlt. - Eine Bean verschwindet im Build, weil kein sichtbarer Injektionspunkt auf sie zeigt — gesucht wird sie per Reflection.
- Der Ableselauf startet in drei Instanzen und legt jeden Auftrag dreifach an; der Timer lief bisher im Server nur einmal.
- Die Bestätigung erfolgt vor der Datenbankänderung — ein Fehler verliert die Nachricht.
persistence.xmlund Properties bestehen nebeneinander, der Start bricht ab.- Lazy Loading schlägt erst außerhalb der Transaktion fehl, also in Produktion.
- Der Rollenanspruch liegt verschachtelt, der Pfad zeigt auf die falsche Ebene: 403 trotz gültigem Token.
- Liveness und Readiness zeigen auf denselben Endpunkt — jede Störung der Datenbank wird zum Neustart.
- Ein Secret steht im Klartext im Manifest, weil es vorher in der Serverkonfiguration stand.
- Es wird nativ gebaut, aber nur auf der JVM getestet.
Dieses Thema als Schulung für Ihr Team
Dieser Beitrag erklärt das Thema. Damit Ihr Team es danach auch anwendet, gibt es Jakarta EE modernisieren als Schulung — an Ihrem eigenen Code, mit den Fragen, die ein Text nicht beantwortet. Sie wählen die Module, wir bauen daraus ein Programm.
3 Tage·ab 900 EUR netto pro Tag (bis 3 Teilnehmende) ·Termin nach Vereinbarung
Als Team-Schulung anfragenZum Seminar Jakarta EE modernisieren →