JBlog3 — Von Spring Boot zu Quarkus
Nach fast drei Jahren mit JBlog2 (Spring Boot) war es an der Zeit, das Backend grundlegend zu modernisieren. Das Ergebnis: JBlog3, komplett auf Quarkus umgebaut. Gleiche Funktionalität, modernerer Stack, und ein paar harte Lektionen auf dem Weg.
Wie es dazu kam
JBlog2 lief stabil auf Spring Boot 2.x mit Morphium als MongoDB-ORM. Kein Grund, etwas anzufassen — solange Spring Boot 2.x supported ist. Aber das End-of-Life rückte näher, und eine Migration auf Spring Boot 3 stand sowieso an. Neue Packages (javax → jakarta), geänderte Auto-Configuration, diverse Breaking Changes — das wäre kein Nachmittagsprojekt gewesen.
Und dann dachte ich: Wenn ich eh alles anfassen muss, warum nicht gleich was Neues ausprobieren?
Quarkus hatte mich schon länger gereizt. CDI statt Spring-Magic, JAX-RS statt Spring MVC, Qute statt Freemarker — ein komplett anderer Ansatz, aber mit dem Versprechen von schnellerer Startup-Zeit, weniger Speicherverbrauch und der Perspektive auf Native Images. Außerdem ist ein Blog-Backend das perfekte Spielfeld: überschaubare Komplexität, aber genug Ecken und Kanten, um ein Framework wirklich kennenzulernen.
Der entscheidende Faktor war aber Heiko. Er hatte eine Quarkus-Extension für Morphium gebaut — sauber, funktional, drop-in-ready. Ohne diese Arbeit hätte ich den MongoDB-Layer komplett neu verdrahten müssen. So konnte ich Morphium 1:1 mitnehmen: gleiche Version (6.2.0), gleiche Datenbank, gleiche Collections. Das hat die Hürde massiv gesenkt.
Die Umsetzung
Unterm Strich war die Migration weniger dramatisch als erwartet. Die Grundstruktur von JBlog2 ließ sich gut auf Quarkus übertragen — REST-Endpoints, Service-Layer, Entity-Klassen. Vieles war ein mechanisches Umschreiben: @Autowired → @Inject, @RequestMapping → @GET/@POST mit @Path, @Component → @ApplicationScoped.
Die Gelegenheit habe ich genutzt, um alten Ballast loszuwerden. Code, der seit Jahren nicht mehr gebraucht wurde. Workarounds für Probleme, die längst gelöst waren. Überflüssige Abstraktionen. So ein Rewrite ist auch immer ein guter Frühjahrsputz.
Ein paar Stellen haben aber doch für Kopfschmerzen gesorgt:
Templates: Freemarker → Qute
Die Template-Syntax ist komplett anders, aber der Umstieg war straightforward — bis auf ein Detail:
Qute escaped standardmäßig alle String-Werte. Umlaute in i18n-Texten wurden plötzlich als ä dargestellt. Die Lösung: Die i18n-Methode gibt jetzt RawString statt String zurück.
return new RawString(locService.getText(lang, key));
}
REST: @RequestParam → @FormParam + @QueryParam
Das war der schmerzhafteste Teil der Migration. Spring's @RequestParam liest Parameter sowohl aus der URL als auch aus dem Form-Body. In JAX-RS gibt es diese Magie nicht:
@QueryParamliest nur aus der URL (?key=value)@FormParamliest nur aus dem Form-Body (application/x-www-form-urlencoded)
Mein CLI-Tool jnl sendet alle POST-Daten als Form-Body (curl --data-urlencode). Nach der Migration waren plötzlich alle POST-Endpoints kaputt — Authentifizierung, Blog-Einträge bearbeiten, Kategorien pushen. Alles.
Die Lösung: Alle POST-Endpoints auf @FormParam + @Consumes(APPLICATION_FORM_URLENCODED) umstellen. Dazu den AuthFilter erweitern, damit er das Token auch aus dem Form-Body extrahieren kann:
MediaType mt = ctx.getMediaType();
if (mt == null || !mt.isCompatible(
MediaType.APPLICATION_FORM_URLENCODED_TYPE)) {
return null;
}
byte[] body = ctx.getEntityStream().readAllBytes();
ctx.setEntityStream(new ByteArrayInputStream(body));
// parse and extract token...
}
Wichtig: Den Entity-Stream nach dem Lesen zurücksetzen, damit die Endpoints ihn noch lesen können.
Go-Live
Der Umstieg auf Produktion lief völlig problemlos. JBlog2 gestoppt, JBlog3 gestartet, Health-Check grün — fertig. Keine Downtime, keine Datenmigration nötig (gleiche MongoDB, gleiche Collections), die Seiten waren sofort erreichbar.
Die eigentlichen Probleme kamen erst Tage später, als ich mir die Statistiken genauer anschaute.
Stats-System: Zwei Bugs gestapelt
jnl blog stats recent zeigte für alle Posts 0 Hits an. Die Ursache: zwei Bugs, die sich gegenseitig überlagerten und das Debugging zur Geduldsprobe machten.
Bug 1: Auth-Check mit null-Role
Der AuthFilter rief isAllowedWithoutIpCheck(token, null) auf. Die Methode prüfte am Ende u.getRole().equals(null) — immer false. Der Fallback versuchte dann einen IP-Check mit "0.0.0.0" als IP, was natürlich auch fehlschlug.
Fix: return r == null || u.getRole().equals(r);
Bug 2: URL-Limit
Der hits_by_url Endpoint gab nur die Top 100 URLs zurück. Mit tausenden URLs (Bot-Probes, Bild-Requests, Hack-Versuche) landeten neuere Blog-Posts mit weniger Hits nicht in den Top 100.
Fix: Limit entfernt.
Stats-Kollision: Zwei JVMs, eine Collection
Während der Testphase lief JBlog3 im Homelab gegen die gleiche MongoDB wie JBlog2 in Produktion. Beide schrieben alle 60 Sekunden ihre Stats in die gleiche Collection mit dem gleichen _id (Datum) — und überschrieben sich gegenseitig. Die Produktion hatte plötzlich nur noch die Minimal-Werte aus dem Homelab.
Fix: stats.enabled=false für das Homelab-Profil.
Was gleich geblieben ist
- Morphium 6.2.0 als MongoDB-ORM — funktioniert identisch in Spring Boot und Quarkus
- MongoDB Replica Set auf dem gleichen Server
- Whitelabel-System — ein Backend bedient mehrere Domains (caluga.de, boesebeck.name, boesebeck.biz, zagdul.de)
- jnl als CLI-Tool für Blog-Management (lokal schreiben, per
pushsynchronisieren) - Markdown als Content-Format mit serverseitigem Rendering
- Token-basierte Authentifizierung mit AES-verschlüsselten Tokens und optionalem TOTP
Deployment
JBlog3 läuft als Uber-JAR via nohup — kein Container, kein Systemd, einfach Java 21:
-Dquarkus.profile=prod \
-jar ./jblog3.jar \
> nohup.out 2>&1 &
Das Deployment-Script baut lokal, kopiert das JAR per SCP, killt den alten Prozess und startet den neuen. Health-Check nach 15 Sekunden. Fertig.
Ein Native Image wäre der nächste Schritt — die Startup-Zeit würde von ~12 Sekunden auf unter eine Sekunde fallen. Aber dafür muss Morphium erst native-kompatibel werden.
Fazit
Die Migration war keine Raketenwissenschaft. Wer vor der Entscheidung steht, Spring Boot 2 → 3 zu migrieren, sollte ernsthaft überlegen, ob nicht gleich ein Wechsel auf Quarkus lohnt — der Aufwand ist vergleichbar, aber man bekommt einen moderneren, schlankeren Stack und lernt dabei eine Menge.
Die Tücken lagen nicht in der Migration selbst, sondern im Detail danach: Parameter-Handling in JAX-RS, Qute-Escaping, und vor allem das Debugging von Problemen, bei denen sich mehrere Bugs gegenseitig verdeckten.
Das Ergebnis lohnt sich: Schnellerer Start, weniger Speicherverbrauch, und ein Stack, der aktiv weiterentwickelt wird. Und die Stats funktionieren jetzt auch.