Start / Cheat Sheets

Cheat Sheet

Jakarta EE nach Quarkus migrieren — Cheat Sheet

Stand: · Jakarta EE modernisieren

QuarkusJakarta EEKubernetesGraalVM Native Image

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:

ZustandHeuteIm Container
SitzungServerspeicherToken oder externer Speicher
Hochgeladene DateiSerververzeichnisObjektspeicher
TimerServerinstanzeine Instanz oder externe Steuerung
CacheServerspeicherakzeptiert kalt oder externer Cache

Modul: Moderne Application Server im Container

Entwicklungsschleife in Quarkus

WerkzeugAufrufAufgabe
Entwicklungsmodusquarkus devLive Reload beim nächsten Aufruf
Fortlaufende Testsim Dev-Modus, Taste rbetroffene Tests je Änderung
Dev UI/q/dev-uiKonfiguration und Erweiterungen einsehen
Dev Servicesautomatischfehlende 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üfungBeantwortetFolge bei Fehlschlag
StartupIst der Start abgeschlossenmehr Zeit, dann Neustart
LivenessLäuft der Prozess noch sinnvollNeustart des Containers
ReadinessKann er jetzt Anfragen annehmenkein 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

ErweiterungZweckHinweis
quarkus-restJakarta RESTBasis jeder Ressource
quarkus-rest-jacksonJSON über Jacksoneine JSON-Bindung wählen
quarkus-rest-jsonbJSON über JSON-Bnäher am bisherigen Verhalten
quarkus-rest-clientAufrufe nach außentypisierte 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

StelleTypische AbweichungWirkung beim Aufrufer
FeldnamenGroß- und KleinschreibungFeld wird nicht gefunden
NullwerteFeld fehlt statt nullAuswertung bricht ab
DatumZeitzone oder Formatfalscher Abrechnungstag
Statuscode500 statt 404Wiederholung 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

ScopeErzeugungHinweis
ApplicationScopedbeim ersten MethodenaufrufStandard für zustandslose Dienste
Singletonbeim Injizierenkein Proxy, kein spätes Erzeugen
RequestScopedje AnfrageKontext muss aktiv sein
Dependentje InjektionsstelleLebensdauer 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

TypVerhaltenTypischer Einsatz
REQUIREDnutzt vorhandene, sonst neueStandardfall
REQUIRES_NEWimmer neue, alte ruhtProtokolleintrag trotz Rollback
MANDATORYFehler ohne laufendeinterner Baustein
NEVERFehler mit laufenderreiner 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

EinstellungBisherJetzt
ZugangsdatenServerkonfigurationUmgebungsvariable oder Secret
PoolgrößeServerkonsolequarkus.datasource.jdbc.max-size
Dialektpersistence.xmlaus db-kind und db-version
Schemaverhaltenpersistence.xmlschema-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

BisherBedeutungDanach
Queueein Verbraucher je NachrichtKanal auf Broker-Ziel
Topicmehrere VerbraucherKanal mit Broadcast
Message-Driven BeanVerbraucher im ServerMethode mit Incoming
Bestätigung in Transaktionalles oder nichtsausdrü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

OrtInhaltNach der Migration
Realm des ServersBenutzer und RollenIdentity Provider
web.xmlSecurity ConstraintsAnnotationen an Methoden
AnnotationenRolesAllowedbleiben unverändert
ServerkonsoleRollenzuordnungAnspruch 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

MittelZweckHinweis
QuarkusTestAnwendung im Test startenschnell, im selben Prozess
QuarkusIntegrationTestgebautes Artefakt prüfenauch nativ und im Container
TestTransactionÄnderungen zurückrollenhält die Testdatenbank sauber
InjectMockBean durch Mock ersetzennur 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

ErweiterungSignalHinweis
quarkus-opentelemetryTraces, Logsempfohlener Weg für Traces
quarkus-micrometerMetrikenbreite Instrumentierung
quarkus-micrometer-opentelemetryalle dreikombiniert beide Wege
quarkus-smallrye-healthHealthPfade 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).

ThemaKubernetesOpenShift
Zugang von außenIngressRoute
Image-Bauaußerhalb des Clustersauch im Cluster möglich
Erweiterungquarkus-kubernetesquarkus-openshift
Rechteoft weit gefasststrengere 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

MerkmalJVM-ImageNative-Image
BauzeitMinutendeutlich länger
StartzeitSekundenbruchteile bis Sekundensehr kurz
Speicherbedarfhöherniedriger
Fehlerbildervertrauterst zur Laufzeit sichtbar

Die Bauzeit gehört in die Rechnung — sie zahlt das Entwicklungsteam täglich.

LastprofilPassende VarianteGrund
viele kurze InstanzennativStart und Speicher zählen
dauerhafte LastJVMOptimierung im Betrieb
seltener Batch-Laufbeides möglichStartzeit fällt kaum ins Gewicht
unklares ProfilJVMweniger 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 @Blocking fehlt.
  • 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.xml und 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.

Zum Seminar Jakarta EE modernisieren