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.