Code snippets
All code examples from the six-book Spring Boot series. The QR codes in the books lead straight to the matching snippet.
1921 snippets across 6 books and 105 lessons
Books in the series
- Buch 1Java & Kotlin Foundations180 snippets · 10 lessons
- Buch 2Spring Boot in Kotlin308 snippets · 13 lessons
- Buch 3Enterprise & Architecture552 snippets · 24 lessons
- Buch 4Kotlin Stack for Freelance Projects324 snippets · 20 lessons
- Buch 5Kotlin in the Microsoft Stack274 snippets · 18 lessons
- Buch 6Kotlin for AI Agents283 snippets · 20 lessons
Book 1 — Java & Kotlin Foundations
180 snippetsLesson 1
5 snippets- B1L01-0035 · Anatomie eines frischen Spring-Boot-Kotlin-Projektstext
Nach dem Auspacken siehst du sowas:
- B1L01-0045 · Anatomie eines frischen Spring-Boot-Kotlin-Projektskotlin
Die Entry-Point-Datei sieht so aus:
- B1L01-005`build.gradle.kts` in zwei Minutenkotlin
Lies das gleich richtig:
- B1L01-0082 · Projekt generierenbash
Über Spring Initializr per curl (geht ohne Browser, ohne lokales
- B1L01-0115 · Ersten eigenen Endpoint bauenkotlin
projekt/src/main/kotlin/com/example/hello/HelloController.kt
Lesson 2
26 snippets- B1L02-0012.1 · Eine Klasse, die du sofort verstehstjava
mitzumachen reicht, damit dir die Begriffe in Lektion 02 (Annotations, Generics, Lambdas, Streams) nicht um die Ohren
- B1L02-0032.3 · Verzeichnis-Layout, das du immer wieder sehen wirsttext
zu viele Imports zusammenkommen — bleib bei expliziten Imports.
- B1L02-0043 · Sichtbarkeit — vier Stufen, eine Default-Fallejava
nützlich für interne Helper, kann aber überraschen, wenn man damit nicht rechnet.
- B1L02-0054.1 · `static` — gehört der Klasse, nicht der Instanzjava
Template-Method-Patterns).
- B1L02-0064.2 · `final` — „nicht mehr ändern"java
final hat drei Bedeutungen, je nachdem, vor was es steht:
- B1L02-0094.4 · Mini-Pattern: Singleton mit `static`java
Ein typisches Klausur-Beispiel; in echtem Spring-Code praktisch nicht nötig:
- B1L02-0105.3 · Auto-Boxing — bequem, aber heimtückischjava
Java konvertiert automatisch zwischen int und Integer (und allen anderen Paaren):
- B1L02-0126.2 · String-Pool — der Performance-Trick mit der `==`-Fallejava
StringBuilder ist die mutable Alternative für Hot-Loops.
- B1L02-0136.3 · Wichtige `String`-Operationenjava
false, wenn die Argumente unterschiedlich sind oder nur eines null ist, ohne dass dir die NPE um die Ohren fliegt.
- B1L02-0157.2 · Mehrere Konstruktoren — Method-Overloadingjava
Java erlaubt mehrere Konstruktoren mit unterschiedlichen Parameterlisten in derselben Klasse:
- B1L02-0178.1 · `extends`java
PHP-Constructor-Promotion + readonly.
- B1L02-0198.3 · Abstract Classesjava
Klassen, von denen du nicht direkt eine Instanz bauen kannst — nur Subklassen:
- B1L02-0219 · Interfaces — Verträge ohne Erbejava
richtige provider()-Implementierung. Wie in PHP, nur strenger typisiert.
- B1L02-02410.1 · `equals` — das Sicherheitsnetz für „gleich"java
Wenn du semantische Gleichheit willst (gleiche E-Mail = gleicher User), überschreibst du equals:
- B1L02-02811.1 · Checked Exceptions — Compile-Wandjava
und der Unterschied wird vom Compiler erzwungen:
- B1L02-02911.2 · Unchecked Exceptions — Laravel-Gefühljava
Hierarchie ist ein Beispiel — die JDBC-SQLException (checked) wird in eine RuntimeException umgewickelt.
- B1L02-03011.3 · Eigene Exceptionsjava
Spring-Code.
- B1L02-03211.5 · `finally` und `multi-catch`java
try-Blocks automatisch. Ein Pendant in PHP gibt es nicht; dort musst du finally { fclose($f); } schreiben.
- B1L02-034Klassen, Konstruktoren, Felderjava
Behandlung kommt in B1/L05–L10.
- B1L02-037`static` → `companion object` und Top-Levelkotlin
Kotlin kennt zwei idiomatischere Wege:
- B1L02-038Vererbung, Interfaces, abstract classeskotlin
Kotlin: gleiches Konzept, aber kompakter:
- B1L02-0413 · `Address.java` — eine kleine Value-Klassejava
src/com/example/shop/Address.java:
- B1L02-0424 · `User.java`java
src/com/example/shop/User.java:
- B1L02-0435 · `AdminUser.java` — Vererbungjava
src/com/example/shop/AdminUser.java:
- B1L02-0446 · `App.java` — die ausführbare `main`-Methodejava
src/com/example/shop/App.java:
- B1L02-045Weg A — in IntelliJtext
Output (in der Reihenfolge der println-Aufrufe):
Lesson 3
28 snippets- B1L03-0012.1 · Was eine Annotation istjava
Compiler, eine IDE, ein Framework wie Spring).
- B1L03-0032.2 · Eigene Annotation deklarierenjava
Hibernate-Universum ist darauf aufgebaut.
- B1L03-0042.2 · Eigene Annotation deklarierenjava
FIELD, METHOD, PARAMETER, CONSTRUCTOR. Du kannst mehrere kombinieren: @Target({TYPE, METHOD}).
- B1L03-0052.3 · Meta-Annotationenjava
intern definiert als:
- B1L03-0093.3 · Eigene generische Klassejava
in modernen Tools. Generics in Java sind dasselbe Konzept, aber vom Compiler erzwungen.
- B1L03-0103.3 · Eigene generische Klassejava
Mehrere Parameter:
- B1L03-0113.4 · Generische Methodenjava
Eine Methode kann ihren eigenen Typ-Parameter haben — unabhängig von der Klasse:
- B1L03-0123.5 · Bounded Generics — Begrenzungen am Typjava
Manchmal willst du den Typ einschränken: „T muss Comparable sein."
- B1L03-0143.6 · Wildcards — `?`, `? extends T`, `? super T`java
Oder „eine Liste, deren Elemente von Number erben" — sinnvoll, wenn du nur lesend zugreifst:
- B1L03-0163.8 · Generics in Spring-APIs, die dir später begegnenjava
mit Erasure.
- B1L03-0174.1 · Was ein Lambda istjava
≤ 7) hat man so etwas immer als anonyme Klasse geschrieben:
- B1L03-0194.1 · Was ein Lambda istjava
(parameters) -> { statements; }
- B1L03-0204.2 · Funktionale Interfaces — die Empfänger von Lambdasjava
- B1L03-0234.4 · Lambdas und die Variable-Capture-Regeljava
(nach der Initialisierung nicht mehr zugewiesen werden):
- B1L03-0245.1 · Die Ideephp
genau das, was du in Laravel mit Collections machst:
- B1L03-0265.2 · Stream entstehen, Stream-Operationen, Stream beendenjava
Drei Phasen:
- B1L03-0295.4 · Die wichtigsten Stream-Operationen mit Beispielenjava
Wenn du dieselbe Pipeline zweimal brauchst, bau sie zweimal — oder leg das Zwischenergebnis als List ab.
- B1L03-0305.5 · `collect` und `Collectors` — die Aggregations-Werkstattjava
- B1L03-0315.6 · Primitive Streams — Performance ohne Boxingjava
IntStream, LongStream, DoubleStream sind spezialisierte Streams für Primitive — kein Boxing, schneller:
- B1L03-0325.7 · `Optional` als Stream-Vorgriffjava
Ergebnis fehlen kann (leerer Stream). Du arbeitest mit Optional wie folgt:
- B1L03-0335.8 · `flatMap` — wenn map-Ergebnisse selbst Streams sindjava
Wenn jede Element-Transformation mehrere neue Elemente erzeugt, brauchst du flatMap:
- B1L03-0356 · Wie das in Spring zusammenwirkt — Vorgriffjava
Damit du eine Vorahnung hast, wie diese vier Bausteine in Phase 2 zusammenspielen:
- B1L03-038Streams → Collection-Operationen oder Sequenceskotlin
Kotlin hat zwei Wege:
- B1L03-0412 · `@Audit`-Annotation deklarierenjava
src/com/example/audit/Audit.java:
- B1L03-0423 · `Order`-Datentypjava
src/com/example/orders/Order.java:
- B1L03-0434 · `OrderStats` — generischer Container als Übungsbeispieljava
src/com/example/orders/OrderStats.java:
- B1L03-0445 · `OrdersDemo` mit `main`-Methodejava
src/com/example/orders/OrdersDemo.java:
- B1L03-045Weg B — Dockerbash
Streams-Ergebnisse mit For-Loop-Ergebnissen — sie müssen identisch sein.
Lesson 4
34 snippets- B1L04-0012.1 · Das Problem, das Records lösenjava
Eine typische DTO-Klasse in „klassischem" Java sieht so aus:
- B1L04-0032.2 · Records — die Sprachformphp
PHP-Vergleich:
- B1L04-0042.3 · Eigene Methoden, Validation, Compact Constructorjava
Records sind keine Datenkonserven. Du kannst Methoden hinzufügen:
- B1L04-0052.3 · Eigene Methoden, Validation, Compact Constructorjava
Normalisierung:
- B1L04-0072.4 · Records mit zusätzlichen statischen Factory-Methodenjava
das ist auch erlaubt, aber selten nötig.
- B1L04-0092.6 · Records in Spring — was du erwartet vorfinden wirstjava
interfaces (siehe Sektion 7).
- B1L04-0102.7 · Internals — wie ein Record im Bytecode aussiehttext
javap -p UserDto.class zeigt dir ungefähr:
- B1L04-0123.3 · `Optional` verarbeiten — die kanonischen Operationenjava
Optional<String> maybe = Optional.ofNullable(possiblyNullVar);
- B1L04-0255.2 · Switch-Expressionsjava
Vorher (switch-Statement):
- B1L04-0285.2 · Switch-Expressionsjava
Mit Block-Body und yield für Mehr-Zeilen-Logik:
- B1L04-0295.3 · Pattern Matching in `switch`java
Seit Java 21 final:
- B1L04-0305.4 · Null-Pattern (Java 21+)java
prüft der Compiler auf Vollständigkeit — du kannst keinen Case vergessen.
- B1L04-0315.5 · `when`-Guards (Java 21+)java
Endlich null im switch ohne separaten if-Check davor.
- B1L04-0346.4 · `Result<T, E>` — der klassische Anwendungsfalljava
Keine Suche nach „wer erbt von X" — der Header sagt's.
- B1L04-0356.4 · `Result<T, E>` — der klassische Anwendungsfalljava
static <T, E> Result<T, E> err(E error) { return new Err<>(error); }
- B1L04-0377 · Text Blocks — Mehrzeilen-Strings ohne `\n`-Spaghettijava
Mit Text Blocks (Java 15 final):
- B1L04-0387 · Text Blocks — Mehrzeilen-Strings ohne `\n`-Spaghettijava
Praktische Anwendung:
- B1L04-0419 · Zusammenspiel — Records + Sealed + Pattern Matchingjava
Damit du das Zusammenspiel einmal komplett siehst, hier ein realistischer Mini-Domainblock, der alle Features
- B1L04-043Records → `data class`kotlin
return unitPrice.multiply(BigDecimal.valueOf(quantity));
- B1L04-044`Optional<T>` → Nullable Typesjava
beides funktioniert in Records nicht direkt. Volle Lehre in B1/L07.
- B1L04-046Pattern Matching → `when` mit Smart Castsjava
Immutability sichtbar.
- B1L04-047Pattern Matching → `when` mit Smart Castskotlin
default -> "unbekannt";
- B1L04-0531 · Sandbox-Struktur erweiterntext
Layout nach diesem Schritt:
- B1L04-0542 · `Order` zu Record refactorenjava
Öffne src/com/example/orders/Order.java und ersetze die klassische Klasse durch:
- B1L04-0552 · `Order` zu Record refactorenjava
Validiere mit einem Aufruf:
- B1L04-0563 · `Result<T, E>` sealed Hierarchiejava
src/com/example/result/Result.java:
- B1L04-0574 · `PaymentEvent` als sealed mit Recordsjava
src/com/example/payments/PaymentEvent.java:
- B1L04-0584 · `PaymentEvent` als sealed mit Recordsjava
src/com/example/payments/CardEvent.java:
- B1L04-0594 · `PaymentEvent` als sealed mit Recordsjava
src/com/example/payments/BankEvent.java:
- B1L04-0604 · `PaymentEvent` als sealed mit Recordsjava
src/com/example/payments/CashEvent.java:
- B1L04-0614 · `PaymentEvent` als sealed mit Recordsjava
src/com/example/payments/RefundEvent.java:
- B1L04-0625 · `PaymentEventHandler` mit Pattern Matchingjava
src/com/example/payments/PaymentEventHandler.java:
- B1L04-0636 · `PaymentDemo` — alles zusammenbringenjava
src/com/example/payments/PaymentDemo.java:
- B1L04-064Weg B — Dockerbash
die Lektion verstanden.
Lesson 5
17 snippets- B1L05-0063 · Properties statt Getter/Setterjava
In Java schreibst du Felder und Accessors selbst:
- B1L05-008Custom Getter/Setterkotlin
schreibst du Custom Accessors:
- B1L05-0104 · Primary Constructor — die Klasse in einer Zeilejava
In Java schreibst du den Konstruktor separat:
- B1L05-0124 · Primary Constructor — die Klasse in einer Zeilekotlin
Wenn die Klasse einen Body braucht, hängst du ihn dran:
- B1L05-013`init`-Block für Konstruktor-Logikkotlin
Berechnungen. Dafür gibt es den init-Block:
- B1L05-0155 · Top-Level-Funktionen und -Propertiesjava
Java verlangt für jede Funktion eine Klasse:
- B1L05-0165 · Top-Level-Funktionen und -Propertieskotlin
Kotlin erlaubt dir, Funktionen direkt am Datei-Top zu definieren:
- B1L05-017Top-Level-Propertieskotlin
Konstanten und Konfiguration-Werte landen genauso am Top:
- B1L05-020Beispiel: Extensions am `LocalDate`kotlin
3. Bestehende Java-APIs Kotlin-idiomatisch nutzen. Du kannst der Java-Klasse java.time.LocalDate eine Extension isWorkday() hinzufügen, ohne die Klasse zu kennen oder modifizieren zu können.
- B1L05-0268 · Default-Werte und Named Argumentskotlin
und Builder-Pattern. Kotlin löst es mit Default-Werten am Parameter:
- B1L05-029Wann Named Arguments verwenden?kotlin
benennen. Beispiel:
- B1L05-0311 · Sandbox-Setupkotlin
build.gradle.kts:
- B1L05-0332 · Die Ausgangs-Java-Klasse (zur Referenz)java
sie ist nur der Maßstab, gegen den deine Kotlin-Variante steht:
- B1L05-0343 · Das gleiche in Kotlin — drei Wegekotlin
strings.kt — Top-Level-Funktionen:
- B1L05-0353 · Das gleiche in Kotlin — drei Wegekotlin
stringextensions.kt — dieselben drei Funktionen als Extensions:
- B1L05-0363 · Das gleiche in Kotlin — drei Wegekotlin
Main.kt — die fun main(), die alles ausprobiert:
- B1L05-0385 · Ein Property mit Custom-Getterkotlin
Extension-Aufruf:
Lesson 6
8 snippets- B1L06-013Wo Smart Casts nicht greifenkotlin
Workaround: lokale val zwischenspeichern.
- B1L06-019`run` — Argument als `this`, gibt Lambda-Ergebnis zurückkotlin
Hauptanwendung: ein Objekt initialisieren und einen Wert daraus
- B1L06-0219 · Sammelpunkt — eine realistische Funktionkotlin
Wie sieht das in einer kleinen, realistischen Funktion zusammen aus?
- B1L06-0231 · Sandbox erweiternkotlin
build.gradle.kts:
- B1L06-0253 · Profil-Beschreibung ohne NPEkotlin
src/main/kotlin/Profile.kt:
- B1L06-0264 · Eine Main-Funktion mit drei Test-Aufrufenkotlin
src/main/kotlin/Main.kt:
- B1L06-0275 · Builder-Konfiguration mit `apply`kotlin
konfiguriere sie idiomatisch:
- B1L06-0286 · Smart Cast — eine kleine Animal-Hierarchiekotlin
Tippe src/main/kotlin/Animal.kt:
Lesson 7
14 snippets- B1L07-002Was eine `data class` für dich erledigtkotlin
- B1L07-0053 · Sealed Classes und Sealed Interfaces — geschlossene Hierarchienkotlin
beim Pattern Matching prüfen, ob du alle Fälle abgedeckt hast.
- B1L07-006Object-Subtypen statt Data-Classkotlin
idiomatische Weg:
- B1L07-0084 · `object`-Deklaration — Singleton als Sprachfeaturekotlin
Kotlin macht das mit einem Schlüsselwort:
- B1L07-0095 · `companion object` — das statische Pendantkotlin
schreibst du companion object innerhalb der Klasse:
- B1L07-010Modus 1: `when` mit Subjektkotlin
mächtiger. Drei Modi.
- B1L07-011Modus 2: `when` als Pattern-Match auf sealedkotlin
exhaustive ist (Int ist nicht sealed).
- B1L07-012Modus 3: `when` ohne Subjektkotlin
Cast (siehe B1/L06) angewendet auf sealed Hierarchien.
- B1L07-0147 · Eine kleine Order-Domäne — alles zusammenkotlin
Wie sieht das in einem realistischen Mini-Modell aus?
- B1L07-0161 · Sandboxkotlin
build.gradle.kts:
- B1L07-0172 · `Result<T, E>` als sealed Hierarchiekotlin
Tippe src/main/kotlin/Result.kt:
- B1L07-0183 · `Money` als Value Objectkotlin
Tippe src/main/kotlin/Money.kt:
- B1L07-0194 · `ShoppingCart` als Aggregatekotlin
Tippe src/main/kotlin/Cart.kt:
- B1L07-0205 · Main-Funktion mit Pattern-Matchingkotlin
Tippe src/main/kotlin/Main.kt:
Lesson 8
12 snippets- B1L08-005`launch` — fire-and-forgetkotlin
Innerhalb eines Scopes gibt es zwei Wege, eine Coroutine zu starten.
- B1L08-007`async` — mit Ergebniskotlin
Scope wartet implizit auf alle Children, bevor er endet.
- B1L08-0085 · Strukturierte Concurrency — Scopes und Cancellationkotlin
Children fertig sind.
- B1L08-009Cancellation propagiertkotlin
manuelle Cleanup-Logik schreiben:
- B1L08-010`supervisorScope` — wenn Children sich nicht gegenseitig abbrechen sollenkotlin
nicht mitnimmt. Dann nimmst du supervisorScope:
- B1L08-0138 · Ein kleines Beispiel — paralleler Fetch mit Aggregationkotlin
Wie sieht das in einer realistischen Funktion aus?
- B1L08-0141 · Sandbox mit Coroutines-Dependencykotlin
build.gradle.kts:
- B1L08-0152 · Zwei parallele „API-Calls"kotlin
Tippe src/main/kotlin/Main.kt:
- B1L08-0173 · Cancellation propagierenkotlin
ersetze die obige Funktion zeitweise):
- B1L08-0184 · `supervisorScope` zum Vergleichkotlin
Unterschied:
- B1L08-0195 · Dispatcher-Wechsel mit `withContext`kotlin
Dispatchers.IO umgeleitet wird:
- B1L08-0206 · Alle drei Bausteine zusammenkotlin
Bau ein finales Main.kt, das alles kombiniert:
Lesson 9
16 snippets- B1L09-0011 · Worauf du dich hier einlässt — die mentale Umstellungkotlin
aus Gradle KTS, das du in jeder Buch-2-Lektion sehen wirst:
- B1L09-004Eine Funktion mit Block-Argumentkotlin
entsteht das, was wie eine Block-DSL aussieht:
- B1L09-005`apply`, `with`, `run` — schon DSL-Vorbotenkotlin
Die Scope-Functions aus B1/L06 sind Standard-Library-DSL-Funktionen:
- B1L09-0063 · Eine eigene Mini-DSL — HTML-Builderkotlin
Tags, body und h1, plus Text-Inhalten:
- B1L09-0073 · Eine eigene Mini-DSL — HTML-Builderkotlin
html.block()
- B1L09-0084 · `@DslMarker` — Verschachtelungs-Verwechslungen verhindernkotlin
den Receiver von außen sichtbar hält:
- B1L09-0094 · `@DslMarker` — Verschachtelungs-Verwechslungen verhindernkotlin
anderen Marker-Receiver vom Compiler ausgeblendet.
- B1L09-0105 · Gradle KTS lesen — eine echte DSL aus der Praxiskotlin
ein Kotlin-Projekt generiert:
- B1L09-0116 · Spring-Security-DSL lesen (Vorschau auf Buch 2)kotlin
Spring Security in Boot 4 bringt eine Kotlin-DSL mit. Sie sieht so aus:
- B1L09-0127 · Ktor-Routing lesen (Vorschau auf Buch 4)kotlin
verschachtelte Block-Struktur geschrieben:
- B1L09-0141 · HTML-DSL nachbauen und erweiternkotlin
build.gradle.kts:
- B1L09-0151 · HTML-DSL nachbauen und erweiternkotlin
Tippe src/main/kotlin/HtmlDsl.kt:
- B1L09-0162 · DSL anwendenkotlin
Tippe src/main/kotlin/Main.kt:
- B1L09-0183 · `@DslMarker` ausprobierenkotlin
Versuche bewusst, eine ungültige Verschachtelung zu tippen:
- B1L09-0194 · Eigene Lektion über Gradle-KTSbash
oder generiere ein neues mit:
- B1L09-0204 · Eigene Lektion über Gradle-KTSkotlin
Beispiel-Struktur:
Lesson 10
20 snippets- B1L10-0012 · Anatomie eines Spring-Boot-Kotlin-build.gradle.ktskotlin
Boot-4-Kotlin-Projekt mit Web + DevTools generiert (gekürzt):
- B1L10-0033.1 · `kotlin("plugin.spring")` — Pflichtkotlin
Methoden standardmäßig final — Subklasse-Bilden scheitert.
- B1L10-0043.2 · `kotlin("plugin.jpa")` — Pflicht, wenn JPAkotlin
Reflection-basiertes Instanziieren).
- B1L10-0053.4 · `kotlin("plugin.serialization")` — für kotlinx.serializationkotlin
KSerializer-Implementierungen für @Serializable-Klassen generiert.
- B1L10-0064 · Dependencies und Spring-spezifische Modulekotlin
4 wird es Standard.
- B1L10-0085 · KSP — Kotlin Symbol Processingkotlin
So aktiviert man KSP für ein Projekt:
- B1L10-0096 · Version Catalogs — Versionen zentral verwaltentoml
Ein Version Catalog liegt unter gradle/libs.versions.toml:
- B1L10-0106 · Version Catalogs — Versionen zentral verwaltenkotlin
Im build.gradle.kts referenzierst du dann typsicher:
- B1L10-0127 · Convention Plugins — Build-Logik wiederverwendenkotlin
Inhalt eines Convention Plugins (kotlin-spring-conventions.gradle.kts):
- B1L10-0147 · Convention Plugins — Build-Logik wiederverwendenkotlin
In einem Subproject-build.gradle.kts:
- B1L10-0161 · Frisches Boot-4-Kotlin-Projekt generierenbash
Hole dir einen sauberen Stand zum Spielen:
- B1L10-0172 · `gradle/libs.versions.toml` anlegentoml
schon im Projekt vom Gradle-Wrapper):
- B1L10-0183 · `build.gradle.kts` auf den Catalog umstellenkotlin
Ersetze den plugins-Block:
- B1L10-0193 · `build.gradle.kts` auf den Catalog umstellenkotlin
Und den dependencies-Block:
- B1L10-0214 · Convention Plugin schreibenkotlin
build-logic/build.gradle.kts:
- B1L10-0224 · Convention Plugin schreibenkotlin
build-logic/src/main/kotlin/kotlin-spring-conventions.gradle.kts:
- B1L10-0234 · Convention Plugin schreibenkotlin
referenzieren:
- B1L10-0244 · Convention Plugin schreibenkotlin
Jetzt das Haupt-build.gradle.kts umstellen — der Plugin-Block schrumpft
- B1L10-0276 · KSP-Mini-Demo (optional, ~15 Min)kotlin
koin-demo/build.gradle.kts:
- B1L10-0286 · KSP-Mini-Demo (optional, ~15 Min)kotlin
src/main/kotlin/Main.kt:
Book 2 — Spring Boot in Kotlin
308 snippetsLesson 1
20 snippets- B2L01-0012 · `@SpringBootApplication` — drei Annotations in einerkotlin
Du hast die Annotation in Buch 1 / Lektion 01 schon gesehen — hier in der Kotlin-Variante, die Boot 4 als Default ausgibt:
- B2L01-0022 · `@SpringBootApplication` — drei Annotations in einerjava
@SpringBootApplication ist eine Meta-Annotation — sie kombiniert drei einzelne Annotations. Wenn du die Source-Datei aufmachst (in IntelliJ: Cmd+Click auf die Annotation), siehst du im Wesentlichen:
- B2L01-0043 · Spring-Boot-Starters — Dependency-Bündel mit Version-Managementtext
spring-boot-starter-web ist ein Starter — eine Meta-Dependency, die selbst keinen ausführbaren Code enthält, aber über transitive Dependencies einen aufeinander abgestimmten Stack mitbringt. Wenn du ./gradlew dependencie
- B2L01-005Das Dependency-Management-Plugin und die BOMkotlin
In build.gradle.kts steht oben:
- B2L01-007Schritt 1 — Discovery: `AutoConfiguration.imports`text
Das ist eine schlichte Textdatei mit einem voll qualifizierten Klassennamen pro Zeile. Beispiel-Inhalt aus spring-boot-autoconfigure:
- B2L01-008Schritt 2 — Conditional Activationjava
Jede Auto-Configuration-Klasse ist eine ganz normale @Configuration-Klasse, hat aber zusätzlich @Conditional…-Annotations darauf, die entscheiden, ob sie aktiviert wird. Ein gekürztes Beispiel aus dem Spring-Source-Code:
- B2L01-009Schritt 3 — Properties-Bindingjava
Viele Auto-Configurations haben @EnableConfigurationProperties(XxxProperties.class). Die XxxProperties-Klasse ist eine annotierte POJO, deren Felder aus application.yml/application.properties befüllt werden. Beispiel:
- B2L01-012oder, wenn du die App schon paketiert hast:text
Der Output sieht so aus:
- B2L01-0145 · `application.properties` / `application.yml`yaml
application.yml (YAML, hierarchisch):
- B2L01-016Bannertext
Beim Start druckt Spring Boot dieses ASCII-Art-Banner:
- B2L01-0176 · Embedded Tomcat / Jetty / Undertowkotlin
Tomcat ist Default. Wechseln auf Jetty: In build.gradle.kts Tomcat ausschließen, Jetty einziehen:
- B2L01-0186 · Embedded Tomcat / Jetty / Undertowyaml
Wichtige Server-Properties:
- B2L01-019`CommandLineRunner` als Bootstrap-Hookkotlin
Das praktischste Beispiel ist Setup-Code, der einmal beim Start laufen soll. Eine Bean, die CommandLineRunner implementiert, bekommt die Command-Line-Argumente und wird genau einmal nach Context-Refresh ausgeführt:
- B2L01-0211 · Auto-Configuration-Report mit `--debug` lesenmarkdown
Speichere dir 5 bis 10 Zeilen aus dem Report als Kommentar oben in einer neuen Datei projekt/src/main/resources/AUTOCONFIG-NOTES.md (Markdown — nicht versionsrelevant, dient nur als Lern-Notiz). Beispiel:
- B2L01-0222 · Banner änderntext
Lege folgendes File an: projekt/src/main/resources/banner.txt
- B2L01-0233 · Von `.properties` auf `.yml` umstellenyaml
Im Generator wurde application.properties angelegt. Wir machen daraus YAML.
- B2L01-0244 · Dev-Profile mit eigenem Portyaml
Lege ein zweites Property-File an: projekt/src/main/resources/application-dev.yml
- B2L01-0315 · Property mit `${}`-Substitutionyaml
Erweitere application.yml (NICHT die application-dev.yml):
- B2L01-0325 · Property mit `${}`-Substitutionbash
Anschließend startest du die App dreimal und beobachtest das Verhalten:
- B2L01-0336 · Eigener CommandLineRunner als Bootstrap-Beobachterkotlin
Lege folgende Klasse an: projekt/src/main/kotlin/com/example/hello/StartupReporter.kt
Lesson 2
28 snippets- B2L02-0011 · Was „Inversion of Control" tatsächlich heißtphp
Laravel-Brücke. Du kennst das Pattern bereits, ohne den Namen unbedingt zu nutzen:
- B2L02-0021 · Was „Inversion of Control" tatsächlich heißtkotlin
Spring macht das Gleiche. Andere Annotation-Namen, andere Sprache, identisches Pattern — und in Kotlin schrumpft die Klasse auf eine Zeile:
- B2L02-0032 · Der `ApplicationContext`text
Springs IoC-Container heißt ApplicationContext. Stell ihn dir vor als eine Map<Klasse, Objekt> mit zusätzlichem Lebenszyklus-Management. Beim Start sammelt er Bean-Definitions, instanziiert sie, verdrahtet sie miteinande
- B2L02-005Drei Wege, eine Bean zu definierenkotlin
Weg B — @Bean-Methode in einer @Configuration-Klasse:
- B2L02-007Component-Scanning — wie Beans gefunden werdentext
Beispiel der Package-Layout-Regel:
- B2L02-012Setter-Injection (Spezialfall)kotlin
Field-Injection war in Spring 3/4 verbreitet und sieht in alten Tutorials oft so aus. Schreib heute keine neue Klasse mit Field-Injection — und in Kotlin verbietet sich das lateinit-Pattern doppelt, weil es das Idiom „ei
- B2L02-0134 · Konflikte auflösen — `@Qualifier`, `@Primary`kotlin
Was, wenn es zwei Beans desselben Typs gibt? Beispiel: zwei GreetingService-Implementationen.
- B2L02-017Lösung B — `@Qualifier` am Injektions-Punktkotlin
Der Qualifier-Name ist standardmäßig der Klassenname mit kleinem Anfangsbuchstaben (CasualGreetingService → casualGreetingService). Du kannst auch eigene Qualifier-Namen vergeben:
- B2L02-018Lösung C — eigene Qualifier-Annotationkotlin
Wenn du eine semantisch starke Wahl willst, baust du dir eine eigene Annotation. In Kotlin sieht das so aus:
- B2L02-019Lösung C — eigene Qualifier-Annotationkotlin
annotation class ist Kotlins direktes Pendant zu Javas public @interface — gleiche Semantik, kompakteres Schlüsselwort. @Target und @Retention kommen aus dem Kotlin-Reflection-Package und mappen auf dieselben Bytecode-At
- B2L02-0226 · Internals — BeanFactory, ApplicationContext, `refresh()`text
Springs Container-Implementierung ist hierarchisch aufgebaut:
- B2L02-0236 · Internals — BeanFactory, ApplicationContext, `refresh()`text
In der Praxis fragst du Spring meist gar nicht direkt — du holst dir Beans über Konstruktor-Injection, nicht über applicationContext.getBean(...). Für das Verständnis ist trotzdem nützlich:
- B2L02-0247 · `@Bean`-Methoden vs. Stereotypen — wann waskotlin
Beispiel: Du willst einen RestClient (Klasse aus spring-web, in Spring Framework 6 eingeführt, in Boot 4 als Default-HTTP-Client etabliert), der mit deiner App-Konfiguration vorinitialisiert ist. RestClient ist nicht dei
- B2L02-0257 · `@Bean`-Methoden vs. Stereotypen — wann waskotlin
Mehrere @Bean-Methoden vom selben Typ sind kein Problem — der Bean-Name ist standardmäßig der Methodenname und damit unterscheidbar:
- B2L02-0279 · Den `ApplicationContext` direkt befragenkotlin
Im Alltag injizierst du Beans, indem du sie als Konstruktor-Parameter deklarierst. Manchmal, vor allem in Tests oder zum Debuggen, willst du aber direkt in den Container schauen.
- B2L02-0281 · Interface und zwei Implementations anlegentext
Lege folgende Package-Struktur an:
- B2L02-0301 · Interface und zwei Implementations anlegenkotlin
greeting/FormalGreetingService.kt:
- B2L02-0311 · Interface und zwei Implementations anlegenkotlin
greeting/CasualGreetingService.kt:
- B2L02-0322 · `HelloController` auf Constructor-Injection umbauenkotlin
Ersetze HelloController.kt durch:
- B2L02-0353 · Was passiert *ohne* `@Qualifier`?kotlin
Entferne den @Qualifier-Parameter komplett:
- B2L02-0363 · Was passiert *ohne* `@Qualifier`?text
Starte die App. Erwartet: Die App startet nicht, sondern bricht mit einer Fehlermeldung ab. Such im Stacktrace nach:
- B2L02-0384 · `@Primary` als alternative Lösungkotlin
Im Controller den @Qualifier weglassen:
- B2L02-0395 · `@Configuration`-Klasse mit `@Bean`-Methodekotlin
Lege im Package com.example.hello.greeting an: LoudGreeter.kt
- B2L02-0405 · `@Configuration`-Klasse mit `@Bean`-Methodekotlin
Und eine @Configuration-Klasse: config/GreetingConfig.kt
- B2L02-0425 · `@Configuration`-Klasse mit `@Bean`-Methodekotlin
Test, dass die laute Variante per Qualifier nutzbar ist:
- B2L02-0446 · Container-Inspektion via Testkotlin
Lege folgende Test-Klasse an: projekt/src/test/kotlin/com/example/hello/ContextInspectionTest.kt
- B2L02-0467 · Optional — eigene Qualifier-Annotationkotlin
Wenn du Lust hast, baust du dir eine semantische Qualifier-Annotation. Lege an: projekt/src/main/kotlin/com/example/hello/greeting/Casual.kt
- B2L02-0487 · Optional — eigene Qualifier-Annotationkotlin
Im Controller kannst du jetzt schreiben:
Lesson 3
27 snippets- B2L03-001Phasenübersichttext
Eine Bean wird nicht einfach mit Foo() erzeugt und dann benutzt. Spring durchläuft eine definierte Sequenz von Schritten, in die du an mehreren Stellen eingreifen kannst. Das hier ist die Vollansicht. Meistens nutzt du d
- B2L03-003Phase 2 — Property Populationkotlin
Felder, die mit @Autowired, @Resource, @Value annotiert sind, werden befüllt. Setter werden aufgerufen. Bei reiner Constructor-Injection ist diese Phase ein No-op.
- B2L03-004Phase 5 — Initialisierung: `@PostConstruct`kotlin
Genau hier greifst du im normalen Code ein:
- B2L03-005Phase 8 — Cleanup: `@PreDestroy`kotlin
Wird beim sauberen Shutdown des Containers aufgerufen (ctx.close(), oder bei graceful JVM-Shutdown):
- B2L03-006Das Phänomenkotlin
Schau dir folgende @Configuration-Klasse an:
- B2L03-007Lite-Modekotlin
Wenn du @Bean-Methoden in einer Klasse hast, die nicht mit @Configuration annotiert ist (etwa in einer @Component-Klasse), wird kein CGLIB-Proxy erzeugt. Das ist der Lite-Mode. Folge:
- B2L03-008Wann Lite-Mode trotzdem sinnvoll istkotlin
Es gibt einen einzigen Fall: Wenn du eine @Component hast (klassische Service-Klasse), die zusätzlich eine @Bean-Methode anbietet, und diese Bean unabhängig von anderen @Bean-Methoden derselben Klasse ist. Beispiel:
- B2L03-009`proxyBeanMethods = false`kotlin
Spring 5.2 hat eine Optimierung eingeführt: Wenn du dir sicher bist, dass die Bean-Methoden deiner Config-Klasse einander nicht aufrufen, kannst du den CGLIB-Proxy abschalten:
- B2L03-010`@Value` — punktueller Zugriffkotlin
Du willst Properties aus application.yml in deine Beans bekommen. Es gibt zwei Hauptwege.
- B2L03-011`@ConfigurationProperties` — type-safe Property-Gruppenkotlin
Statt einzelner @Value-Annotations bündelst du eine Property-Gruppe in eine eigene Klasse. In Kotlin nimmst du dafür eine data class mit Constructor-Binding:
- B2L03-016Nutzung im Servicekotlin
Weg B (Scan) oder Weg C ist im modernen Spring Boot idiomatisch. Ich benutze in diesem Kurs Weg B, weil es die @EnableConfigurationProperties-Aufrufe in der Main-Klasse erspart.
- B2L03-018Profile-spezifische Beanskotlin
Hier kommt das eigentlich interessante Pattern. Mit @Profile kannst du eine Bean nur in bestimmten Profilen aktivieren:
- B2L03-019Profile-spezifische Beanskotlin
Statt das auf @Service-Ebene zu machen, kannst du @Profile auch auf @Bean-Methoden in einer @Configuration-Klasse setzen:
- B2L03-020Profile-spezifische `@Configuration`-Klassenkotlin
Manchmal willst du ein ganzes Bündel von Beans nur in einem Profil aktivieren:
- B2L03-0236 · Die `Environment`-API als Programmier-Schnittstellekotlin
Bei @Value und @ConfigurationProperties macht Spring das Property-Binding für dich. Du kannst auch direkt auf die Property-Quellen zugreifen, über das Environment-Bean:
- B2L03-0247 · `@ConditionalOn…`-Annotations für eigene Konfigurationkotlin
Du hast @ConditionalOnClass, @ConditionalOnMissingBean und @ConditionalOnProperty in Lektion 01 als Werkzeug der Auto-Configuration kennengelernt. Du kannst sie auch selbst nutzen, vor allem in eigenen @Configuration-Kla
- B2L03-0262 · `AppMailProperties` als `data class` anlegenkotlin
Lege folgende Datei an: projekt/src/main/kotlin/com/example/hello/config/AppMailProperties.kt
- B2L03-0272 · `AppMailProperties` als `data class` anlegenkotlin
Damit Spring die Klasse als Bean erkennt und befüllt, fügst du @ConfigurationPropertiesScan auf der Main-Klasse hinzu:
- B2L03-0283 · Properties in `application.yml` befüllenyaml
Erweitere projekt/src/main/resources/application.yml:
- B2L03-0293 · Properties in `application.yml` befüllenyaml
Und projekt/src/main/resources/application-dev.yml:
- B2L03-0314 · `MailSender`-Interface und zwei Implementationskotlin
mail/FakeMailSender.kt (Dev und alle Nicht-Prod-Profile):
- B2L03-0324 · `MailSender`-Interface und zwei Implementationskotlin
mail/SmtpMailSender.kt (nur in Prod):
- B2L03-0335 · `MailService` mit Properties und Senderkotlin
mail/MailService.kt:
- B2L03-0346 · Endpoint zum Triggernkotlin
Erweitere HelloController.kt um einen zweiten Endpoint:
- B2L03-046from: noreply@example.com ← auskommentierttext
Erwartet: Die App bricht beim Start ab, mit einer Fehlermeldung wie:
- B2L03-04710 · Bean-Lifecycle in einem Test sichtbar machenkotlin
Lege folgenden Test an: projekt/src/test/kotlin/com/example/hello/MailSenderLifecycleTest.kt
- B2L03-05111 · `Environment`-API als alternative Sichtkotlin
Erweitere den StartupReporter aus Übung B2/L01 um die app.mail-Werte:
Lesson 4
15 snippets- B2L04-0011 · Worum es geht — Laravel-Brücke zuerstphp
In Laravel sieht Routing- und Controller-Code etwa so aus:
- B2L04-0021 · Worum es geht — Laravel-Brücke zuerstkotlin
Spring macht im Grunde dasselbe, packt aber alles in eine Klasse: Routing-Definition und Handler-Methoden stehen beieinander, das Mapping geschieht über Annotations direkt an der Methode. Eine separate Routes-Datei gibt
- B2L04-0032 · `@Controller` vs. `@RestController` — der MVC-Stackkotlin
Spring kommt aus der Model-View-Controller-Welt. Bevor JSON-APIs Standard waren, hat ein Spring-Controller HTML aus Templates (JSP, Thymeleaf, Freemarker) gerendert. Die Default-Annotation dafür ist @Controller. Eine Met
- B2L04-0042 · `@Controller` vs. `@RestController` — der MVC-Stackkotlin
Für JSON-APIs willst du aber das genaue Gegenteil: Der Rückgabewert ist der Body. Du willst keinen View-Resolver dazwischen. Genau das macht @RestController:
- B2L04-0052 · `@Controller` vs. `@RestController` — der MVC-Stackjava
@RestController ist eine Composed Annotation: intern ist sie @Controller plus @ResponseBody. Das @ResponseBody sagt Spring: „Schreib den Rückgabewert direkt in den Response-Body, nutz dafür einen HttpMessageConverter." D
- B2L04-0063 · Request-Mapping — die `@*Mapping`-Familiekotlin
Die zentrale Routing-Annotation ist @RequestMapping. Sie kann an einer Klasse stehen (als Präfix) oder an einer Methode (als konkrete Route). Die Kurzformen @GetMapping, @PostMapping, @PutMapping, @DeleteMapping, @PatchM
- B2L04-007Mehrere Pfade, Methoden, Bedingungenkotlin
@RequestMapping kennt eine Reihe von Attributen, die die Kurzformen erben:
- B2L04-0125 · Query-Parameter — `@RequestParam`kotlin
Die URL GET /users?role=admin&page=2 hat zwei Query-Parameter. So holst du sie:
- B2L04-0187 · Headers und Cookies — `@RequestHeader`, `@CookieValue`kotlin
Beide funktionieren analog zu @RequestParam:
- B2L04-0208 · Response — Return-Type oder `ResponseEntity`kotlin
Wenn du Statuscode, Headers oder Body-Typ beeinflussen willst, packst du das Ganze in ein ResponseEntity<T>:
- B2L04-0241 · User-Domain-Modell anlegenkotlin
projekt/src/main/kotlin/com/example/hello/user/User.kt
- B2L04-0252 · Request-DTO als Data-Classkotlin
projekt/src/main/kotlin/com/example/hello/user/UserCreateRequest.kt
- B2L04-0263 · Controller mit In-Memory-Storagekotlin
projekt/src/main/kotlin/com/example/hello/user/UserController.kt
- B2L04-0295 · IntelliJ-HTTP-Client-Datei anlegenhttp
projekt/src/test/http/users.http
- B2L04-0328 · Bonus — eigene Path-Variable mit Regexkotlin
Erweitere UserController um folgenden Endpoint:
Lesson 5
22 snippets- B2L05-0011 · Worum es geht — Laravel-Brücke zuerstphp
In Laravel hast du dafür zwei Werkzeuge: FormRequest-Klassen für Validation und API-Resources für die Output-Form.
- B2L05-005Annotations am DTO platzieren — und der `@field:`-Stolpersteinkotlin
Validation-Annotations gehören vor die jeweilige Property:
- B2L05-009Was passiert bei Verletzung?json
Bei einem Validation-Fehler wirft Spring eine MethodArgumentNotValidException. Default-Verhalten von Spring Boot 4.0: Diese Exception wird automatisch zu einem 400 Bad Request mit einer Standardform (ProblemDetail nach R
- B2L05-010Verschachtelte DTOs validierenkotlin
Wenn ein DTO ein anderes DTO enthält, musst du @Valid zusätzlich am inneren Feld setzen — sonst greift die Validation nicht in die Tiefe:
- B2L05-011Schritt 1 — Die Annotationkotlin
Die eingebauten Annotations decken 90 % ab. Für domänenspezifische Regeln baust du dir eine eigene. Sie besteht aus zwei Teilen: der Annotation und dem Validator.
- B2L05-012Schritt 2 — Der Validatorkotlin
annotation class ist Kotlins direktes Pendant zu Javas public @interface — gleiche Semantik, kompakteres Schlüsselwort. @Constraint(validatedBy = [PhoneNumberValidator::class]) verknüpft Annotation und Validator-Klasse,
- B2L05-0146 · Über `data class` hinaus — Validation bei klassischen Klassen und Methodenkotlin
Wenn du einmal keine data class verwendest (Legacy-Code, JPA-Entities mit var-Properties), funktioniert Validation an klassischen Properties genauso — der @field:-Prefix bleibt:
- B2L05-0156 · Über `data class` hinaus — Validation bei klassischen Klassen und Methodenkotlin
Spring kann zusätzlich Method-Level-Validation auf Service-Methoden anwenden — dafür setzt du @Validated an die Klasse und die Annotations an die Methodenparameter:
- B2L05-017Schritt 2 — Constraints den Groups zuordnenkotlin
Ein object in Kotlin ist ein Singleton-Container — hier nur als Namespace für die beiden Marker. Die interface-Deklarationen darin werden zu vollwertigen Java-Interfaces im Bytecode, exakt wie Bean Validation sie erwarte
- B2L05-018Schritt 3 — Im Controller die richtige Gruppe triggernkotlin
Lies das so: name ist immer Pflicht. email ist beim Create Pflicht, beim Update optional — falls gesetzt, muss es im Mail-Format sein. Beachte das String? bei email: Beim Update darf der Client das Feld weglassen, also m
- B2L05-020Option A — Manuell, im Servicekotlin
Wenn Eingabe-DTO und Domain-Objekt nicht identisch sind, brauchst du Mapping-Code. Drei Optionen stehen zur Wahl:
- B2L05-021Option B — Eigene Mapper-Klassekotlin
Einfach, explizit, ohne Tooling. Bei fünf bis zehn Feldern völlig in Ordnung. Die Mapping-Funktion als Extension-Function auf der Domain-Klasse zu schreiben (fun User.toResponse()), ist idiomatisches Kotlin — du liest sp
- B2L05-0242 · DTOs in eigene Dateien refaktorisierenkotlin
projekt/src/main/kotlin/com/example/hello/user/UserCreateRequest.kt — überschreiben:
- B2L05-0252 · DTOs in eigene Dateien refaktorisierenkotlin
projekt/src/main/kotlin/com/example/hello/user/UserUpdateRequest.kt — neu:
- B2L05-0262 · DTOs in eigene Dateien refaktorisierenkotlin
projekt/src/main/kotlin/com/example/hello/user/UserResponse.kt — neu:
- B2L05-0273 · Domain-`data class` erweiternkotlin
Erweitere User.kt um phone:
- B2L05-0284 · Eigene Constraint — `@PhoneNumber`kotlin
projekt/src/main/kotlin/com/example/hello/user/PhoneNumber.kt:
- B2L05-0294 · Eigene Constraint — `@PhoneNumber`kotlin
projekt/src/main/kotlin/com/example/hello/user/PhoneNumberValidator.kt:
- B2L05-0305 · `UserController` aufräumenkotlin
Der vollständige Controller danach (du darfst ihn übernehmen, lies ihn aber vorher):
- B2L05-0316 · Negativ-Tests mit invaliden Payloadshttp
Erweitere deine projekt/src/test/http/users.http:
- B2L05-0327 · Bonus — Validation Groups in einem DTOkotlin
projekt/src/main/kotlin/com/example/hello/user/ValidationGroups.kt:
- B2L05-0337 · Bonus — Validation Groups in einem DTOkotlin
UserRequest.kt (neu, parallel zu den beiden bestehenden DTOs):
Lesson 6
21 snippets- B2L06-0011 · Worum es geht — Laravel-Brücke zuerstphp
Schau dir einen typischen Laravel-Controller an, wie er in vielen Projekten aussieht:
- B2L06-0021 · Worum es geht — Laravel-Brücke zuerstphp
Die Lösung in Laravel wäre eine Action- oder Service-Klasse:
- B2L06-0031 · Worum es geht — Laravel-Brücke zuersttext
Spring kanonisiert dieses Muster. Es heißt dort Layered Architecture und ist nicht eine Option unter vielen, sondern die Default-Erwartung an Spring-Code. Jedes Lehrbuch, jedes Tutorial und jede Enterprise-Architektur se
- B2L06-0052 · `@Service` — was diese Annotation eigentlich tutkotlin
In 95 % der Fälle geht es allerdings schlicht um kommunikative Klarheit: Wer @Service sieht, weiß sofort, dass hier Business-Logik wohnt.
- B2L06-0064 · Skinny-Controller-Patternkotlin
Vorher (vereinfachte Variante aus B2/L04 und L05):
- B2L06-0074 · Skinny-Controller-Patternkotlin
Storage und ID-Generierung leben im Controller, das Mapping steckt in einer privaten Methode. Funktioniert, vereint aber drei Verantwortlichkeiten auf einmal.
- B2L06-008Repository als Interfacekotlin
Das Repository ist der Datenzugriffs-Layer. Es kapselt, woher die Daten kommen. In B2/L07 wird daraus Spring Data JPA mit echter DB; jetzt bauen wir eine In-Memory-Variante, die dieselbe Schnittstelle hat. Damit bleibt d
- B2L06-009Implementierung in-memorykotlin
3. Dependency Inversion Principle (DIP) — das „D" in SOLID. Höhere Schichten hängen nicht von konkreten Implementierungen tieferer Schichten ab.
- B2L06-016Service-Schicht — Translationkotlin
Beide sind valide Patterns. Nullable Return ist expliziter, JPA-Exceptions sind im Aufrufcode kompakter. In dieser Lektion bleiben wir bei User?.
- B2L06-0189 · Testbarkeit — der echte Grundkotlin
Die ganze Schichten-Trennung lohnt sich aus einem konkreten Grund: Tests. Der Service ist die wichtigste und gleichzeitig die am besten testbare Schicht deiner App.
- B2L06-019Anemic Domain Model vs. Rich Domain Modelkotlin
Anemic Domain Model: Die Domain-Klasse (User) ist eine reine Datenstruktur ohne Verhalten. Aller Code wandert in den Service:
- B2L06-02111 · `@Transactional` — kurze Vorschaukotlin
In B2/L07 ziehen wir das im Detail durch, hier reicht der Überblick:
- B2L06-02212 · Beispiel-Skelett, das du in der Übung bausttext
Vorausblick auf die fertige Struktur:
- B2L06-0242 · Repository-Interfacekotlin
projekt/src/main/kotlin/com/example/hello/user/UserRepository.kt:
- B2L06-0253 · In-Memory-Implementierungkotlin
projekt/src/main/kotlin/com/example/hello/user/InMemoryUserRepository.kt:
- B2L06-0264 · Mapper-Klassekotlin
projekt/src/main/kotlin/com/example/hello/user/UserMapper.kt:
- B2L06-0275 · Service-Schichtkotlin
projekt/src/main/kotlin/com/example/hello/user/UserService.kt:
- B2L06-0286 · Controller — jetzt skinnykotlin
projekt/src/main/kotlin/com/example/hello/user/UserController.kt komplett ersetzen:
- B2L06-0318 · Unit-Test für `UserService`kotlin
projekt/src/test/kotlin/com/example/hello/user/UserServiceTest.kt:
- B2L06-0328 · Unit-Test für `UserService`kotlin
projekt/src/test/kotlin/com/example/hello/user/FakeUserRepository.kt:
- B2L06-0349 · Verzeichnis-Checktext
Nach dem Refactor sollte dein User-Package so aussehen:
Lesson 7
21 snippets- B2L07-0012 · Anatomie einer Aggregate-Klassekotlin
Wir bauen ein einfaches Beispiel: User mit eingebetteten Address-Objekten.
- B2L07-003Beispiel-Aufruf aus einem Servicekotlin
meisten Operationen unhandlich ist.
- B2L07-0044 · Build-Setup — Gradle KTS und H2kotlin
build.gradle.kts:
- B2L07-0054 · Build-Setup — Gradle KTS und H2yaml
src/main/resources/application.yml:
- B2L07-0064 · Build-Setup — Gradle KTS und H2sql
src/main/resources/schema.sql:
- B2L07-0086 · Custom-Queries mit `@Query`kotlin
Für alles, was über simple Property-Lookups hinausgeht, nimmst du
- B2L07-0097 · Beziehungen jenseits 1:nkotlin
Data JDBC kennt mehrere Beziehungs-Typen, alle ohne @OneToMany/@ManyToOne:
- B2L07-0107 · Beziehungen jenseits 1:nkotlin
Referenzen statt Verschachtelung:
- B2L07-0118 · Tests mit `@DataJdbcTest` + Testcontainerskotlin
hochfährt — nicht die ganze App:
- B2L07-0129 · Auditing — `createdAt`, `lastModifiedAt`kotlin
Wer Audit-Spalten will, aktiviert sie mit zwei Annotations:
- B2L07-0131 · Dependencies hinzufügenkotlin
Bearbeite build.gradle.kts:
- B2L07-0142 · Schema und application.ymlsql
src/main/resources/schema.sql:
- B2L07-0152 · Schema und application.ymlyaml
src/main/resources/application.yml:
- B2L07-0163 · Domain-Klassenkotlin
src/main/kotlin/com/example/users/domain/User.kt:
- B2L07-0173 · Domain-Klassenkotlin
Audit-Date aktivieren — src/main/kotlin/com/example/users/UsersApplication.kt:
- B2L07-0184 · Repositorykotlin
src/main/kotlin/com/example/users/domain/UserRepository.kt:
- B2L07-0195 · Service und Controllerkotlin
src/main/kotlin/com/example/users/application/UserService.kt:
- B2L07-0205 · Service und Controllerkotlin
src/main/kotlin/com/example/users/web/UserController.kt:
- B2L07-0226 · Manuell testenbash
In einem zweiten Terminal:
- B2L07-0237 · Test mit `@DataJdbcTest` + Testcontainerskotlin
src/test/kotlin/com/example/users/UserRepositoryTest.kt:
- B2L07-0278 · Optional: Reuse-Container-Mode (~5 Min)kotlin
Und im Test:
Lesson 8
21 snippets- B2L08-0022 · `@ExceptionHandler` auf Controller-Ebene — die lokale Lösungkotlin
Du kannst pro Controller einzelne Exception-Typen abfangen:
- B2L08-0033 · `@RestControllerAdvice` — die globale Fehler-Stellekotlin
Laravel-Brücke direkt: app/Exceptions/Handler.php mit seinem register()-Aufruf, der pro Exception-Klasse einen Callback registriert. Spring macht es per Annotation auf Methoden in einer Bean.
- B2L08-0054 · `ProblemDetail` und RFC 7807 — das moderne Error-Formatjson
[RFC 7807](https://www.rfc-editor.org/rfc/rfc7807) definiert ein JSON-Format für HTTP-Fehler. Der zugehörige Content-Type ist application/problem+json. Vorgeschriebene Felder:
- B2L08-006Konstruktionkotlin
Wann nutzen? Immer, wenn du eine API schreibst. Drei tragende Gründe:
- B2L08-0085 · Eigene Exception-Hierarchiekotlin
Bevor du Handler schreibst, brauchst du Exceptions zum Handhaben. Bau eine kleine Hierarchie pro Bounded Context:
- B2L08-0095 · Eigene Exception-Hierarchiekotlin
) : RuntimeException(message, cause)
- B2L08-0117 · Validation-Errors strukturiert zurückgebenkotlin
Was du brauchst: eine flache Liste der Field-Errors mit Feldname und Message.
- B2L08-0127 · Validation-Errors strukturiert zurückgebenjson
Response sieht dann so aus:
- B2L08-0138 · `ResponseEntityExceptionHandler` als Basis-Klassekotlin
Spring bringt eine fertige Basis-Klasse mit, die viele Standard-Web-Exceptions schon kennt: ResponseEntityExceptionHandler. Du erbst davon und überschreibst nur, was du anders willst.
- B2L08-01410 · `DataIntegrityViolationException` — der Constraint-Verletzungs-Klassikerkotlin
Du hast in deinem Service in B2/L07 sauber mit existsByEmail vorgeprüft, aber das ist race-condition-anfällig. Zwischen deinem Check und dem Insert kann eine andere Request schon den Eintrag angelegt haben. Der DB-Constr
- B2L08-01511 · Logging-Strategie — was, wo, wiekotlin
Praktisches Pattern im Handler:
- B2L08-01612 · `ResponseEntity` vs. direkter Return-Typ — Stilfragenkotlin
Beide gültig:
- B2L08-01714 · Kotlin-Ergänzung — Sealed Errors als alternatives Patternkotlin
In funktionalen Stacks (Arrow, Kotlin-Domain-Layer) siehst du oft ein Pattern, das Fehler nicht als Exceptions wirft, sondern als algebraischen Datentyp im Return-Wert zurückgibt:
- B2L08-01814 · Kotlin-Ergänzung — Sealed Errors als alternatives Patternkotlin
Im Controller mappst du den Result-Typ explizit auf HTTP-Antworten:
- B2L08-0191 · Exception-Hierarchie sauber ziehenkotlin
Lege ein eigenes Package com.example.projekt.error an (passe an deinen Package-Namen an). Darin:
- B2L08-0201 · Exception-Hierarchie sauber ziehenkotlin
Die bestehenden Exceptions migrieren — UserNotFoundException und DuplicateEmailException erben jetzt von DomainException statt direkt von RuntimeException. Felder am Exception-Objekt ergänzen, damit der Handler sie ausle
- B2L08-0212 · `GlobalExceptionHandler` anlegenkotlin
Im selben error-Package:
- B2L08-0223 · Gegen jeden Handler testenhttp
requests.http ergänzen:
- B2L08-0244 · `existsByEmail` rausnehmen — testen, ob der DB-Constraint fängtkotlin
Im UserService.create() die existsByEmail-Vorprüfung temporär auskommentieren:
- B2L08-0256 · Bonus — Trace-ID in jeden Responsekotlin
Lege einen Filter an, der pro Request eine Trace-ID generiert, in MDC packt und im Response-Header ausliefert:
- B2L08-0287 · Optional — Sealed-Errors-Variante ausprobierenkotlin
Für eine kleine Vertiefung kannst du den findById-Pfad parallel als sealed-Errors-Variante anlegen, ohne den existierenden Controller zu brechen:
Lesson 9
23 snippets- B2L09-0021 · YAML vs. Properties — und warum YAML gewinntyaml
application.yml (modern):
- B2L09-006oder via ENV (am häufigsten in Container):yaml
Beispiel — Dev-Setup:
- B2L09-007Multi-Document-YAMLyaml
Statt zwei separate Files kannst du in einer Datei mit --- mehrere Dokumente bündeln und pro Block einen Profile-Filter setzen:
- B2L09-010Modernes Pattern mit `data class`kotlin
Die Idee: Statt einzelne Werte mit @Value("\${app.foo}") einzustreuen, mappst du einen ganzen Config-Block auf eine Kotlin-Klasse. Vorteile: Type-Safety, IDE-Completion, Validation an einer Stelle, refactorbar.
- B2L09-011Modernes Pattern mit `data class`kotlin
Aktivierung — eine Annotation auf einer @Configuration-Klasse (oder auf der Main-App-Klasse):
- B2L09-013Modernes Pattern mit `data class`yaml
In application.yml:
- B2L09-014Nutzen im Servicekotlin
Wichtig in Kotlin: Constraint-Annotationen brauchen das @field:-Target, damit sie tatsächlich am Field landen und nicht am Constructor-Parameter. Also @field:NotBlank val from: String, nicht nur @NotBlank. Spring liest d
- B2L09-0167 · Logging — Logback ist Defaultkotlin
Spring Boot bringt Logback als Default-Logger mit (zugeordnet via SLF4J). Du nutzt im Code:
- B2L09-017Config in `application.yml`yaml
Level-Hierarchie: TRACE < DEBUG < INFO < WARN < ERROR < OFF. Default-Level für root ist INFO.
- B2L09-020Structured Logging (JSON) für Productionxml
src/main/resources/logback-spring.xml:
- B2L09-0228 · Actuator — Production-Operability als Bordmittelyaml
Per Default ist nur /actuator/health exposed (das reicht für Container-Healthchecks). Die anderen schaltest du explizit frei:
- B2L09-0238 · Actuator — Production-Operability als Bordmittelyaml
Security-Hinweis: Actuator-Endpoints sind ein Daten-Leak-Risiko. In Production läufst du sie auf einem separaten Management-Port und schützt sie mit Spring Security (oder per Network-Policy auf Cluster-Ebene):
- B2L09-0249 · `@ConditionalOn…` — Feature-Toggles auf Bean-Ebenekotlin
Manchmal willst du eine ganze Bean nur unter bestimmten Bedingungen registrieren:
- B2L09-0251 · Default-Config schlank, Profile-Files separatyaml
Neues application.yml (nur das, was in beiden Profilen gilt):
- B2L09-0261 · Default-Config schlank, Profile-Files separatyaml
Neues application-dev.yml:
- B2L09-0271 · Default-Config schlank, Profile-Files separatyaml
Neues application-prod.yml:
- B2L09-0292 · `AppFeatureFlags` als validierte `data class`-`@ConfigurationProperties`kotlin
Neues Package com.example.projekt.config:
- B2L09-0302 · `AppFeatureFlags` als validierte `data class`-`@ConfigurationProperties`kotlin
Auf der App-Hauptklasse @ConfigurationPropertiesScan ergänzen:
- B2L09-0313 · `FeatureFlagsService` — die Properties im Code nutzenkotlin
runApplication<ProjektApplication>(args)
- B2L09-0323 · `FeatureFlagsService` — die Properties im Code nutzenkotlin
Im UserController-Paket einen Debug-Endpoint einbauen, der die Flags lebendig zeigt:
- B2L09-0406 · Bonus 1 — Actuator-Healthyaml
In application.yml:
- B2L09-0437 · Bonus 2 — `@ConditionalOnProperty` für Strategy-Wechselkotlin
Lege zwei SearchEngine-Implementierungen an und steuere per Flag, welche aktiv ist:
- B2L09-0447 · Bonus 2 — `@ConditionalOnProperty` für Strategy-Wechselkotlin
Ein Controller, der einfach den SearchEngine-Bean injecten lässt und nicht weiß, welche Implementation kommt:
Lesson 10
20 snippets- B2L10-002Annotations, die du sofort brauchstkotlin
JUnit ist das, was PHPUnit für PHP ist: das Test-Runner-Framework. Es gibt JUnit 4 (das ältere) und JUnit 5 (auch „Jupiter" genannt, seit 2017). Im Spring-Boot-Starter ist JUnit 5 Standard. Bei alten Code-Beispielen aus
- B2L10-005`@Nested` — Tests gruppierenkotlin
Wenn eine Klasse mehrere Szenarien testet, gruppierst du sie in @Nested-Inner-Classes. Das gibt dir hierarchische Test-Reports und einen klaren Setup-Scope pro Szenario:
- B2L10-006`@ParameterizedTest` — denselben Test mit verschiedenen Inputskotlin
Wichtig in Kotlin: inner class, nicht nur class. Sonst hat die innere Klasse keinen Zugriff auf die Felder der äußeren — und JUnit erkennt sie nicht als verschachtelte Test-Klasse.
- B2L10-007kotlin-test-Stilkotlin
Beide vertragen sich. Faustregel: kotlin-test für einfache Asserts, AssertJ wenn die Aussage komplexer wird.
- B2L10-010Standard-Assertions, die du dauernd brauchstkotlin
Eine Kette: links steht was, rechts womit verglichen — wie du es im Kopf sprichst.
- B2L10-012MockK-Grundsyntaxkotlin
> Wenn du in eine bestehende Java-/Kotlin-Codebase einsteigst, die schon Mockito (org.mockito:mockito-core) + org.mockito.kotlin:mockito-kotlin nutzt, lass das so. Beide Welten funktionieren in Spring Boot 4. Für neuen K
- B2L10-015`slot` — das Argument einsammelnkotlin
Wenn du genau wissen willst, was der Service ans Repository übergeben hat, nimmst du einen slot:
- B2L10-017`@MockK`-Annotation für mehrere Mockskotlin
Wenn deine Test-Klasse mehrere Mocks und das Subject-Under-Test braucht, ist die Annotation-Variante kompakter:
- B2L10-0185 · `@SpringBootTest` — der volle Contextkotlin
Wenn du eine echte Bean-Verkabelung testen willst — Service → Repository → Datenbank, alles mit Spring-DI verschaltet —, nutzt du @SpringBootTest:
- B2L10-0206 · `@WebMvcTest` — der Web-Slicekotlin
Wenn du nur den Controller testen willst (Routing, Request-Mapping, Validation, JSON-Serialisierung), brauchst du keinen Persistenz-Stack:
- B2L10-021`MockMvc` — der eingebaute HTTP-Testerkotlin
MockMvc simuliert HTTP-Requests gegen deinen Controller, ohne echten Tomcat. Es ist schnell und sehr realistisch — der gesamte MVC-Stack läuft (Routing, Argument-Resolver, Validation, Response-Konverter).
- B2L10-022Validation-Fehler testenkotlin
jsonPath(...) ist eine JSONPath-Expression. $ ist die Wurzel, $.name ist das Top-Level-Property name, $.users[0].name der name des ersten Array-Elements unter users.
- B2L10-0247 · `@DataJdbcTest` — der Persistenz-Slicekotlin
Repositories und Custom-Queries willst du gegen eine echte Datenbank testen, aber nicht den ganzen Webstack hochfahren. Das macht @DataJdbcTest für Spring Data JDBC (B2/L07):
- B2L10-0268 · `@MockkBean` vs. `mockk<T>()` — der wichtigste Unterschiedkotlin
Warum überhaupt zwei? Weil ein lokales mockk<T>() ein reines Kotlin-Feld in deiner Test-Klasse erzeugt, @MockkBean aber die Bean im Spring-Container ersetzt. In einem @WebMvcTest-Test braucht der Controller einen UserSer
- B2L10-029Test mit `@ServiceConnection`kotlin
Spring Boot bringt zusätzlich die spring-boot-testcontainers-Integration mit der @ServiceConnection-Annotation, die seit Boot 3.1 das Zusammenstecken trivial macht. Du hast das Pattern in B2/L07 schon gesehen.
- B2L10-030Setupkotlin
RestTestClient ist nicht automatisch in @SpringBootTest injiziert; du aktivierst ihn mit @AutoConfigureRestTestClient:
- B2L10-031`@Sql` — SQL-Dateien vor dem Test ausführenkotlin
> Hinweis Boot 3 → Boot 4 Migration: Wenn du Tutorials/Snippets aus 2024 findest, die WebTestClient in einem @SpringBootTest-Servlet-Setup zeigen, ist das nicht falsch, aber nicht mehr idiomatisch. Ersetze für neue Tests
- B2L10-032`application-test.yml` + `@ActiveProfiles`yaml
Die Datei src/test/resources/sql/three-users.sql enthält Plain-SQL-Inserts. Praktisch für Setup, das du in vielen Tests wiederverwendest.
- B2L10-03413 · Coverage mit Jacoco — kurzkotlin
Spring-Tutorials erwähnen Coverage selten. Wenn du es willst, ist Jacoco der Standard. Plugin in build.gradle.kts:
- B2L10-03615 · Kotest-Assertions als Alternativekotlin
testImplementation("io.kotest:kotest-assertions-core:5.9.1")
Lesson 11
27 snippets- B2L11-001Starter und Importskotlin
Es ist die Library für Auth in der Spring-Welt. Alles andere ist Selbstbau, und das macht niemand ernsthaft.
- B2L11-0032 · Die Filter-Chain — was *wirklich* passiert, wenn ein Request reinkommttext
Spring Security ist in Wahrheit eine Servlet-Filter-Chain. Wenn ein HTTP-Request hereinkommt, läuft er durch eine Reihe von Filtern, bevor er deinen Controller erreicht:
- B2L11-004Historischer Kontext — `WebSecurityConfigurerAdapter` ist totjava
Wenn du älteren Spring-Code liest, siehst du oft so etwas:
- B2L11-0053 · `SecurityFilterChain` — die moderne Konfigurationkotlin
Heute schreibst du eine @Configuration-Klasse, die eine SecurityFilterChain-Bean zurückgibt. Spring Security baut daraus den FilterChainProxy. In Kotlin nutzt du dafür die Kotlin-DSL aus spring-security-config:
- B2L11-006Mehrere `SecurityFilterChain`s — z. B. API vs. Webkotlin
Wenn du eine App hast, die sowohl eine API (/api/) als auch eine Web-UI (/admin/) bedient, kannst du zwei Filter-Chains definieren — je nach URL eine andere Auth-Strategie:
- B2L11-009Variante A — In-Memory (für Demos, Tests, kleine Apps)kotlin
fun loadUserByUsername(username: String): UserDetails
- B2L11-010Variante B — JPA-User aus eigener Entitykotlin
Wenn du eine eigene AppUser-Entity mit username, passwordHash und Rollen hast, schreibst du dir einen UserDetailsService, der diese aus dem AppUserRepository lädt:
- B2L11-0126 · `PasswordEncoder` — Passwörter sicher hashenkotlin
OWASP empfiehlt für neue Apps seit 2023 Argon2id als ersten Algorithmus, BCrypt als akzeptable Alternative, scrypt und PBKDF2 als Drittes. Spring Security hat das alles mit dabei:
- B2L11-0157 · Form-Login — für klassische Server-Side-Appskotlin
Spring Boot rendert mit einer einzigen Zeile (formLogin { }) ein komplettes Login-Formular, inklusive Default-CSS und Submit-Handling. Das ist die typische Wahl für Server-Side-Apps mit Thymeleaf (Buch 3).
- B2L11-0167 · Form-Login — für klassische Server-Side-Appshtml
loginPage = "/login" heißt: Spring Security rendert nicht mehr selbst, sondern erwartet, dass dein Controller die /login-Route bedient. Du musst dann selbst ein Thymeleaf-Template bauen:
- B2L11-018Konfigurationkotlin
Der idiomatische Weg in Spring Boot 4 ist spring-boot-starter-oauth2-resource-server. Das klingt nach „brauche ich nur, wenn ich OAuth2 mache", ist aber auch der Standard für „mein Service akzeptiert JWTs, egal woher die
- B2L11-019JWT-Decoder konfigurierenyaml
Symmetrisch (HMAC) — gemeinsamer Secret-Key, einfach, gut für kleine Apps:
- B2L11-020application.ymlkotlin
Plus eine JwtDecoder-Bean, weil Symmetric-Keys nicht aus Auto-Discovery kommen:
- B2L11-021application.ymlyaml
Asymmetrisch (RSA, JWKS) — Public-Key-Cryptography, der passende Weg, wenn ein externer Identity-Provider (Keycloak, Auth0, AWS Cognito) die Tokens ausstellt:
- B2L11-022Tokens selbst ausstellen — minimaler Auth-Endpointkotlin
Für Bonus-Aufgaben/Übungs-Zwecke kannst du dir einen POST /auth/login schreiben, der einen JWT zurückgibt:
- B2L11-023Tokens selbst ausstellen — minimaler Auth-Endpointkotlin
Den JwtEncoder und AuthenticationManager brauchst du als Beans:
- B2L11-025`@PreAuthorize` — Check vor dem Aufrufkotlin
@EnableMethodSecurity ist seit Spring Security 5.6 der Standard und seit 6 mit prePostEnabled = true als Default. In Security 7 ist @EnableGlobalMethodSecurity entfernt — ältere Codebases müssen migrieren, sonst kompilie
- B2L11-02811 · CSRF — wann an, wann auskotlin
Faustregeln:
- B2L11-02912 · CORS — Cross-Origin sauberkotlin
Spring Security muss CORS-Preflight-Requests (OPTIONS …) bewusst durchlassen. Idiomatisch:
- B2L11-03013 · Security-Header — was Spring Security still ausliefertkotlin
Du kannst sie an- und ausschalten:
- B2L11-031Variante A — `@WithMockUser`kotlin
Wenn deine App geschützt ist, schlagen alle deine MockMvc-Tests aus B2/L10 sofort mit 401 fehl. Dafür gibt es spring-security-test mit zwei wichtigen Mechanismen.
- B2L11-032Variante B — Request-Postprocessorkotlin
@WithMockUser setzt vor dem Test einen authentifizierten SecurityContext mit dem gegebenen Username und Rollen. Nach dem Test räumt es auf.
- B2L11-03417 · Kotlin-Vertiefung — Rollen als sealed hierarchy (optional)kotlin
Wenn dir der String-Vergleich auf "ROLEADMIN" zu lose ist, lohnt eine typsichere Rolle. In Kotlin geht das mit einer sealed-Klasse:
- B2L11-03618 · JWT mit Authority-Mapping — kurzer Ausblickkotlin
Der Default-JwtAuthenticationConverter extrahiert Authorities aus dem scope/scp-Claim und hängt das Prefix SCOPE davor. Wenn dein Identity-Provider Rollen unter roles oder realmaccess.roles (Keycloak) ausliefert, brauchs
- B2L11-0382 · In-Memory Users + Security-Konfigurationbash
Manuell testen mit curl:
- B2L11-0435 · Bonus — JWT-Issue-Endpoint + JWT-Protected GETkotlin
JwtDecoder- und JwtEncoder-Beans im SecurityConfig definieren:
- B2L11-0445 · Bonus — JWT-Issue-Endpoint + JWT-Protected GETkotlin
Tipp zum JSON-Parsing in MockMvc (Kotlin-DSL):
Lesson 12
39 snippets- B2L12-0022 · Setup — Thymeleaf in Spring Boottext
Verzeichnis-Layout, das im Rest der Lektion vorausgesetzt wird:
- B2L12-0043 · `@Controller` vs. `@RestController` — der View-Resolver-Pfadkotlin
Kurze Wiederholung mit Schwerpunkt: Du hast einen @RestController in B2/L04 kennengelernt. Jetzt zurück zur normalen Form.
- B2L12-0053 · `@Controller` vs. `@RestController` — der View-Resolver-Pfadphp
Laravel-Brücke — derselbe Controller in Laravel:
- B2L12-006Alternative — `ModelAndView`kotlin
Wenn du View-Name und Model in einem Objekt zurückgeben willst:
- B2L12-0074 · Die Model-API im Detailkotlin
Du gibst Daten ins Template ausschließlich über Model (oder ModelAndView, oder ein paar Edge-Cases mit @ModelAttribute-Methoden). Schau dir die wichtigsten Wege an:
- B2L12-008Globale Model-Attributes via `@ModelAttribute`-Methodekotlin
Wenn du in jedem Request denselben Wert ins Model legen willst (Beispiel: aktueller Benutzer, Build-Version, Feature-Flags), schreib eine @ModelAttribute-Methode in den Controller oder in einen @ControllerAdvice:
- B2L12-009Globale Model-Attributes via `@ModelAttribute`-Methodekotlin
Vor jedem Request in diesem Controller wird appVersion ins Model gelegt — im Template als ${appVersion} verfügbar. Wenn du das App-weit haben willst:
- B2L12-016Schleifen — `th:each`html
Wenn du den Index brauchst:
- B2L12-022Was SpEL kannhtml
> val/var-Property automatisch.
- B2L12-032Variante A — natives Fragment-Pattern (ohne Layout-Dialect)html
templates/layouts/base.html:
- B2L12-033Variante A — natives Fragment-Pattern (ohne Layout-Dialect)html
templates/users/list.html — die konkrete Seite:
- B2L12-035Variante B — Thymeleaf Layout Dialect (näher an Blade)html
templates/layouts/base.html:
- B2L12-036Variante B — Thymeleaf Layout Dialect (näher an Blade)html
templates/users/list.html:
- B2L12-037Variante B — Thymeleaf Layout Dialect (näher an Blade)blade
Blade-Pendant:
- B2L12-038Wiederverwendbare Fragments — kleine Partialshtml
templates/users/row.html:
- B2L12-040Dateien anlegenproperties
src/main/resources/messages.properties (Default-/Fallback-Sprache):
- B2L12-041Dateien anlegenproperties
src/main/resources/messagesde.properties (Deutsch):
- B2L12-042Dateien anlegenyaml
Spring Boot scannt messages.properties im Default-Konfigurationspfad automatisch. Du kannst alles via application.yml anpassen:
- B2L12-044LocaleResolver — wie Spring entscheidet, welche Sprachekotlin
Konfiguration in einer @Configuration-Klasse:
- B2L12-047Cache-Busting (kurz)yaml
In Produktion willst du, dass Nutzer nach einem Deployment das neue CSS sehen — nicht das alte aus ihrem Browser-Cache. Spring Boot kann das mit Content-Hash-basierten URLs lösen:
- B2L12-04812 · Historischer Kontext — JSPs vs. Thymeleafjsp
Vor Thymeleaf war im Spring-Stack JSP (JavaServer Pages) Standard für Server-Rendering — vergleichbar mit dem Vor-Blade-Pendant <?= $foo ?> in alten PHP-Templates. JSP-Templates sahen so aus:
- B2L12-04913 · Bonus — Thymeleaf + htmx als 2026-Stackhtml
[htmx](https://htmx.org) ist eine kleine JavaScript-Library, die HTML-Attribute hinzufügt, mit denen du AJAX, WebSockets, Server-Sent-Events und DOM-Updates ohne eigenen JavaScript-Code schreibst. Beispiel:
- B2L12-05113 · Bonus — Thymeleaf + htmx als 2026-Stackhtml
Im Template users/list.html:
- B2L12-0541 · Thymeleaf + Layout-Dialect als Dependenciesyaml
In application.yml setz für lokale Entwicklung den Template-Cache aus:
- B2L12-0552 · Verzeichnis-Skelett anlegentext
Lege die Struktur unter src/main/resources/ an:
- B2L12-0564 · `UserViewController`kotlin
Lege einen neuen Controller an, parallel zum bestehenden UserController (REST). Verwende @Controller, nicht @RestController:
- B2L12-0575 · Layout-Template `layouts/base.html`html
Baue das Layout. Es enthält Header mit Logo + Navigation + Locale-Switcher, ein <main> mit dem layout:fragment="content"-Slot, und einen Footer:
- B2L12-0586 · `users/list.html` und das Fragment `_row.html`html
users/list.html erbt vom Layout und rendert die Tabelle. Verwende ein Fragment für die einzelne Tabellenzeile.
- B2L12-0596 · `users/list.html` und das Fragment `_row.html`html
users/row.html:
- B2L12-0607 · `users/detail.html`html
Falls deine User-Entity kein createdAt-Feld hat, lass die Spalte weg oder ersetze sie durch ein Feld, das du tatsächlich hast.
- B2L12-0618 · `messages.properties` und `messages_de.properties`properties
messages.properties (Default, Englisch):
- B2L12-0628 · `messages.properties` und `messages_de.properties`properties
messagesde.properties (Deutsch):
- B2L12-0639 · `error/404.html`html
Spring sucht automatisch nach templates/error/<status>.html, sobald deine App eine Exception in einen 404 verwandelt. Leg eine schlichte Seite an:
- B2L12-0649 · `error/404.html`properties
Ergänze die Keys in beiden messages.properties:
- B2L12-06510 · CSScss
src/main/resources/static/css/app.css — minimal, aber lesbar. Du kannst gerne übertreiben, aber Mindestumfang:
- B2L12-06611 · Security-Anpassung — `/users/*` ist die UI, `/api/users/*` bleibt APIkotlin
Damit die UI mit Form-basierter Authentication läuft (statt Basic-Auth oder JWT für die REST-API), erweitere deine SecurityConfig aus B2/L11 um eine zweite SecurityFilterChain — eine für /api/ (stateless, kein CSRF) und
- B2L12-06813 · Bonus — htmx-Inline-Edit für den User-Namenhtml
Im row.html machst du die Name-Zelle interaktiv:
- B2L12-06913 · Bonus — htmx-Inline-Edit für den User-Namenkotlin
Neuer Controller-Endpoint, der nur ein Fragment zurückgibt:
- B2L12-07113 · Bonus — htmx-Inline-Edit für den User-Namenhtml
Und konfiguriere htmx so, dass er den Header bei jedem nicht-GET-Request automatisch anhängt:
Lesson 13
24 snippets- B2L13-0012 · Das Form-DTOkotlin
Wie bei REST trennst du das Form-Modell von deiner Entity. Ein dediziertes Form-DTO enthält:
- B2L13-0023 · GET → Form anzeigenkotlin
Der Controller stellt das leere Form bereit:
- B2L13-0033 · GET → Form anzeigenhtml
Im Template binden wir gegen das Form-Objekt:
- B2L13-0044 · POST → Form verarbeitenkotlin
Hier wird es interessant. Das Standard-Pattern in Spring:
- B2L13-006Programmatisch Errors hinzufügenkotlin
Manchmal schlägt eine Business-Regel im Service fehl, nicht in der Bean-Validation — etwa „E-Mail bereits vergeben". Du fügst dann manuell einen Error in den BindingResult:
- B2L13-010Bei `<select>` und `<textarea>`html
<input type="text" id="name" name="name" value="Ma" class="is-invalid">
- B2L13-018CSRF für AJAX/htmxjs
Und im JavaScript:
- B2L13-019Custom-Validation-Constraintkotlin
Ein wiederkehrender Use-Case: „E-Mail darf nicht schon vergeben sein." Bean-Validation-Stil:
- B2L13-020Custom-Validation-Constraintkotlin
val payload: Array<KClass<out Payload>> = [],
- B2L13-022Wie es funktioniertkotlin
Nach einem erfolgreichen POST willst du dem Nutzer auf der Folgeseite sagen: „User wurde gespeichert". Das ist eine Flash-Message — sie existiert genau für einen Folge-Request, dann ist sie weg.
- B2L13-023Im Layout-Template ausgebenhtml
4. Im Template ist ${flash.success} verfügbar — genau dieses eine Mal.
- B2L13-024Keys, die du überschreiben kannstproperties
Hibernate-Validator hat einen Default-Key pro Constraint, den du in messages.properties überschreibst:
- B2L13-025Custom-Code, den du selbst per rejectValue oder im Constraint ausgelöst hastproperties
user.email.duplicate=This email address is already taken.
- B2L13-03010 · File-Upload — kurzkotlin
<button type="submit">Hochladen</button>
- B2L13-03110 · File-Upload — kurzyaml
Multipart-Support ist in Spring Boot per Default an. Konfigurations-Knöpfe in application.yml:
- B2L13-0341 · `UserFormDto` anlegenkotlin
In com.example.demo.user (parallel zu User-Aggregate und ggf. UserCreateDto/UserUpdateDto für REST):
- B2L13-0352 · Service-Methoden ergänzenkotlin
UserService braucht zwei neue/angepasste Methoden für die UI-Use-Cases. Wenn du sie schon aus der REST-API hast — gut, dann wiederverwenden. Sonst:
- B2L13-0363 · Controller-Methoden im `UserViewController`kotlin
Erweitere den UserViewController aus Übung B2/L12 um die fünf Form-Endpoints:
- B2L13-0374 · `users/form.html`html
Dieselbe Datei für Create und Edit. Der Unterschied steht im formMode-Attribut.
- B2L13-0385 · Flash-Message-Slot im Layouthtml
In layouts/base.html ergänzt du im Layout direkt vor dem Content-Slot:
- B2L13-0396 · Delete-Button in der List-View und Detail-Viewhtml
In users/row.html (oder direkt in list.html) eine zusätzliche Spalte mit einem Delete-Form. Kein Link für Delete — wir brauchen einen POST für CSRF-Schutz:
- B2L13-0406 · Delete-Button in der List-View und Detail-Viewhtml
Auf der Detail-Seite (users/detail.html) ergänzt du in den Actions:
- B2L13-0428 · `messages.properties` und `messages_de.properties` ergänzenproperties
messages.properties — alles in Englisch:
- B2L13-043Delete-Confirmproperties
messagesde.properties — alles in Deutsch:
Book 3 — Enterprise & Architecture
552 snippetsLesson 1
20 snippets- B3L01-0012 · Layered Architecture — die Basis, ihre Schwächentext
In B2/L06 hast du Layered aufgesetzt:
- B3L01-002Schwäche 1 — Der Service kennt die Infrastrukturkotlin
In klassischem Layered ist UserRepository ein CrudRepository<UserEntity, Long>:
- B3L01-003Schwäche 2 — Die Domain trägt Persistenz-Annotationenkotlin
In klassischem Layered hat User Annotationen wie @Table, @Id, @MappedCollection:
- B3L01-004Skizzetext
→ Vertiefung im [Glossar — Adapter / Port](../GLOSSAR.md#adapter--port-hexagonal-architecture).
- B3L01-005Konkretes Spring-Beispielkotlin
Wir refactoren das User-CRUD aus Buch 2. Vorher in Layered:
- B3L01-006Konkretes Spring-Beispieltext
Nach dem hexagonalen Refactor:
- B3L01-007Konkretes Spring-Beispielkotlin
Die Domain-Klasse wird zur reinen Kotlin-Klasse:
- B3L01-008Konkretes Spring-Beispielkotlin
Der Outbound-Port — ein Interface, definiert vom Kern:
- B3L01-009Konkretes Spring-Beispielkotlin
Der Inbound-Port — auch ein Interface:
- B3L01-010Konkretes Spring-Beispielkotlin
Die Use-Case-Implementierung:
- B3L01-011Konkretes Spring-Beispielkotlin
Der Outbound-Adapter:
- B3L01-012Konkretes Spring-Beispielkotlin
Und das Spring-Data-ListCrudRepository ist hinter dem Adapter versteckt:
- B3L01-013Konkretes Spring-Beispielkotlin
Der Inbound-Adapter (Controller):
- B3L01-0144 · Clean Architecture — Uncle Bobs vier Kreisetext
Robert C. Martin (Uncle Bob) hat in seinem Buch „Clean Architecture" (2017) eine sehr ähnliche Idee unter anderem Namen verkauft. Er zeichnet sie als vier konzentrische Kreise:
- B3L01-0155 · Onion Architecture — die mittlere Positiontext
Jeffrey Palermo prägte 2008 Onion Architecture als Reaktion auf klassisches N-Tier. Auch hier konzentrische Kreise:
- B3L01-016Vorher (Layered, Buch 2)kotlin
Hier der ganze Service-Code von vorher (Layered) und nachher (Hexagonal) im Diff-Stil, damit du die Bewegung konkret nachvollziehen kannst.
- B3L01-017Nachher (Hexagonal)kotlin
data class CreateUserRequest(val name: String, val email: String)
- B3L01-018Was du tatsächlich gewinnstkotlin
Layered — der Test braucht einen MockK-Mock auf UserRepository:
- B3L01-019Was du tatsächlich gewinnstkotlin
Hexagonal — der Test braucht eine zweite Implementierung des Outbound-Ports:
- B3L01-0201 · Pakete anlegentext
Im projekt/-Quellbaum unter com.example.app.user strukturierst du um zu:
Lesson 2
22 snippets- B3L02-0013.1 — Entity (im DDD-Sinn) vs. Persistenz-Entitykotlin
Eine DDD-Entity ist ein Verhaltens-Träger:
- B3L02-0023.2 — Value Objectkotlin
Mit Kotlins data class ist das eine Zeile:
- B3L02-0033.2 — Value Objectkotlin
Weitere VO-Beispiele aus dem Alltag:
- B3L02-0053.4 — Aggregate-Persistenz mit Spring Data JDBCkotlin
Konkret: Die Aggregate-Root bekommt @Table und @Id. Kinder hängen über @MappedCollection an der Root und werden mit-geladen und mit-gespeichert. Keine @OneToMany, keine mappedBy, keine Cascade-Annotationen. Die Aggregate
- B3L02-0063.5 — Repository (im DDD-Sinn)kotlin
Achtung — derselbe Begriff, anderer Kontext als in [B2/L07 — Spring Data JDBC](../../02-spring-boot-kotlin/07-spring-data/lesson.md): Dort hieß Repository ein konkretes CrudRepository<T, Id>-Interface. Im DDD-Sinn ist Re
- B3L02-0073.6 — Domain Servicekotlin
Manchmal gibt es Domänen-Logik, die zu keiner Entity natürlich gehört, weil sie zwischen zwei Aggregaten lebt. Klassisches Beispiel: Geld zwischen zwei Konten transferieren.
- B3L02-0083.7 — Domain Eventkotlin
Als Kotlin-data class:
- B3L02-0093.7 — Domain Eventkotlin
Mit Spring kannst du sie über ApplicationEventPublisher veröffentlichen:
- B3L02-0103.7 — Domain Eventkotlin
events.publishEvent(OrderPlaced(order.id, order.customerId, order.totalAmount(), Instant.now()))
- B3L02-011Sealed Hierarchien für Eventskotlin
Mit sealed interface kannst du eine algebraische Datentyp-Struktur für Domain Events bauen, an die der Compiler dich erinnert, wenn du einen Fall vergisst:
- B3L02-012Sealed Hierarchien für Eventskotlin
Ein Handler kann when-Exhaustiveness nutzen — der Compiler prüft, dass alle Varianten abgedeckt sind:
- B3L02-0133.8 — Application Servicekotlin
Genau das, was Spring üblicherweise @Service nennt.
- B3L02-0143.9 — Factorykotlin
Wenn die Erzeugung eines Aggregats komplex ist (mehrere Schritte, Cross-Validierung, Initial-Items), packst du das in eine Factory:
- B3L02-015Anemic Domain Modelkotlin
Das wichtigste Anti-Pattern in DDD-Welt. Eine Domain-Klasse hat nur Getter/Setter, alle Logik liegt in Services:
- B3L02-0165 · `data class` als VOs — die Kotlin-Brückekotlin
data class ist die Kotlin-Antwort auf Value Objects:
- B3L02-0176 · `sealed` für Domain-Events als ADTkotlin
Wenn du eine abgeschlossene Menge von Event-Typen hast, ist sealed interface der korrekte Mechanismus:
- B3L02-0186 · `sealed` für Domain-Events als ADTkotlin
) : PaymentEvent
- B3L02-020Aggregate-Persistenz mit Spring Data JDBCkotlin
Wenn dein DDD-Aggregat auch eine Spring-Data-JDBC-Entity ist:
- B3L02-0211 · Bounded Context anlegentext
Lege unter com.example.app.task parallel zu user einen zweiten Context an, in derselben hexagonalen Struktur:
- B3L02-0236 · Domain Eventkotlin
Lege task/domain/event/TaskAssigned.kt als data class an:
- B3L02-0247 · Use-Case-Port und Application Servicekotlin
task/application/port/in/AssignTaskUseCase.kt:
- B3L02-0257 · Use-Case-Port und Application Servicekotlin
task/application/port/out/TaskRepository.kt:
Lesson 3
28 snippets- B3L03-0011 · Worum es geht — Laravel-Brücke zuersttext
Schau dir die Default-Struktur eines Laravel-Projekts an:
- B3L03-0021 · Worum es geht — Laravel-Brücke zuersttext
Im klassischen Spring-Tutorial-Stil sieht es identisch aus:
- B3L03-0033 · Package-by-Featuretext
In Package-by-Feature sind die Top-Level-Ordner Features (oder Bounded Contexts), nicht Schichten:
- B3L03-0044 · Hybrid — Feature-First mit innerer Schichtungtext
In der Praxis kombinierst du Package-by-Feature mit der hexagonalen Sub-Struktur aus [Lektion 17](../01-architektur-stile/lesson.md):
- B3L03-0055.1 — Setupkotlin
In build.gradle.kts:
- B3L03-0065.2 — Module deklarierenkotlin
Optional kannst du @ApplicationModule per File-Level-Annotation an einer Kotlin-Datei anbringen, die als Package-Marker dient. Kotlin kennt das package-info.java-Konstrukt nicht direkt — der idiomatische Weg ist eine eig
- B3L03-0075.3 — `internal`-Konventiontext
Innerhalb eines Moduls ist standardmäßig alles public sichtbar von anderen Modulen — außer Klassen in einem internal-Sub-Package. Konvention:
- B3L03-0095.4 — Module-Testskotlin
Der wichtigste Mehrwert:
- B3L03-0105.5 — Cross-Module-Communication via Domain Eventskotlin
In user/api/:
- B3L03-0125.5 — Cross-Module-Communication via Domain Eventskotlin
Im task/-Modul ein Listener:
- B3L03-0136 · Migrationspfad — Monolith → Modulith → Microservicestext
Eines der nützlichsten Denkmuster:
- B3L03-0147.1 — Top-Level-Strukturtext
Wir nehmen das projekt/ mit user und task (aus Übungen 17 und 18) und ziehen Modulith dazu.
- B3L03-0157.2 — `package-info.kt`kotlin
└── Money.kt
- B3L03-0167.2 — `package-info.kt`kotlin
import org.springframework.modulith.ApplicationModule
- B3L03-0187.3 — `internal` markierentext
1. Modulith-idiomatisch: nur api/ (oder spi/) ist public. Alle anderen Sub-Packages werden zu internal/. Hexagonale Schichten leben innerhalb von internal/:
- B3L03-0207.4 — Module-Test schreibenkotlin
Für die Lehre nehmen wir in der Übung Variante 1 — sie ist Modulith-konform und macht die Sichtbarkeitsregeln direkt sichtbar.
- B3L03-0217.5 — Modul-spezifischer Integrationstestkotlin
Die writeDocumentation() schreibt unter build/spring-modulith-docs/ Markdown-Files mit Modul-Übersicht und PlantUML-Diagrammen. Sehr nützlich für Reviews und Onboarding.
- B3L03-0229 · Laravel-Brücketext
Laravels Default ist klar Package-by-Layer (app/Http/, app/Models/, app/Services/). Package-by-Feature gibt es als Konvention nicht, sondern wird in Domain-Heavy-Projekten manuell aufgebaut:
- B3L03-0231 · Address-Feature anlegentext
Damit wir wirklich drei Module haben, ergänze unter com.example.app.address ein drittes Feature:
- B3L03-0241 · Address-Feature anlegenkotlin
Für AddressEvent bietet sich ein sealed interface an, das du in der api/-Datei direkt mit den drei Varianten kombinierst:
- B3L03-0252 · `user/` und `task/` zu Modulith-Konvention umorganisierentext
Pro Modul gilt jetzt die api//internal/-Konvention. Verschiebe in user/:
- B3L03-0262 · `user/` und `task/` zu Modulith-Konvention umorganisierentext
Dasselbe für task/:
- B3L03-0273 · Modulith-Dependency einbauenkotlin
In build.gradle.kts ergänze:
- B3L03-0284 · `package-info.kt` pro Modulkotlin
Lege für jedes der drei Module eine package-info.kt an. Die @file:-Annotation deklariert das Modul, das Package-Statement positioniert die Datei:
- B3L03-0294 · `package-info.kt` pro Modulkotlin
import org.springframework.modulith.ApplicationModule
- B3L03-0304 · `package-info.kt` pro Modulkotlin
import org.springframework.modulith.ApplicationModule
- B3L03-0315 · Modulith-Testkotlin
Lege src/test/kotlin/com/example/app/ApplicationModulesTest.kt an:
- B3L03-0327 · Modul-spezifischer Integrationstestkotlin
Lege src/test/kotlin/com/example/app/user/UserModuleIntegrationTest.kt an:
Lesson 4
18 snippets- B3L04-0023.1 — SRP: Single Responsibility Principletext
In Spring-Praxis: Wenn dein UserService 30 Methoden hat, ist er zu groß. Splittel nach Use-Case:
- B3L04-0033.2 — OCP: Open/Closed Principlekotlin
Typische Anti-Patterns: Eine when-Kaskade auf einem type-Feld:
- B3L04-0043.2 — OCP: Open/Closed Principlekotlin
OCP-konformer Refactor mit Strategy-Pattern:
- B3L04-0053.2 — OCP: Open/Closed Principlekotlin
class PushNotificationStrategy(...) : NotificationStrategy { / ... / }
- B3L04-0063.3 — LSP: Liskov Substitution Principlekotlin
Klassisches Verletzungs-Beispiel:
- B3L04-0073.3 — LSP: Liskov Substitution Principlekotlin
In Spring-Praxis: JPA-Inheritance-Fallen. Wenn du eine @Inheritance-Hierarchie machst:
- B3L04-0083.4 — ISP: Interface Segregation Principlekotlin
Anti-Pattern in Spring: Ein Mega-Repository:
- B3L04-0093.4 — ISP: Interface Segregation Principlekotlin
ISP-konform:
- B3L04-0104.1 — Replace Conditional with Polymorphismkotlin
Klassischer Refactor, OCP-Anwendung. Mit Kotlins sealed interface und when als Expression elegant — die Permits-Liste ergibt sich aus den Klassen im selben Modul automatisch:
- B3L04-0114.2 — Extract Servicekotlin
Wenn ein Service zu viele Verantwortungen hat:
- B3L04-0124.3 — Introduce Domain Event statt direkter Service-Callkotlin
Anti-Pattern: Ein Service ruft drei andere Services direkt auf.
- B3L04-0134.3 — Introduce Domain Event statt direkter Service-Callkotlin
notificationService.notifyAdmins(user) // direkter Call
- B3L04-0144.4 — Replace Method with Method Objectkotlin
Wenn eine Methode zu viele lokale Variablen hat und sich nur schwer zerteilen lässt, baust du eine Klasse draus:
- B3L04-0154.5 — Move Method (Anemic → Rich Domain)kotlin
Sieh dir das aus [Lektion 02 — Domain-Driven Design](../02-domain-driven-design/lesson.md) angesprochene Refactoring nochmal an:
- B3L04-0225.6 — Service ruft `RestTemplate` direkt (statt eines Ports)kotlin
In [Lektion 02 — Domain-Driven Design](../02-domain-driven-design/lesson.md) behandelt, hier nochmal als Smell: Domain-Klasse hat nur Getter/Setter, alle Verhalten in Services. Refactor mit Move-Method (4.5).
- B3L04-0235.6 — Service ruft `RestTemplate` direkt (statt eines Ports)kotlin
Der Service kennt hier die HTTP-Bibliothek. Wenn die Profil-API durch einen Event-Stream ersetzt wird, fällt der Service auseinander. Hexagonal-Lösung:
- B3L04-0261 · Audit-Notizen anlegenmarkdown
Lege im Wurzel-Verzeichnis des projekt/ eine Datei audit-2026-05-19.md an. Dort dokumentierst du den Befund. Vorlage:
- B3L04-0274 · Einen Refactor wirklich durchführenmarkdown
In der audit-2026-05-19.md ergänzt du nach dem Refactor:
Lesson 5
17 snippets- B3L05-0012.1 · Das Konzeptkotlin
CompletableFuture, das er später auslesen kann).
- B3L05-0022.1 · Das Konzeptkotlin
Aktivieren musst du das pro Anwendung einmal:
- B3L05-0032.3 · Internals — wie `@Async` funktioniertkotlin
Konsequenz Nummer eins, und sie ist groß: Self-Calls funktionieren nicht.
- B3L05-0042.4 · Was `@Async` zurückgibtkotlin
deshalb einen eigenen:
- B3L05-0052.5 · TaskExecutor konfigurierenkotlin
wird. Die Defaults (8 Core-Threads, unbeschränkter Queue) sind für viele Fälle vernünftig. Wenn du sie tunen willst:
- B3L05-0073.2 · Die kanonischen Operationenkotlin
CompletableFuture<T> (Java 8) ist die Antwort: ein Future, mit dem du Pipelines baust.
- B3L05-0083.3 · Mit Spring zusammenkotlin
das beide Pfade sieht.
- B3L05-0093.4 · `allOf` und `anyOf`kotlin
Für N parallele Aufrufe:
- B3L05-0114.5 · Pinning — die zwei Stolperfallenkotlin
Lösung: ReentrantLock statt synchronized. Der Lock kann mit Loom-Awareness parken.
- B3L05-0124.6 · Praktisch sehenkotlin
(hey -n 1000 -c 100 http://localhost:8080/slow), und schau dir die Differenz an:
- B3L05-0145.1 · Aktivierung und Basis-Syntaxkotlin
Dann an irgendeiner Bean-Methode:
- B3L05-0175.3 · Internals — wie `@Scheduled` läuftkotlin
Erste Pflicht: gib dem Scheduler mehr Threads.
- B3L05-0196.1 · Setupsql
Tabelle (Flyway-Migration in B3/L08):
- B3L05-0206.1 · Setupkotlin
lockedby VARCHAR(255) NOT NULL
- B3L05-0237.2 · Setupyaml
implementation("org.springframework.boot:spring-boot-starter-quartz")
- B3L05-0247.3 · Job + Triggerkotlin
wir die als Flyway-Migration ein.
- B3L05-0267.4 · Quartz-Falle: AutoWiring im Jobkotlin
musst du Springs AutowiringSpringBeanJobFactory setzen:
Lesson 6
21 snippets- B3L06-0011 · Warum Spring Batch — und nicht einfach `for`-Schleife mit `@Scheduled`?kotlin
kopiert werden, aggregiert nach Region, das Ergebnis als 500-MB-CSV in einen S3-Bucket geschrieben.
- B3L06-0032 · Die Konzept-Bausteinetext
sich manchmal selbst zusammenbauen — nur seit 18 Jahren ausgereift.
- B3L06-0042.1 · `Job`kotlin
Ein Job ist der Container. Er hat einen Namen, eine geordnete Folge von Steps und einen Lifecycle. Du baust ihn
- B3L06-0052.2 · `Step`kotlin
Stammdaten neu", „verschieb Output-File zu S3" — alles, was nicht „lies viele, verarbeite, schreib viele" ist.
- B3L06-0062.3 · `ItemReader<T>`, `ItemProcessor<I, O>`, `ItemWriter<T>`java
Framework-Source, der bleibt Java):
- B3L06-0073.1 · Wie ein Chunk-Step abläufttext
den letzten Step-Context und startet ab der Stelle, an der es abgebrochen ist. Dafür ist das ganze Persistenz-Theater
- B3L06-0083.1 · Wie ein Chunk-Step abläuftkotlin
Vergleichsweise viel — aber jede ist klein, Rollback-fähig, und die Lock-Dauer pro Tx ist niedrig.
- B3L06-0093.3 · ItemReader — Implementierungen für jeden Anlasskotlin
Millionen Rows.
- B3L06-0103.3 · ItemReader — Implementierungen für jeden Anlasskotlin
den Pages losgelassen.
- B3L06-0113.3 · ItemReader — Implementierungen für jeden Anlasskotlin
FlatFileItemReader<T> liest CSV-/TSV-/Fixed-Width-Files Zeile für Zeile.
- B3L06-0123.4 · ItemProcessor — Transformation und Filterungkotlin
Multi-threaded Steps, Sektion 6.
- B3L06-0133.5 · ItemWriter — die Ziel-Seitekotlin
JdbcBatchItemWriter<T> macht batched SQL-Inserts und -Updates.
- B3L06-0143.5 · ItemWriter — die Ziel-Seitekotlin
FlatFileItemWriter<T> schreibt CSV-/TSV-/Fixed-Width-Files heraus.
- B3L06-0164.1 · Skipkotlin
Bei einem fehlerhaften Item nicht den ganzen Job killen, sondern überspringen und weitermachen.
- B3L06-0195.2 · `JobLauncher` aus `@Scheduled`kotlin
JobParameters. Idiomatisch für Cron-getriggerte Container-Aufrufe (etwa ein Kubernetes-CronJob).
- B3L06-0205.3 · REST-Endpointkotlin
Wenn ein Operator manuell triggern können soll:
- B3L06-0216.2 · Multi-Threaded Stepkotlin
Pro Step parallel mehrere Worker-Threads:
- B3L06-0226.3 · Partitioningkotlin
kein Locking.
- B3L06-0237 · `Listener`s — Hooks rein und rauskotlin
Idiomatisch annotation-basiert (@BeforeStep, @AfterChunk, ...):
- B3L06-0262 · Das Datenmodellkotlin
2.1 Entity User hast du schon. Schreib eine zweite Klasse UserReportRow als Ziel-DTO:
- B3L06-0283 · Job mit zwei Stepskotlin
3.3 Der Job verkettet beide:
Lesson 7
27 snippets- B3L07-0013.1 · Setup mit Dockeryaml
Lokal hochgezogen via Docker:
- B3L07-0033.2 · Dependencyyaml
application.yml:
- B3L07-0043.3 · Exchange/Queue/Binding deklarierenkotlin
Spring kann das beim Boot deklarieren, wenn du die Beans im Context hast:
- B3L07-0053.4 · Producerkotlin
durable=true überlebt einen RabbitMQ-Restart. Default ist false, was du fast nie willst.
- B3L07-0063.5 · Consumerkotlin
mit Content-Type-Header und schickt sie an den Exchange.
- B3L07-0083.6 · Ack-Modi und Manual-Ackkotlin
spring.rabbitmq.listener.simple.acknowledge-mode: manual
- B3L07-0103.8 · Retry mit `@RetryableTopic`-Äquivalent für AMQPkotlin
Spring AMQP hat kein direktes @RetryableTopic — das ist Kafka-spezifisch. Aber via RetryTemplate baust du das
- B3L07-0114.1 · Setup mit Dockeryaml
Retry-Queue.
- B3L07-0134.2 · Dependencyyaml
Default. Im Lehrtext kannst du Boot-3-Tutorials zu Spring Kafka 1:1 übernehmen.
- B3L07-0144.3 · Producerkotlin
enable-auto-commit: false # wir committen selbst nach Verarbeitung
- B3L07-0154.4 · Consumerkotlin
standardmäßig aktiv, das gibt dir Exactly-Once auf der Producer-Seite.
- B3L07-0164.5 · `@RetryableTopic` — automatisches Retry mit DLQkotlin
Spring Kafka hat seit 2.7 einen schicken Mechanismus:
- B3L07-0186.1 · Idempotency-Keykotlin
processedat TIMESTAMP NOT NULL DEFAULT now()
- B3L07-0196.1 · Idempotency-Keykotlin
assignedAt = Instant.now(),
- B3L07-0206.2 · Conditional Updates / Versionsfelderkotlin
Statt einer dedizierten Idempotency-Tabelle: nutze Versionsfelder im Ziel-Objekt.
- B3L07-0227.2 · Die Lösung — Outbox-Tabellesql
jeder ernste DevOps-Mensch rät davon ab — Locks, Latenz, Operations-Albtraum.
- B3L07-0237.2 · Die Lösung — Outbox-Tabellekotlin
Transaction wie das Business-Update.
- B3L07-0247.2 · Die Lösung — Outbox-Tabellekotlin
Ein separater Publisher liest die Outbox-Tabelle und schickt an den Broker:
- B3L07-0268 · Saga — Vorgriff auf B3/L17text
Kompensations-Schritten für den Rollback-Fall.
- B3L07-0279.1 · Spring's Default — Jacksonkotlin
Event-Klassen — immutable, kompakt, idiomatisch.
- B3L07-02810 · Internals — wie `@KafkaListener` läufttext
MessageListenerContainer erzeugt — der ist im Wesentlichen eine Schleife:
- B3L07-02911 · Tooling — Docker-Compose für die Übungyaml
Komplettes docker-compose.yml für die Übung (RabbitMQ-Pflicht, Kafka-Bonus):
- B3L07-0314 · Outbox-Tabelle und -Servicesql
@PostConstruct-Methode, die die Outbox-Tabelle anlegt:
- B3L07-0324 · Outbox-Tabelle und -Servicekotlin
zusätzlichen Queries:
- B3L07-0335 · Producer mit Outboxkotlin
2. Outbox-Eintrag schreiben:
- B3L07-0346 · Outbox-Publisherkotlin
6.1 Schreib einen OutboxPublisher-Component:
- B3L07-0367 · Notification-Consumer mit Idempotenzkotlin
7.2 NotificationListener:
Lesson 8
15 snippets- B3L08-001Setup in Spring Bootkotlin
Flyway ist das pragmatischste Werkzeug in dieser Kategorie. Du schreibst SQL-Dateien nach einer festen Namens-Konvention, legst sie in einen Classpath-Ordner, und Flyway erledigt den Rest.
- B3L08-002Setup in Spring Bootyaml
Im application.yml minimal:
- B3L08-005Inhalt einer Migrationsql
V001createuserstable.sql:
- B3L08-007Die Meta-Tabelle: `flyway_schema_history`sql
Beim ersten migrate legt Flyway eine Tabelle an:
- B3L08-011Java-basierte Migrationskotlin
Flyway-Migrations sind klassisch in Java geschrieben (das Framework-Source ist Java) — der Aufruf-Konvention nach muss die Klasse im Package db.migration liegen und BaseJavaMigration erweitern. In Kotlin sieht das so aus
- B3L08-012Flyway-CLI für lokales Probe-Migrierenbash
Vor einem Production-Deploy willst du wissen, ob die Migration durchläuft, ohne die App zu starten. Dafür gibt es die Flyway-CLI als Docker-Image:
- B3L08-014Setupyaml
implementation("org.liquibase:liquibase-core")
- B3L08-016Changeset-Struktur (YAML)yaml
db/changelog/001-create-users.yaml:
- B3L08-017XML-Changesetsxml
Die XML-Variante ist die ursprüngliche und liest sich für viele Java-Entwickler vertrauter:
- B3L08-019Spring Modulith: Migration pro Modul (B3/L03)text
In einem Modulith hat jedes Modul seine eigene Verantwortung. Wenn jedes Modul eigene Tabellen besitzt (was die Modulith-Idee verlangt), gehört auch jeder Schema-Teil ins jeweilige Modul:
- B3L08-0216 · Laravel-Brücke — wo Spring strenger istphp
Laravel-Migrations sind PHP-Klassen mit up() und down(), gerne mit dem Schema-Builder geschrieben:
- B3L08-0243 · Aktuelles Schema als `V001__initial_schema.sql` festhaltenyaml
Weg B — Hibernate generieren lassen: vorübergehend in application.yml:
- B3L08-0253 · Aktuelles Schema als `V001__initial_schema.sql` festhaltensql
Das Resultat sieht je nach Entities ungefähr so aus:
- B3L08-0264 · `ddl-auto` umstellen + Baselineyaml
In application.yml:
- B3L08-0295 · Zweite Migration — Task-Priority hinzufügenkotlin
In der Task-Entity (JPA-Variante in Kotlin — kotlin("plugin.jpa") macht den No-Arg-Konstruktor):
Lesson 9
28 snippets- B3L09-0022 · Spring Cache Abstraction — das Konzeptkotlin
implementation("com.github.ben-manes.caffeine:caffeine")
- B3L09-0032 · Spring Cache Abstraction — das Konzeptyaml
runApplication<HelloApplication>(args)
- B3L09-004`@Cacheable` — der Brot-und-Butter-Fallkotlin
Mehr braucht es nicht. Ohne Caffeine im Classpath nimmt Spring einen ConcurrentMapCacheManager (eine simple ConcurrentHashMap) — funktioniert für Lerndemos, hat aber keine Eviction, kein TTL, keine Limits. Sobald es erns
- B3L09-006`@CacheEvict` — invalidierenkotlin
Vorsicht jedoch: Cache-Konsistenz mit @CachePut ist anfällig, sobald andere Wege existieren, die dieselben Daten ändern (DB-Trigger, andere App-Instanzen ohne synchronisiertes Caching, Migrations). Idiomatischer ist oft,
- B3L09-007`@Caching` — mehrere Annotations kombinierenkotlin
Wenn eine Operation mehrere Caches gleichzeitig anfasst (z. B. einen User updaten, der in users und in tasksByUser referenziert wird):
- B3L09-008`@CacheConfig` — DRY für Klassen-weite Cache-Namenkotlin
fun update(id: Long, req: UserUpdateRequest): User { / ... / }
- B3L09-010Condition und Unlesskotlin
Ohne key-Angabe generiert Spring einen Default über SimpleKeyGenerator — das Tuple der Parameter, bzw. SimpleKey.EMPTY bei keinem Parameter. Funktioniert, aber bei refactor-anfälligen Signaturen ist eine explizite Angabe
- B3L09-0113 · Self-Invocation-Falle — diesmal beim Cachingkotlin
Du kennst das schon von @Transactional (B2/L07) und @Async (B3/L05): Spring implementiert die Annotation per AOP-Proxy. Der Container gibt dir nicht deine Klasse, sondern einen Proxy, der deine Klasse umschließt.
- B3L09-0123 · Self-Invocation-Falle — diesmal beim Cachingkotlin
2. Self-Reference-Pattern. Den eigenen Service per Constructor injizieren (mit @Lazy, damit Spring den zirkulären Verweis sonst nicht auflösen muss):
- B3L09-013Caffeine — In-JVM, Default-Wahl für L1yaml
Konfiguration über spring.cache.caffeine.spec:
- B3L09-014Caffeine — In-JVM, Default-Wahl für L1kotlin
Wenn du pro Cache unterschiedliche Specs brauchst, baust du den CacheManager manuell:
- B3L09-016Redis — verteilter L2, Pflicht bei Multi-Instanceyaml
// spring-boot-starter-cache ist eh schon drin
- B3L09-017Redis — verteilter L2, Pflicht bei Multi-Instanceyaml
Redis via Docker:
- B3L09-018compose.yamlkotlin
Neu in Spring Boot 4 — MicrometerTracing in der Redis-Auto-Config: Sobald micrometer-tracing und ein Tracer (Brave oder OpenTelemetry) im Classpath sind, instrumentiert die Redis-Auto-Configuration die Lettuce- bzw. Jedi
- B3L09-020Event-basiertkotlin
Wenn eine Mutation einen Domain-Event auslöst (B3/L02), kannst du Cache-Invalidation als Event-Listener bauen:
- B3L09-023JPA L2-Cache (Hibernate Second-Level)yaml
Über Transaktionen hinweg, in derselben JVM. Du musst ihn explizit aktivieren mit z. B. EHCache:
- B3L09-0282 · `@EnableCaching` aktivierenkotlin
Auf der Hauptklasse:
- B3L09-0293 · Caffeine-Spec setzenyaml
In application.yml:
- B3L09-0304 · `@Cacheable` auf `findById`kotlin
Im UserService:
- B3L09-0314 · `@Cacheable` auf `findById`yaml
Stell sicher, dass application.yml JPA-SQL-Logging hat (aus B2/L07):
- B3L09-0336 · `@CacheEvict` an Update und Deletekotlin
Wenn der zweite Call ein zweites SQL feuert, ist etwas nicht in Ordnung. Erste Verdachtsmomente:
- B3L09-0347 · Stale-Cache provozieren und beweisenbash
Ohne Cache-Evict würde der Cache nach einem Update veraltete Daten ausliefern. Zum Beweis kommentierst du das @CacheEvict temporär aus, startest die App neu und gehst so vor:
- B3L09-0368 · Self-Call-Falle ausprobierenkotlin
Der Fix ist Self-Injection:
- B3L09-0401 · Redis via Dockeryaml
compose.yaml im Projektroot (oder ergänze deine bestehende):
- B3L09-0423 · Konfig auf Redis umstellenyaml
In application.yml:
- B3L09-0434 · JSON-Serialisierung konfigurierenkotlin
Die Java-Default-Serialisierung in Redis funktioniert zwar, ist aber unleserlich und versionierungs-fragil. Stell auf JSON um:
- B3L09-0476 · Multi-Instance-Invalidation testenbash
Im Log von Instance B siehst du keinen findById: DB-Hit und kein SQL. Der Cache-Eintrag, den Instance A geschrieben hat, kommt bei Instance B ohne DB-Zugriff an. Genau das ist der Sinn von Redis.
- B3L09-0487 · Redis von Hand inspizierenbash
In einer rein Caffeine-basierten Multi-Instance-App hättest du dagegen Folgendes gesehen: Instance A evictet seinen lokalen Cache, Instance Bs lokaler Cache hält den Eintrag aber noch — name bleibt veraltet. Genau dafür
Lesson 10
16 snippets- B3L10-0012 · Die Rollen — Resource Owner, Client, Authorization Server, Resource Servertext
OAuth definiert vier Rollen, und es lohnt sich, die im Kopf sauber zu halten:
- B3L10-0025.1 · Authorization Code Flow + PKCE — der Defaulttext
Das ist der Flow für interaktive Apps — Web mit Server-Backend, SPAs (React, Vue, Angular), Mobile-Apps. Seit OAuth 2.1 immer mit PKCE (Proof Key for Code Exchange), auch wenn du Server-Side bist.
- B3L10-0035.2 · Client Credentials Flow — Maschine zu Maschinetext
Kein User im Spiel. Eine Backend-App authentifiziert sich gegen einen Auth-Server und holt sich ein Token für ihre eigene Identität — typisch für Service-to-Service-Kommunikation:
- B3L10-0045.3 · Device Authorization Flow — für TVs, IoT, CLI-Toolstext
Für Geräte ohne richtige Tastatur oder Browser (Smart-TV, Konsolen-CLI, Drucker). Das Prinzip: Das Gerät zeigt einen Code an, der User loggt sich auf einem zweiten Gerät (Smartphone) ein und gibt dort den Code ein.
- B3L10-0057 · Was im JWT drinsteht — Claims im Detailjson
Ein typisches Access-Token von Keycloak sieht decodiert so aus:
- B3L10-0068.1 · Container startenyaml
Im Projektordner eine docker-compose.yml:
- B3L10-0091 · Keycloak via Docker startenyaml
Lege im projekt/-Verzeichnis einen Ordner infra/keycloak/ an mit einer docker-compose.yml:
- B3L10-0127 · Authorization-Code-Flow + PKCE manuell durchspielenbash
Im Terminal:
- B3L10-013Code Challenge: BASE64URL(SHA256(verifier))bash
Schritt 7.2 — Authorization-URL bauen und im Browser öffnen
- B3L10-016Diese URL im Browser öffnenbash
Schritt 7.3 — Code gegen Token tauschen
- B3L10-017Diese URL im Browser öffnenjson
Response (gekürzt):
- B3L10-019Diese URL im Browser öffnenbash
Schritt 7.6 — Refresh-Token verwenden
- B3L10-0208 · Client-Credentials-Flow für Service-Accountsbash
In Keycloak hast du oben bei „Service accounts roles" einen Service-Account aktiviert. Im Clients → task-app → Service accounts roles Tab kannst du diesem die Rolle USER zuweisen (für den Test).
- B3L10-0219 · (Bonus) Device-Code-Flow durchspielenbash
Im Keycloak-Admin: Clients → task-app → Advanced → OAuth 2.0 Device Authorization Grant Enabled: on speichern.
- B3L10-0229 · (Bonus) Device-Code-Flow durchspielenjson
Du bekommst zurück:
- B3L10-0239 · (Bonus) Device-Code-Flow durchspielenbash
Im Terminal pollen:
Lesson 11
22 snippets- B3L11-0022.1 · Dependency und minimale Konfigurationyaml
Mehr ist Code-seitig zunächst nicht nötig. In application.yml:
- B3L11-0032.2 · `SecurityFilterChain` für Resource Serverkotlin
Spring Boot fragt beim Startup automatisch http://localhost:8081/realms/springkurs/.well-known/openid-configuration ab, holt sich von dort die jwksuri und cached die Public Keys. Damit ist der Resource Server konfigurier
- B3L11-0042.3 · Was im Filter passiertkotlin
In deinem Controller:
- B3L11-0052.4 · Authorities aus Keycloak-Roles mappen — der `JwtAuthenticationConverter`kotlin
Keycloak schickt Rollen aber nicht im scope, sondern im realmaccess.roles-Sub-Claim. Wenn du @PreAuthorize("hasRole('ADMIN')") nutzen willst, musst du Spring beibringen, von dort zu lesen:
- B3L11-0082.5 · Resource-spezifische Roles aus Keycloakkotlin
Keycloak unterscheidet Realm Roles (realmaccess.roles) und Client Roles (resourceaccess.<client-id>.roles). Wenn du Rollen lieber pro Client (= App) modellieren willst:
- B3L11-0092.6 · `aud`-Validation hinzufügen — Pflicht in Productionkotlin
Lösung: ein zusätzlicher OAuth2TokenValidator:
- B3L11-0123.2 · Minimales Setup zur Übersichtkotlin
Eine Minimal-Konfiguration:
- B3L11-0144.2 · Client-Registration in `application.yml`yaml
implementation("org.springframework.boot:spring-boot-starter-oauth2-client")
- B3L11-0154.3 · `SecurityFilterChain` für den OAuth-Clientkotlin
{baseUrl} und {registrationId} sind Spring-Platzhalter. Die effektive Redirect-URL wird http://localhost:8080/login/oauth2/code/keycloak — also genau das, was du in Keycloak unter „Valid redirect URIs" eingetragen hast.
- B3L11-0164.4 · `OAuth2User`/`OidcUser` im Controllerkotlin
Was hier passiert:
- B3L11-0174.5 · OAuth-Client und Resource Server in derselben Appkotlin
In manchen Architekturen ist deine Spring-App beides — sie rendert HTML und stellt gleichzeitig eine JSON-API bereit. Dann brauchst du zwei SecurityFilterChain-Beans mit @Order:
- B3L11-0184.6 · Client als API-Aufrufer — `OAuth2AuthorizedClient`kotlin
Wenn deine Server-Side-App im Namen des Users fremde APIs aufrufen will (z. B. Google Drive), brauchst du den Access-Token. Spring stellt ihn via OAuth2AuthorizedClient bereit:
- B3L11-0198.1 · `MockMvc` mit Mock-JWTkotlin
Spring-Security-Test stellt einen JWT-Postprocessor zur Verfügung — damit muss kein echter Keycloak im Test laufen:
- B3L11-0208.2 · Integration-Test gegen echten Keycloakkotlin
Wenn du das Ende-zu-Ende verifizieren willst, geht das mit Testcontainers:
- B3L11-0222 · `application.yml` umstellenyaml
Den vorhandenen starter-security und spring-security-test lässt du drin. Wer in B2/L11 spring-boot-starter-oauth2-resource-server schon fürs Bonus-JWT eingebaut hatte: Property app.jwt.secret und die selbst gebaute JwtEn
- B3L11-0233 · `SecurityConfig.kt` umbauenkotlin
- B3L11-0265 · Tests umstellenkotlin
In UserControllerWebMvcTest.kt:
- B3L11-0276 · End-to-End-Test mit echtem Keycloak (Bonus)kotlin
Lege eine Test-Klasse KeycloakLiveTest.kt an, per Default deaktiviert (mit @Disabled oder @EnabledIfEnvironmentVariable):
- B3L11-0287 · Manueller Smoke-Test mit curlbash
Lass den Test in CI nur laufen, wenn die ENV-Variable KEYCLOAKLIVETEST=true gesetzt ist. Lokal kannst du ihn manuell starten, sobald dein Docker-Compose-Keycloak hochläuft.
- B3L11-0308 · (Bonus) OAuth-Client für die Web-UIyaml
application.yml ergänzen:
- B3L11-0318 · (Bonus) OAuth-Client für die Web-UIkotlin
In SecurityConfig.kt:
- B3L11-0328 · (Bonus) OAuth-Client für die Web-UIkotlin
Eine Mini-Controller-Methode:
Lesson 12
34 snippets- B3L12-0012.2 · RBAC in Springkotlin
In B3/L11 hast du Keycloak-Roles in Spring-Authorities gemappt. Damit hast du RBAC:
- B3L12-0022.2 · RBAC in Springkotlin
Für eine Role-Hierarchy (ADMIN > EDITOR > USER, also Admin erbt alle User-Rechte) legst du eine Bean an:
- B3L12-0043 · ABAC — Attribute-Based, mit Spring SpELkotlin
In Kotlin lässt sich eine Policy direkt als data class modellieren — gut zu lesen, gut zu testen:
- B3L12-0063.2 · Eigene Security-Bean per Namekotlin
Lege eine Bean an, die einen logischen Namen bekommt:
- B3L12-0073.2 · Eigene Security-Bean per Namekotlin
In der annotierten Methode nutzt du sie via SpEL und Bean-Namen mit @-Prefix:
- B3L12-0114.3 · OpenFGA via Docker — der Schnellstartyaml
Alle drei laufen als eigener Service (Docker), stellen eine gRPC- und HTTP-API bereit, deine Spring-App schickt check-Calls dorthin.
- B3L12-0124.3 · OpenFGA via Docker — der Schnellstarttext
Authorization-Model definieren (DSL):
- B3L12-0134.3 · OpenFGA via Docker — der Schnellstartkotlin
Spring-Client-Library: [fga-sdk](https://github.com/openfga/java-sdk). Beispiel-Call in Kotlin:
- B3L12-0156.4 · Strategische Entscheidung — Hibernate-Filter (JPA) vs. manuelle Tenant-Predicate (JDBC)kotlin
Option A — Tenant-Predicate in jedem Repository-Query:
- B3L12-0166.4 · Strategische Entscheidung — Hibernate-Filter (JPA) vs. manuelle Tenant-Predicate (JDBC)kotlin
Option B — TenantContext (ThreadLocal) + AOP-Aspect, der die Tenant-ID einmal pro Request setzt und in jede Repository-Methode automatisch reinreicht:
- B3L12-0176.4 · Strategische Entscheidung — Hibernate-Filter (JPA) vs. manuelle Tenant-Predicate (JDBC)kotlin
Mit einem OncePerRequestFilter, der TenantContext aus dem JWT-Claim füttert:
- B3L12-0196.5 · Beispiel — Option A mit Tenant-Predicate in der Repository-Methodekotlin
Wer bestehende JPA-Codebases pflegt oder die Hibernate-Filter-Ergonomie braucht, findet die @Filter-Variante in B3/L24 (JPA-Legacy). Wir bleiben in der Lektion bei Data JDBC.
- B3L12-0206.5 · Beispiel — Option A mit Tenant-Predicate in der Repository-Methodekotlin
Service-Schicht ruft TenantContext.get() einmal pro Methode auf und reicht den Wert durch:
- B3L12-0217.1 · Decision-Caching mit Caffeinekotlin
Wenn deine ABAC-Logik canRead(taskId) heißt und dazu ein DB-Query macht, hast du bei einem Listing mit 100 Tasks 100 Permission-Checks — 100 zusätzliche Queries. Das ist das N+1-Problem in der Permission-Schicht.
- B3L12-0258.1 · `@CreatedBy` / `@LastModifiedBy` / `@CreatedDate` / `@LastModifiedDate`kotlin
Die Annotations aus org.springframework.data.annotation funktionieren in Spring Data JDBC genauso wie in JPA. Die Task-Entity aus 6.5 hat sie schon. Was du brauchst:
- B3L12-0262 · `Task`-Entity um Owner und Tenant erweiternkotlin
Wenn deine Task-Entity noch nicht existiert, lege sie an. Erweiterte Version als Kotlin data class:
- B3L12-0272 · `Task`-Entity um Owner und Tenant erweiternsql
Flyway-Migration V…addownertenantaudittotasks.sql:
- B3L12-0283 · `TenantContext` + `TenantResolverFilter`kotlin
(Wenn du H2 im Dev-Profil hast: die Spaltentypen passen, BIGINT ist identisch.)
- B3L12-0293 · `TenantContext` + `TenantResolverFilter`kotlin
fun clear() = current.remove()
- B3L12-0304 · Auditing aktivierenkotlin
Wichtig: Der Filter muss nach dem BearerTokenAuthenticationFilter laufen, weil er den SecurityContext schon braucht. Ordered.HIGHESTPRECEDENCE + 10 ist der pragmatische Wert — falls die Reihenfolge nicht greift, FilterCh
- B3L12-0315 · `TaskRepository` mit Tenant-Predicatekotlin
Wichtig: @EnableJdbcAuditing (nicht @EnableJpaAuditing) — wir sind in Data JDBC.
- B3L12-0326 · `TaskSecurityChecker` als ABAC-Beankotlin
Die geerbten findAll(), findById(), deleteById() aus ListCrudRepository benutzt du in produktiven Service-Methoden nicht — sie kennen kein Tenant-Predicate. Halte dich an ForTenant-Varianten.
- B3L12-0337 · `TaskController` und `TaskService`kotlin
Beachte: Der Check nutzt findByIdForTenant — wenn ein User aus Tenant 1 nach Task 42 aus Tenant 2 fragt, gibt das Repository schon null zurück. Der Security-Check fällt damit automatisch auf false. Defense in Depth: zwei
- B3L12-0347 · `TaskController` und `TaskService`kotlin
return ResponseEntity.noContent().build()
- B3L12-0358 · Build-Setup (Gradle KTS, Auszug)kotlin
Beachte den data class-Stil: current.copy(...) für Updates statt Setter. Immutable-by-default ist in Data JDBC der idiomatische Weg.
- B3L12-0369.1 · `TaskSecurityCheckerTest` — Unit, ohne Springkotlin
Kein spring-boot-starter-aop — wir brauchen ihn nicht, weil wir den Hibernate-Filter-Aspect aus der JPA-Welt nicht haben.
- B3L12-0379.2 · `TaskControllerWebMvcTest` mit JWT-Postprocessorkotlin
assertThat(checker.canEdit(1L, userAuth("u1", "USER"))).isFalse
- B3L12-0389.3 · Integration-Test für Tenant-Isolationkotlin
TenantIsolationTest.kt mit Testcontainers Postgres (so wie in B2/L07):
- B3L12-03910 · Manueller Smoke-Test mit zwei Tenantsbash
.andExpect(jsonPath("$[0].title").value("A"))
- B3L12-04111 · (Bonus 1) Postgres Row-Level Security als Defense in Depthkotlin
Ein ConnectionCustomizer, der pro Connection SET app.tenantid setzt:
- B3L12-04212.1 · OpenFGA via Dockeryaml
projekt/infra/openfga/docker-compose.yml:
- B3L12-04312.2 · Authorization-Modeltext
docker compose up -d. Im Browser http://localhost:3000 für den Playground.
- B3L12-04612.3 · Spring-Integrationkotlin
FgaConfig.kt:
- B3L12-04712.3 · Spring-Integrationkotlin
FgaSecurityChecker:
Lesson 13
26 snippets- B3L13-003Level 2 — HTTP-Verben + Status-Codestext
Das ist das, was die meisten meinen, wenn sie „REST" sagen:
- B3L13-004Level 3 — HATEOASjson
Hypermedia as the Engine of Application State. Roy Fielding (der die Diss zu REST geschrieben hat, 2000) versteht unter „echtem REST" eigentlich nur Level 3. Idee: Die Response enthält Links zu allen sinnvollen Folge-Akt
- B3L13-005Level 3 — HATEOASkotlin
Spring bietet dafür Spring HATEOAS (eigene Library, spring-boot-starter-hateoas):
- B3L13-011Pagination, Filtering, Sorting — Konventionenyaml
Stolperfalle: Die Default-Page-Size ist 20. Schickt jemand ?size=1000000, baut Spring das ungebremst aus. Setze einen oberen Deckel:
- B3L13-014Idempotency-Keys für POSTskotlin
Implementierungsskizze:
- B3L13-015Idempotency-Keys für POSTskotlin
Im Controller:
- B3L13-0163 · OpenAPI und SpringDoc — was, warum, wieyaml
OpenAPI (früher Swagger) ist eine YAML- oder JSON-Spezifikation, die eine REST-API komplett maschinenlesbar beschreibt: Endpoints, Parameter, Request- und Response-Schemas, Status-Codes, Auth-Schemes, Beispiele. Aktuelle
- B3L13-018Anreichern mit Annotationenkotlin
Du musst nichts annotieren, um etwas zu sehen — SpringDoc leitet das aus den vorhandenen Annotationen ab. Für Production-taugliche Doku ergänzt du an den passenden Stellen.
- B3L13-019Anreichern mit Annotationenkotlin
@Schema direkt am Property einer data class funktioniert genauso (Spring Boot 3+/4 + Jackson 2.16+):
- B3L13-020Konfigurationyaml
Für Konsumenten deiner API ist das ein erhebliches Geschenk. Für dich selbst ist es ein Smoke-Test während der Entwicklung.
- B3L13-021Konfigurationkotlin
Globale API-Info gibst du in einer @Configuration-Klasse mit einer OpenAPI-Bean an:
- B3L13-022Group-API — getrennte Doku für unterschiedliche Audienceskotlin
SpringDoc unterstützt Groups:
- B3L13-0245.1 · Native Versioning konfigurierenkotlin
Du aktivierst es über einen WebMvcConfigurer-Callback und sagst, wo die Version im Request steht:
- B3L13-0255.1 · Native Versioning konfigurierenyaml
Spring Boot 4 hat parallel einige spring.mvc.apiversion.-Properties (Stand 4.0.6 noch in Bewegung — die WebMvcConfigurer-Variante ist der stabile Weg). Wenn du sie nutzt, ist es analog dazu:
- B3L13-0265.2 · `@RequestMapping(version = ...)` am Endpointkotlin
Sobald der Versioning-Resolver konfiguriert ist, taggst du deine Handler:
- B3L13-0275.4 · OpenAPI-Doku mit Versions-Bewusstseinkotlin
SpringDoc 2.8 versteht das native Versioning. Wenn du Groups nach Version splitten willst:
- B3L13-028Deprecation — `Sunset`-Header (RFC 8594)kotlin
In Spring umgesetzt:
- B3L13-0312 · Globale API-Info konfigurierenkotlin
Neue Datei projekt/src/main/kotlin/com/example/projekt/config/OpenApiConfig.kt:
- B3L13-0322 · Globale API-Info konfigurierenyaml
Optional in application.yml:
- B3L13-0333 · User- und Task-Controller mit `@Operation` anreichernkotlin
Im UserController (Pfad bleibt wie er ist, also /api/v1/users oder was du hast — wir versionieren gleich richtig):
- B3L13-0343 · User- und Task-Controller mit `@Operation` anreichernkotlin
// delete, update analog
- B3L13-0354 · DTOs mit `@Schema` anreichernkotlin
In UserCreateRequest (data class):
- B3L13-0376 · Eine deprecated V1-Old-Variante mit Sunset-Headerkotlin
Lege neuen Controller an: UserControllerV1Deprecated.kt:
- B3L13-0407 · OpenAPI-Spec in einem JUnit-Test validierenkotlin
Neue Test-Klasse projekt/src/test/kotlin/com/example/projekt/openapi/OpenApiSpecTest.kt:
- B3L13-0418 · Bonus — Group-API für public vs. adminkotlin
Wenn dein Projekt schon Admin-Endpoints hat (oder du legst beispielhaft welche an), trennst du sie per GroupedOpenApi:
- B3L13-0429 · Bonus — `Idempotency-Key`-Header dokumentierenkotlin
Wenn du das Stripe-Pattern aus der Lektion umsetzen willst (z. B. für POST /api/v1/tasks), ergänze:
Lesson 14
31 snippets- B3L14-0011 · SLF4J + Logback — die Architekturkotlin
In jeder Klasse, in der du loggen willst, holst du dir einen Logger:
- B3L14-002Ebene 3 — Log4j2 als Alternativekotlin
Auf Log4j2 umschalten in Spring Boot (build.gradle.kts):
- B3L14-003Optional: `kotlin-logging` als ergonomischer Wrapperkotlin
Wer das Idiom mit javaClass oder companion object immer noch klobig findet, kann die Bibliothek io.github.oshai:kotlin-logging dazunehmen. Sie bietet einen SLF4J-Wrapper mit Lambda-API:
- B3L14-0052 · Level-Disziplin — wann wasyaml
Die Logger-Hierarchie folgt der Package-Hierarchie: Ein Logger com.example.projekt.user.UserService erbt das Level seines Parent-Loggers com.example.projekt.user, der erbt von com.example.projekt, der von com.example, de
- B3L14-006Spring Boot Actuator-`/loggers`http
Disziplin in Production:
- B3L14-007Weg A — `application.yml` (Spring Boots Bordmittel)yaml
Reicht für Standardfälle:
- B3L14-008Weg B — `logback-spring.xml` (komplexe Setups)xml
dann src/main/resources/logback-spring.xml:
- B3L14-0094 · MDC — Per-Request-Kontextkotlin
MDC = Mapped Diagnostic Context. Eine Map<String, String> pro Thread (ThreadLocal-basiert), deren Werte automatisch in Log-Patterns interpoliert werden — %X{userId} im Pattern liest aus dem MDC.
- B3L14-010Setzen über einen Filterkotlin
Spring-Idiom: ein OncePerRequestFilter:
- B3L14-011`userId` aus dem SecurityContextkotlin
Wenn der User authentifiziert ist (B2/L11), kommt seine Identität im SecurityContextHolder. Im selben Filter (oder einem nachgelagerten):
- B3L14-012Die ThreadLocal-Falle bei Async/Virtual-Threadskotlin
Lösung: ein TaskDecorator, der den MDC vom aufrufenden Thread auf den Worker kopiert:
- B3L14-013Die ThreadLocal-Falle bei Async/Virtual-Threadskotlin
In der TaskExecutor-Konfiguration:
- B3L14-014Bordmittel (Boot 3.4+, in Boot 4 stabil und erweitert)yaml
Seit Spring Boot 3.4 gibt es logging.structured.format. Boot 4 hat das Feature weiter ausgebaut. Vor allem das ECS-Mapping (Stack-Trace-Felder, Exception-Klassennamen) ist konsistenter, und du kannst per logging.structur
- B3L14-015Bordmittel (Boot 3.4+, in Boot 4 stabil und erweitert)json
Output mit ecs:
- B3L14-016Profile-Trennungyaml
Du willst Plaintext in Dev und JSON in Prod. Pattern:
- B3L14-018Pre-Boot-3.4: Logstash-Encoderxml
implementation("net.logstash.logback:logstash-logback-encoder:7.4")
- B3L14-0196 · PII und Compliance — was nicht ins Log gehörtkotlin
Praktischer Helper für maskiertes Logging:
- B3L14-027Request-Logging-Filterkotlin
Jeden eingehenden Request mit Methode, Pfad, Status, Dauer loggen. Pflicht für jede API.
- B3L14-028Audit-Trail-Loggingkotlin
Geschäftsrelevante Events (User angelegt, Berechtigung geändert, Order angenommen) gehören in ein separates Log und nicht in den Stream mit den Anwendungs-Logs. Pattern:
- B3L14-029Audit-Trail-Loggingxml
Im logback-spring.xml schickst du den Logger AUDIT an einen separaten Appender (eigene Datei, eigener Index im Aggregator):
- B3L14-030Error-Logging im Exception-Handlerkotlin
Aus B2/L08 bekannt, hier in Reinform:
- B3L14-03110 · Correlation-IDs across services (Vorgriff)kotlin
In Spring umgesetzt: einen ClientHttpRequestInterceptor für deinen RestClient/WebClient, der MDC-Werte als Header propagiert.
- B3L14-0321 · `CorrelationIdFilter` bauenkotlin
Neue Datei projekt/src/main/kotlin/com/example/projekt/logging/CorrelationIdFilter.kt:
- B3L14-0352 · Log-Pattern erweitern, damit `correlationId` sichtbar wirdyaml
In application.yml (oder im Default-Block, falls du Profile separiert hast):
- B3L14-0373 · `userId` aus dem SecurityContext in den MDC (optional, wenn Auth vorhanden)kotlin
Neue Datei projekt/src/main/kotlin/com/example/projekt/logging/UserMdcFilter.kt:
- B3L14-0384 · MDC-Propagation für Async via TaskDecoratorkotlin
Erstelle eine Async-Konfiguration, falls noch nicht vorhanden. Datei projekt/src/main/kotlin/com/example/projekt/config/AsyncConfig.kt:
- B3L14-0394 · MDC-Propagation für Async via TaskDecoratorkotlin
Jetzt ein Service mit einer @Async-Methode, um zu zeigen, dass die ID korrekt rüberkommt. Beispiel in UserService oder neu — NotificationService.kt:
- B3L14-0404 · MDC-Propagation für Async via TaskDecoratorkotlin
Und im Controller einen Endpoint, der das aufruft:
- B3L14-0425 · Profile-spezifisches JSON-Logging in `prod`yaml
Erweitere application.yml:
- B3L14-0467 · Request-Logging-Filter (Bonus)kotlin
Wenn du noch Zeit hast: einen RequestLoggingFilter bauen, der pro Request eine Zeile mit Methode, Pfad, Status und Duration loggt:
- B3L14-0478 · Audit-Trail-Logger (Bonus)kotlin
Im UserService einen separaten Logger anlegen:
Lesson 15
4 snippets- B3L15-00215 · Spring Modulith — ein kurzer Blicktext
Modul-Definition via Package-Konvention: Jedes Top-Level-Package unterhalb der @SpringBootApplication-Klasse ist ein Modul. Da Kotlin kein package-info.java kennt, nutzt du eine package-info.kt mit einem @file:JvmName("p
- B3L15-00315 · Spring Modulith — ein kurzer Blickkotlin
Modul-Annotation in Kotlin (File-Level-Annotation in einer leeren ModuleMetadata.kt):
- B3L15-0053 · Schreib die Decision-Docmarkdown
Schreib in projekt/architecture-decision.md eine vollständige Decision-Doc im folgenden Format:
- B3L15-0064 · Modulith-Modul-Spec als Code-Skizzemarkdown
Selbst wenn deine Entscheidung „Microservices" lautet — skizziere als Vorstufe den Modulith-Schnitt: Wenn das Projekt heute als Modulith strukturiert würde, wie sähe die Package-Struktur aus? Schreib in projekt/architect
Lesson 16
45 snippets- B3L16-0013.1 — `RestTemplate` (historisch / deprecated für neue Pfade)kotlin
Der Klassiker seit Spring 3.0 (2009). Synchron, blocking, basiert auf HttpURLConnection oder Apache HttpClient.
- B3L16-0023.2 — `RestClient` (seit Boot 3.2, idiomatischer Default in Boot 4)kotlin
Fluent-API über RestTemplate-Infrastruktur, aber moderner und ergonomischer. Synchron.
- B3L16-0033.3 — `WebClient` (non-blocking, aus WebFlux)kotlin
Mein Default für synchrone Calls in neuen Spring-Boot-4-Projekten — sofern du keine @HttpExchange-Interfaces nutzt (siehe §3.5).
- B3L16-0043.4 — `@HttpExchange` mit Boot-4-Auto-Configuration (der neue idiomatische Weg)kotlin
@HttpExchange als Annotation existiert seit Spring Framework 6.0. Du beschreibst dein HTTP-API als Kotlin-Interface, und Spring generiert eine Proxy-Implementierung zur Laufzeit. Bisher (in Boot 3.x) musstest du den Prox
- B3L16-0053.4 — `@HttpExchange` mit Boot-4-Auto-Configuration (der neue idiomatische Weg)kotlin
Interface-Definition (genauso wie vorher, in Kotlin als interface):
- B3L16-0063.4 — `@HttpExchange` mit Boot-4-Auto-Configuration (der neue idiomatische Weg)kotlin
Registrierung über @ImportHttpServices auf der Application-Klasse (oder einer @Configuration):
- B3L16-0083.4 — `@HttpExchange` mit Boot-4-Auto-Configuration (der neue idiomatische Weg)yaml
Konfiguration über application.yml — das ist der eigentliche Wert der Auto-Config:
- B3L16-009gruppen-spezifische Settings (überschreiben Defaults)kotlin
Verwendung wie eine normale Spring-Bean: Der Proxy ist als Bean registriert, du injectest ihn über den Konstruktor.
- B3L16-010gruppen-spezifische Settings (überschreiben Defaults)kotlin
Anpassung des Clients pro Gruppe geht über eine HttpServiceGroupConfigurer-Bean (selten nötig, wenn du keine spezielle Logik brauchst):
- B3L16-0123.5 — Feign (deklarativ, via Spring Cloud OpenFeign — Legacy in 2026)kotlin
Aktivierung:
- B3L16-0133.5 — Feign (deklarativ, via Spring Cloud OpenFeign — Legacy in 2026)kotlin
Interface-Definition:
- B3L16-0143.5 — Feign (deklarativ, via Spring Cloud OpenFeign — Legacy in 2026)kotlin
Verwendung wie eine normale Bean:
- B3L16-0165.1 — Kubernetes Service Discovery (DNS-basiert) — der Default 2026yaml
Wenn du auf Kubernetes deployst, brauchst du wahrscheinlich kein eigenes Discovery-Tool. Jeder K8s-Service hat einen eigenen DNS-Namen (user-service.default.svc.cluster.local), und der integrierte Service-Proxy (kube-pro
- B3L16-0175.2 — Consul (HashiCorp)yaml
Discovery, Health-Checks und K/V-Store. Funktional vergleichbar mit Eureka, aber breiter aufgestellt — auch außerhalb der Java-Welt verbreitet, Go-, Python- und Node-Services können sich registrieren. Spring-Cloud-Integr
- B3L16-0185.3 — Eureka (Netflix / Spring Cloud Netflix) — **veraltet 2026**yaml
Der Klassiker. Ein Discovery-Server, bei dem sich Services registrieren („ich bin user-service, laufe auf 10.0.0.5:8080, hier mein Health-Endpoint"). Andere Services fragen den Discovery-Server „Wo ist user-service?" und
- B3L16-019eureka-server application.yml:kotlin
fetch-registry: false
- B3L16-020eureka-server application.yml:yaml
Service-Seite:
- B3L16-0236.1 — Spring Cloud Config Server (Git-backed)kotlin
Ein dedizierter Service, der die application.yml-Dateien aus einem Git-Repo ausliefert. Services fragen beim Start nach ihrer Config.
- B3L16-0246.1 — Spring Cloud Config Server (Git-backed)yaml
runApplication<ConfigServerApp>(args)
- B3L16-025config-server application.ymlyaml
Im Config-Repo legst du Dateien an wie user-service.yml, user-service-prod.yml, application.yml (global). Der Client:
- B3L16-0266.2 — Kubernetes ConfigMaps / Secretsyaml
Auf K8s ist die einfachere Antwort: Config in einer ConfigMap, Secrets in einem Secret, beides in den Pod gemountet als Env-Variablen oder als Files.
- B3L16-0288 · Circuit Breaker — der wichtigste Resilience-Patternyaml
Konfiguration (application.yml):
- B3L16-0298 · Circuit Breaker — der wichtigste Resilience-Patternkotlin
Verwendung als Annotation:
- B3L16-0309 · Retry — Wiederholen mit Backoffyaml
Konfiguration:
- B3L16-031Jitter: ±50 %, damit nicht alle Caller im Gleichtakt retryenkotlin
randomized-wait-factor: 0.5
- B3L16-03210.1 — Semaphore-Bulkhead (default, leichtgewichtig)yaml
Begrenzt die Anzahl gleichzeitiger Calls über eine Semaphore. Keine separaten Threads.
- B3L16-03410.2 — ThreadPool-Bulkheadyaml
Eigener Thread-Pool pro Dependency. Schwerer, aber bietet stärkere Isolation.
- B3L16-036Rate Limiteryaml
Begrenzt Calls pro Zeitperiode. Schützt entweder deinen eigenen Service oder dient als Höflichkeits-Limit gegenüber einem externen Service mit Quota.
- B3L16-038Time Limiteryaml
Erzwingt einen harten Timeout für asynchrone Calls (CompletableFuture).
- B3L16-04012 · Internals — wie funktionieren die Annotations?kotlin
Konsequenz: Wenn du @CircuitBreaker auf eine private Methode setzt oder eine Methode innerhalb derselben Klasse aufrufst, greift die Annotation nicht. Self-Invocation umgeht den Proxy. In Kotlin kommt eine zweite Stolper
- B3L16-04112 · Internals — wie funktionieren die Annotations?kotlin
private fun getUserInternal(id: Long): UserDto = client.get()...
- B3L16-04215 · Konkretes Setup — was du in einem realen Service hastyaml
Ein typischer Spring-Boot-4-Microservice in 2026 hat folgende Inter-Service-Konfiguration, basierend auf @HttpExchange-Auto-Config plus Resilience4j:
- B3L16-043Actuator für Health/Metricskotlin
HTTP-Client als @HttpExchange-Interface — ohne manuellen Bean-Setup:
- B3L16-044Actuator für Health/Metricskotlin
Application-Klasse mit @ImportHttpServices:
- B3L16-045Actuator für Health/Metricskotlin
Wrapper-Service mit Resilience-Annotations. @HttpExchange-Interfaces lassen sich nicht direkt annotieren (Proxy auf Proxy), daher schiebst du einen Service davor.
- B3L16-0461 · Gradle-Multi-Project-Setuptext
Erstelle im projekt/-Verzeichnis (oder in einem neuen Unterordner projekt-microservices/, wenn du das bestehende Projekt nicht überschreiben willst) ein Multi-Project-Gradle-Layout:
- B3L16-0481 · Gradle-Multi-Project-Setupkotlin
Root build.gradle.kts definiert gemeinsame Plugin-Versionen und Repositories:
- B3L16-0492 · `user-app` — die einfache REST-APIkotlin
Die Daten kannst du hardcoded in einer Map halten, ein DB-Setup ist nicht nötig. Hier geht es nicht um User-CRUD, sondern um Inter-Service-Communication.
- B3L16-0502 · `user-app` — die einfache REST-APIyaml
user-app läuft auf Port 8081:
- B3L16-0523 · `task-app` — der Caller mit RestClientkotlin
data class TaskDto(val id: Long, val title: String, val assignee: UserDto)
- B3L16-0533 · `task-app` — der Caller mit RestClientkotlin
TaskService ruft UserClient auf, um den User-Anteil zu holen:
- B3L16-0543 · `task-app` — der Caller mit RestClientkotlin
UserClient mit RestClient:
- B3L16-0553 · `task-app` — der Caller mit RestClientkotlin
HttpClientConfig für die RestClient-Bean:
- B3L16-0563 · `task-app` — der Caller mit RestClientyaml
task-app/src/main/resources/application.yml:
- B3L16-0574 · Dependencies in `task-app/build.gradle.kts`kotlin
enabled: true
Lesson 17
21 snippets- B3L17-005Minimale `application.yml`yaml
> Migration aus Boot 3.x: Das alte Artifact spring-cloud-starter-gateway mappt in Spring Cloud 2025.x weiterhin auf den WebFlux-Server (rückwärtskompatibel). Bei einem Greenfield-Setup in Boot 4 bevorzuge die expliziten
- B3L17-006Filteryaml
Filter sind der eigentliche Hebel.
- B3L17-007Custom Global Filter — Beispiel Authkotlin
Spring Cloud Gateway hat etwa 30 eingebaute Filter (siehe Doku) — AddRequestHeader, RemoveRequestHeader, RewritePath, Retry, CircuitBreaker, RequestRateLimiter, SetStatus, StripPrefix, …
- B3L17-008Kotlin-DSL als Alternativekotlin
Statt YAML kannst du Routen auch programmatisch definieren. Spring Cloud Gateway bringt seit der 4.x-Linie eine Kotlin-DSL (gateway { … } Receiver-Block) mit, die deutlich knapper ist als die Java-Variante:
- B3L17-0127 · Choreography Saga — dezentral via Eventstext
Beispiel: User-Registrierung
- B3L17-013Implementierung in Springkotlin
Choreography baust du typischerweise auf Spring Kafka oder Spring AMQP (RabbitMQ) in Kombination mit dem Outbox-Pattern auf (B3/L07 hat das eingeführt). Skizze:
- B3L17-014Implementierung in Springkotlin
Im notification-service:
- B3L17-01612 · Idempotenz auf Service-Ebene (Wiederaufgriff)kotlin
In einer Saga können Events mehrfach ankommen (At-Least-Once-Delivery). Dein Consumer muss idempotent sein:
- B3L17-0191 · Drittes Gradle-Sub-Projekt anlegentext
Erweitere das projekt-microservices/-Setup aus B3/U16 um ein drittes Sub-Projekt:
- B3L17-0211 · Drittes Gradle-Sub-Projekt anlegenkotlin
api-gateway/build.gradle.kts zieht die Spring-Cloud-BOM (Version 2025.0.x, kompatibel zu Boot 4) und spring-cloud-starter-gateway-server-webflux rein:
- B3L17-0221 · Drittes Gradle-Sub-Projekt anlegenkotlin
ApiGatewayApplication.kt:
- B3L17-0232 · Routen konfigurierenyaml
api-gateway/src/main/resources/application.yml:
- B3L17-0243 · Testenbash
Starte alle drei Apps (user-app:8081, task-app:8082, api-gateway:8080).
- B3L17-0264 · Mock-Token-Validation-Filter (Custom Global Filter)kotlin
Schreib einen Global-Filter, der einen Header Authorization: Bearer <token> erwartet, das Token (vereinfacht) gegen einen statischen Wert prüft und im Erfolgsfall einen vertrauenswürdigen Header X-User-Id setzt, den Down
- B3L17-0306 · Viertes Modul: `notification-app`kotlin
notification-app/build.gradle.kts:
- B3L17-0316 · Viertes Modul: `notification-app`yaml
notification-app/src/main/resources/application.yml:
- B3L17-0337 · `user-app` erweitern — RabbitMQ-Publisherkotlin
data class WelcomeEmailFailedEvent(val userId: Long, val messageId: String)
- B3L17-0347 · `user-app` erweitern — RabbitMQ-Publisherkotlin
RabbitMQ-Config in user-app:
- B3L17-0357 · `user-app` erweitern — RabbitMQ-Publisherkotlin
Kompensations-Listener:
- B3L17-0368 · `notification-app` — Email-Versand-Mock + Saga-Schrittkotlin
notification-app:
- B3L17-0378 · `notification-app` — Email-Versand-Mock + Saga-Schrittkotlin
): Binding = BindingBuilder.bind(queue).to(exchange).with("user.registered")
Lesson 18
17 snippets- B3L18-0012 · Wie die JVM aufgebaut isttext
Bevor wir an Tuning denken: was läuft da eigentlich, wenn du java -jar app.jar startest?
- B3L18-0043.1 · Generational Heaptext
sterben jung" (die Weak Generational Hypothesis).
- B3L18-0053.2 · Wichtige Tuning-Flagsbash
hast du ein Problem.
- B3L18-0126.1 · JFR (Java Flight Recorder)bash
Recording starten mit jcmd:
- B3L18-0146.2 · async-profilerbash
Die Hauptanwendung sind Flame Graphs.
- B3L18-0156.5 · Heap-Dumps und Eclipse MATbash
Heap-Objekte zum aktuellen Zeitpunkt.
- B3L18-0167.1 · Warum „normaler Code" nicht benchmarkbar istkotlin
ja, da ist ein Cache, der nie evicted wird. Bug gefunden.
- B3L18-0177.2 · JMH löst daskotlin
Beispiel-Setup (Gradle Kotlin DSL, mit dem me.champeau.jmh-Plugin):
- B3L18-0187.2 · JMH löst daskotlin
Beispiel-Benchmark — zwei String-Hash-Implementationen vergleichen:
- B3L18-0218.2 · Memory-Leaks durch ungebremste Cacheskotlin
Das ist mit Abstand das häufigste Performance-Problem in JPA-Apps. Wenn dein p95 plötzlich rauscht, prüf das
- B3L18-0221 · Ein langsamer Endpoint zum Profilenkotlin
Bau in projekt/ einen Endpoint, der bewusst eine messbare Last erzeugt — sonst hast du im JFR-Recording nichts
- B3L18-0252 · Lasttest mit k6 (oder wrk)javascript
Mit k6: lege loadtest.js neben dein Projekt:
- B3L18-0283 · JFR-Recording aufnehmen während des Lasttestsbash
Während der Lasttest läuft, in einem zweiten Terminal:
- B3L18-0295 · Den Bug fixen und nochmal messenkotlin
Refactor den Endpoint:
- B3L18-0306 · JMH-Benchmark schreibenkotlin
benchmarks/build.gradle.kts):
- B3L18-0316 · JMH-Benchmark schreibenkotlin
Schreib HashBenchmark.kt:
- B3L18-0368 · Bonus: Heap-Tuning experimentierenbash
Probiere verschiedene Heap-Größen:
Lesson 19
22 snippets- B3L19-0012.3 · Springs eingebaute Falle: `HttpSession`kotlin
so etwas wie:
- B3L19-0033.1 · Setupyaml
implementation("org.springframework.session:spring-session-data-redis")
- B3L19-0044.2 · Die kritischen Propertiesyaml
wird seit Jahren von Brett Wooldridge gepflegt — keine Boot-4-Migrations-Sorgen.
- B3L19-0065.1 · Read-Replicastext
entlastest du den Primary und kannst nahezu linear skalieren.
- B3L19-0075.1 · Read-Replicaskotlin
In Spring konfigurierst du das mit zwei DataSources:
- B3L19-0095.1 · Read-Replicaskotlin
Im Service-Code:
- B3L19-0117.4 · DB-Locks ohne Timeoutkotlin
Cluster-Modus). Pflicht für jedes horizontale Deployment.
- B3L19-0128.1 · Kubernetes HPAyaml
Der Horizontal Pod Autoscaler (HPA) ist Kubernetes eingebauter Mechanismus. Eine HPA-Definition:
- B3L19-0138.2 · Custom Metricsyaml
Queue-Tiefe (für Worker-Apps).
- B3L19-0159.2 · Lasttest-Toolsjavascript
Beispiel k6:
- B3L19-0171 · Spring Session + Redis einbauenyaml
In application.yml:
- B3L19-0181 · Spring Session + Redis einbauenkotlin
Bau einen kleinen Test-Endpoint, der eine Session-Variable setzt und liest:
- B3L19-0192 · Docker-Compose mit 3 App-Instanzen + Redis + Nginxyaml
Lege docker-compose.yml neben dein Projekt:
- B3L19-0202 · Docker-Compose mit 3 App-Instanzen + Redis + Nginxnginx
Und nginx.conf mit Round-Robin-LB:
- B3L19-0233 · Session-Überlebens-Testbash
In einem zweiten Terminal:
- B3L19-0265 · HikariCP unter Last tunenkotlin
Bau einen einfachen DB-lastigen Endpoint:
- B3L19-0275 · HikariCP unter Last tunenyaml
Setze HikariCP klein an:
- B3L19-0285 · HikariCP unter Last tunenbash
Stoppe und starte Compose neu. Fahr einen Lasttest:
- B3L19-0347 · Bonus: Read-Replica simulierenyaml
Erweitere docker-compose.yml um eine zweite Postgres-Instanz als „Replica":
- B3L19-0357 · Bonus: Read-Replica simulierenkotlin
Im Code: zwei DataSources + Routing.
- B3L19-0367 · Bonus: Read-Replica simulierenyaml
application.yml:
- B3L19-0377 · Bonus: Read-Replica simulierenkotlin
Bau einen Endpoint, der das demonstriert:
Lesson 20
27 snippets- B3L20-002Dependencyyaml
Per Default ist nur /actuator/health exponiert — sicheres Default. Alles andere musst du bewusst freischalten:
- B3L20-003Custom Health Indicatorskotlin
Health-Checks sollen ehrlich antworten — nicht nur „der HTTP-Server steht", sondern „die Datenbank ist tatsächlich erreichbar und ich kann eine Query absetzen". Spring bringt für die Standard-Stacks (DataSource, Redis, R
- B3L20-004Custom Health Indicatorskotlin
Auch für die DB direkt — ein typisches Beispiel, das die Lektion gleich zweimal aufgreift:
- B3L20-005Custom Health Indicatorsyaml
Spring Boot 4 unterscheidet das per Default via Groups:
- B3L20-008Wo den `MeterRegistry` herbekommen?kotlin
Spring registriert dir eine MeterRegistry-Bean (in Boot 4 + Actuator: PrometheusMeterRegistry, wenn der Prometheus-Starter dabei ist; bei aktivem spring-boot-starter-opentelemetry zusätzlich eine OtlpMeterRegistry, falls
- B3L20-009`@Timed` und `@Counted` — Annotations für den Häufig-Fallkotlin
Für simple Timing-Use-Cases gibt es Annotations, die einen Timer automatisch um die Methode legen:
- B3L20-010Gauges sauber registrierenkotlin
Gauges sind tückisch, weil sie eine Referenz auf ein Objekt halten.
- B3L20-012Dependencytext
Beispiel-Output:
- B3L20-013Prometheus-Configyaml
prometheus.yml:
- B3L20-015Grafana — die Default-Dashboardspromql
Eigene Dashboards baust du in der Grafana-UI per Drag-and-Drop. Queries gegen Prometheus laufen in PromQL:
- B3L20-016Alertmanageryaml
Alerts werden in Prometheus als Rules definiert:
- B3L20-017Die zwei Worlds: OTel vs. Bravekotlin
Spring Boot hat Micrometer Tracing als Facade vor beiden — du programmierst gegen die Micrometer-Tracing-API und wählst zur Build-Zeit, welche Implementation reinkommt. In Spring Boot 4 ist das idiomatisch in einem einzi
- B3L20-019Der neue `spring-boot-starter-opentelemetry` (Boot 4)yaml
Die wichtigen Properties:
- B3L20-022Manuelle Spanskotlin
Auto-Instrumentation deckt 90 % ab. Manchmal willst du eine eigene Operation explizit als Span sichtbar machen:
- B3L20-024Context-Propagation und Virtual Threadsyaml
In application.yml:
- B3L20-0276 · Logs + Traces verbinden — Korrelation als Praxisjson
In strukturiertem JSON-Log:
- B3L20-0301 · Dependencies in allen drei Services ergänzenkotlin
In jedem Spring-Modul (gateway/, user-app/, task-app/) im build.gradle.kts:
- B3L20-0312 · Actuator + Tracing konfigurierenyaml
In allen drei Services in application.yml:
- B3L20-0323 · Custom Health Indicatorskotlin
In user-app schreibe einen Indicator für die DB-Verbindung (Spring bringt einen automatischen mit — verifiziere, dass er da ist) und einen für RabbitMQ (falls du in B3/L07 RabbitMQ eingeführt hast). Falls noch keine MQ-A
- B3L20-0334 · Drei Custom-Metriken im Projektkotlin
a) Counter — users.created:
- B3L20-0344 · Drei Custom-Metriken im Projektkotlin
b) Timer — task.assignment.duration über @Timed:
- B3L20-0354 · Drei Custom-Metriken im Projektkotlin
c) Gauge — tasks.active.count:
- B3L20-0365 · Docker-Compose mit dem vollen Stackyaml
Im Projekt-Root compose.observability.yaml:
- B3L20-0376 · Prometheus-Configyaml
observability/prometheus.yml:
- B3L20-0387 · Tempo-Configyaml
observability/tempo.yaml:
- B3L20-0398 · Grafana-Datasources auto-provisionierenyaml
observability/grafana/datasources/datasources.yml:
- B3L20-04110 · Bonus: Dashboard + Alert-Ruleyaml
In Prometheus eine Alert-Rule definieren (observability/alert.rules.yml, dann in prometheus.yml referenzieren):
Lesson 21
25 snippets- B3L21-0022 · Multi-Stage Build — Build- und Runtime-Stage trennendockerfile
Punkte 1 und 2 lösen wir mit Multi-Stage und Layered Jars. Punkt 3 ist akademisch — für Spring-Boot-Apps in K8s reicht der Default.
- B3L21-0063 · Layered Jars — den Cache pro Spring-Boot-Schicht nutzendockerfile
Das erzeugt ein Verzeichnis pro Layer. Das nutzt du im Dockerfile:
- B3L21-0085 · Spring Boot Buildpacks — kein Dockerfiletext
Output (gekürzt):
- B3L21-0095 · Spring Boot Buildpacks — kein Dockerfilekotlin
Konfiguration in build.gradle.kts:
- B3L21-011Spring Boot Nativekotlin
Wenn du eigene Libraries oder dynamisches Verhalten hast, das AOT nicht automatisch findet, musst du Reflection-Hints liefern:
- B3L21-012CRaC — die Alternative zu Native für Cold-Startbash
Seit Spring Boot 3.2 (und in Boot 4 stabilisiert) gibt es eine zweite Antwort auf das Cold-Start-Problem: CRaC (Coordinated Restore at Checkpoint). Die Idee dahinter: du startest deine App einmal ganz normal, lässt sie w
- B3L21-013Probes — das, was du als Spring-Dev unbedingt verstehen musstyaml
Spring Boot 4 bietet /actuator/health/liveness und /actuator/health/readiness als getrennte Endpoints (seit Boot 2.3 etabliert). K8s-Manifest:
- B3L21-014Resource Requests und Limitsyaml
K8s scheduled Pods basierend auf Requests und enforcet hartes Cap mit Limits:
- B3L21-016K8s-Manifestyaml
terminationGracePeriodSeconds: 60 # gib der App 60s
- B3L21-018Helm und Kustomizeyaml
Kustomize — Patches-on-Bases statt Templates. Du hast ein base/-Verzeichnis mit Default-Manifests und ein overlays/prod/-Verzeichnis mit Patches:
- B3L21-0199 · CI/CD-Hinweiseyaml
GitHub Actions als CI:
- B3L21-02010 · Internals — wie Buildpacks die JVM tunentext
Paketo Buildpacks lösen Problem 2 mit einem Java Memory Calculator. Beim Pod-Start läuft ein Init-Script, das aus dem Container-Memory-Limit, der Klassen-Anzahl, der Thread-Stack-Size und der GC-Wahl die optimalen JVM-Fl
- B3L21-0231 · Handgeschriebenes Multi-Stage-Dockerfile mit Layered Jardockerfile
Wir nehmen user-app/ als Referenz. Lege im Modul ein Dockerfile an:
- B3L21-024--- Stage 3: Runtime ---text
Lege außerdem ein .dockerignore an:
- B3L21-026--- Stage 3: Runtime ---bash
docker run --rm -p 8081:8080 projekt/user-app:dockerfile
- B3L21-0282 · Buildpacks via `bootBuildImage`kotlin
In user-app/build.gradle.kts:
- B3L21-0323 · GraalVM Native Imagekotlin
In user-app/build.gradle.kts das GraalVM-Buildtools-Plugin aktivieren (kommt aus dem Spring-Initializr-Template, ggf. manuell ergänzen):
- B3L21-0365 · Health-Probes konfigurierenyaml
In user-app/src/main/resources/application.yml:
- B3L21-0396 · K8s-Manifest schreibenyaml
projekt/k8s/configmap.yaml:
- B3L21-0406 · K8s-Manifest schreibenyaml
projekt/k8s/secret.yaml:
- B3L21-0416 · K8s-Manifest schreibenyaml
projekt/k8s/deployment.yaml:
- B3L21-0426 · K8s-Manifest schreibenyaml
projekt/k8s/service.yaml:
- B3L21-0436 · K8s-Manifest schreibenyaml
projekt/k8s/ingress.yaml:
- B3L21-0446 · K8s-Manifest schreibenyaml
projekt/k8s/hpa.yaml:
- B3L21-0457 · Bonus — in kind oder minikube deployenbash
Wenn du Zeit hast:
Lesson 22
27 snippets- B3L22-0012.1 · Wo welches Tool greifttext
Stell es dir als Pipeline vor — die Tools picken in unterschiedlichen Phasen deiner Code-Reise:
- B3L22-0023.1 · SpotBugs einbindenkotlin
build.gradle.kts:
- B3L22-0033.1 · SpotBugs einbindenjava
Beispiele, die SpotBugs findet:
- B3L22-0043.2 · ErrorProne — semantische Bugs schon beim Compilekotlin
Compiler-Plugin-Setup für Gradle:
- B3L22-0053.2 · ErrorProne — semantische Bugs schon beim Compilejava
Was ErrorProne findet (Auswahl der ~500 Bug-Patterns):
- B3L22-0093.3 · NullAway — Kotlin-Null-Safety für Javakotlin
Setup als ErrorProne-Plugin in Gradle:
- B3L22-0103.4 · Detekt — Kotlin-Linter als Ergänzung zu ArchUnitkotlin
build.gradle.kts:
- B3L22-0113.4 · Detekt — Kotlin-Linter als Ergänzung zu ArchUnityaml
Beispiel-Regeln aus detekt.yml:
- B3L22-0123.5 · PMD — der konfigurierbare Linterkotlin
build.gradle.kts:
- B3L22-0133.5 · PMD — der konfigurierbare Linterxml
pmd-ruleset.xml:
- B3L22-0143.6 · Checkstyle vs. Spotless — der Format-Kriegkotlin
Spotless ist die jüngere, idiomatischere Lösung: es formatiert automatisch während des Build (./gradlew spotlessApply) und checkt im CI (./gradlew spotlessCheck). Es kann Google-Java-Format, Palantir-Java-Format, Eclipse
- B3L22-0153.6 · Checkstyle vs. Spotless — der Format-Kriegkotlin
Setup über das sonarqube-Gradle-Plugin:
- B3L22-0185.2 · Setupkotlin
Eine erste Test-Klasse — ArchUnit ist für Kotlin transparent, weil es auf Bytecode-Ebene arbeitet:
- B3L22-0215.3 · Die DSL — wie ArchUnit-Regeln aussehenkotlin
Die that()-Klauseln filtern, was du betrachten willst:
- B3L22-0235.4.1 · Layered-Dependency-Directionkotlin
Die klassische Schichten-Regel: Controller → Service → Repository, niemals umgekehrt.
- B3L22-0245.4.1 · Layered-Dependency-Directionkotlin
ArchUnit hat eingebaute Pattern für die kanonischen Architekturen: layeredArchitecture(), onionArchitecture(). Kompakt und lesbar.
- B3L22-0255.4.2 · Domain darf nicht von Infrastruktur abhängenkotlin
Wenn du Hexagonal aus B2/L02 umsetzt, ist das die zentrale Regel:
- B3L22-0265.4.3 · Naming-Conventions als Regelkotlin
Strukturelle Konventionen einfangen:
- B3L22-0295.4.6 · Keine `System.out` / `System.err`kotlin
Klein, aber als Hygienemarker wichtig:
- B3L22-0305.4.7 · Annotation-Disziplinkotlin
.as("Logger statt System.out verwenden")
- B3L22-0315.6 · Frozen Rules — Legacy elegant einfrierenkotlin
ArchUnit hat dafür Frozen Rules:
- B3L22-0336.3 · Setupkotlin
Der Mutation-Score ist getötete Mutanten geteilt durch alle Mutanten. Eine ehrliche Metrik dafür, ob deine Tests Verhalten prüfen oder nur ausführen.
- B3L22-0347 · Code Coverage mit JaCoCokotlin
Auch wenn Coverage allein irreführend ist (Sektion 6), ist ein Mindest-Schwellenwert im Build trotzdem sinnvoll — als untere Grenze.
- B3L22-0358.1 · Dependency-Convergencekotlin
Gradle bietet dafür Dependency Locking und das failOnVersionConflict()-Resolution-Strategy-Flag:
- B3L22-0368.2 · OWASP Dependency-Checkkotlin
Scannt deinen Klassenpfad gegen die National Vulnerability Database (NVD) und meldet Dependencies mit bekannten CVEs.
- B3L22-0378.3 · Renovate und Dependabotjson
renovate.json minimal:
- B3L22-0399.2 · GitHub Actionsyaml
.github/workflows/ci.yml:
Lesson 23
19 snippets- B3L23-0062.3 · Die wichtigsten Operatorenkotlin
Streaming-Operatoren auf Flux:
- B3L23-0092.5 · Schedulers — wo die Threads landenkotlin
Du kannst explizit umschalten:
- B3L23-0103.1 · Annotated-Controllerskotlin
Die Variante, die dir am vertrautesten aussieht, sind annotierte Controller mit reaktiven Return-Types:
- B3L23-0113.2 · Functional Endpointskotlin
WebFlux hat eine zweite, funktionale Routing-Variante ohne Annotations:
- B3L23-0123.4 · WebClient — der reactive HTTP-Clientkotlin
RestTemplate und RestClient (synchron) haben in WebFlux einen reactive Bruder: WebClient. Wenn du in einem WebFlux-Endpoint einen externen Call machst, muss der reactive sein, sonst pinnst du den Event-Loop-Thread:
- B3L23-0153.6 · Reactive Securitykotlin
Spring Security hat ein reactive Pendant. Konzepte sind dieselben (Filter-Chain, Authentication, Authorization), die API ist andere Annotationen und andere Konfigurations-Klassen:
- B3L23-0216.3.1 · Streaming-Use-Caseskotlin
Server-Sent Events (SSE), WebSockets, langlebige Streams — also Endpoints, die eine einzelne Connection minutenlang halten und dabei laufend Daten senden. Reactive ist hier semantisch passend, weil ein Flux<T> über Zeit
- B3L23-0226.3.1 · Streaming-Use-Caseskotlin
Mit Coroutines + Flow wäre dieselbe Stream-Variante:
- B3L23-0256.5 · Mein Empfehlungs-Treetext
Für ein neues Projekt:
- B3L23-0287.3 · BlockHound — der Detektorkotlin
Setup im Test:
- B3L23-0318.2 · Hot vs. Cold Publisherskotlin
Falle: du hast einen Flux, der einen HTTP-Call macht, und subscribed zweimal (z. B. zu zwei downstream Stages). Cold-Verhalten heißt dann: zwei HTTP-Calls. Wenn du das nicht wolltest, brauchst du .cache() oder .publish()
- B3L23-0328.4 · Reactor Context vs. ThreadLocalkotlin
Reactor bietet als Alternative Context:
- B3L23-0341 · Spike-Modul aufsetzen (~15 Min)kotlin
spike-reactive/build.gradle.kts:
- B3L23-0352 · Fake-Backend mit Latenz (~10 Min)yaml
Vorschlag: starte einen WireMock-Container, der auf GET /users/{id} mit einer Latenz von 200 ms antwortet.
- B3L23-036docker-compose.yml im spike-reactive/-Moduljson
wiremock/mappings/users.json:
- B3L23-0383 · MVC-Variante mit Virtual Threads (~25 Min)kotlin
Endpoint GET /aggregate/{n} — aggregiert n Calls an WireMock und gibt die User zurück.
- B3L23-0414 · WebFlux-Variante (~25 Min)kotlin
Endpoint analog, aber reactive:
- B3L23-0424 · WebFlux-Variante (~25 Min)kotlin
Alternative mit suspend fun plus Coroutines-Bridge:
- B3L23-0456 · Reactor-Operatoren üben (~20 Min)kotlin
Alternative mit Flow<T>:
Lesson 24
20 snippets- B3L24-0012 · Die vier Schichten unter JPAtext
die zusammenarbeiten:
- B3L24-0023 · Anatomie einer JPA-Entity in Kotlinkotlin
jakarta.persistence. ist nur die offensichtlichste Änderung.
- B3L24-0055 · Lazy-Loading — der Klassiker unter den Stolperfallenkotlin
Open-Class. Beide zusammen sind in Boot-Kotlin-JPA-Projekten
- B3L24-0078.2 · `@OneToMany` → `@MappedCollection`kotlin
CustomerRepository. Explizit. Kein Lazy-Loading.
- B3L24-0088.3 · `@ManyToOne` → Referenz über IDkotlin
von Data JDBC übernimmt Cascade und Orphan-Removal automatisch.
- B3L24-0098.4 · `@ManyToMany` → Join-Tabelle als eigenes Aggregatekotlin
val customerId: Long
- B3L24-0108.5 · `JpaRepository` → `ListCrudRepository`kotlin
val roleId: Long,
- B3L24-0118.7 · Tests umstellenkotlin
Die @Transactional-Annotation ist davon abstrahiert.
- B3L24-0121 · JPA-Baseline anlegenkotlin
Starte mit einem frischen Boot-4-Kotlin-Projekt. build.gradle.kts:
- B3L24-0131 · JPA-Baseline anlegenyaml
application.yml:
- B3L24-0141 · JPA-Baseline anlegenkotlin
sehen, wo Lazy-Loading zuschlägt.
- B3L24-0162 · Lazy-Loading-Crash provozierenkotlin
fun findByEmail(email: String): User?
- B3L24-0172 · Lazy-Loading-Crash provozierenkotlin
fun listAddresses(id: Long): List<Address> = get(id).addresses.toList()
- B3L24-0213 · Drei Fixes vergleichenkotlin
UserResponse:
- B3L24-0225 · Migration auf Spring Data JDBCkotlin
Bearbeite build.gradle.kts:
- B3L24-0235 · Migration auf Spring Data JDBCkotlin
Entities umschreiben:
- B3L24-0255 · Migration auf Spring Data JDBCyaml
application.yml:
- B3L24-026spring.jpa.* alles raus — Data JDBC kennt das nichtsql
schema.sql neu (Data JDBC nutzt kein ddl-auto):
- B3L24-027spring.jpa.* alles raus — Data JDBC kennt das nichtkotlin
Service anpassen — kein Lazy-Loading mehr, alle Daten werden zusammen
- B3L24-0286 · Tests umstellenkotlin
addresses = user.addresses.map { AddressDto(it.street, it.zip, it.city) }.toList(),
Book 4 — Kotlin Stack for Freelance Projects
324 snippetsLesson 1
8 snippets- B4L01-003Spring Boot 4.0 mit Kotlinkotlin
Drei Dateien (Standard-Initializr-Output, gekürzt):
- B4L01-004Spring Boot 4.0 mit Kotlinkotlin
runApplication<HelloApplication>(args)
- B4L01-005Spring Boot 4.0 mit Kotlinkotlin
fun hello(): Map<String, String> = mapOf("message" to "Hello, Spring")
- B4L01-006Ktor 3.4 mit Kotlinkotlin
Zwei Dateien:
- B4L01-007Ktor 3.4 mit Kotlinkotlin
call.respond(mapOf("message" to "Hello, Ktor"))
- B4L01-0113 · Startzeit messenmarkdown
Schreib dir die beiden Werte in eine Datei notes.md im ktor-hello-Verzeichnis, mit jeweils zwei bis drei Stichworten Kontext (Maschine, JDK-Version, warmer/kalter Build). Beispiel:
- B4L01-0124 · JAR-Größe messenbash
Bau aus beiden Projekten ein deployment-fähiges JAR:
- B4L01-0135 · Den Hello-Endpoint umbauenkotlin
Öffne src/main/kotlin/com/example/hello/Routing.kt (oder wie der Initializr die Datei genannt hat — meist Routing.kt oder direkt in Application.kt). Ändere den Endpoint so, dass er statt String einen JSON-Body zurückgibt
Lesson 2
19 snippets- B4L02-0011.1 · `build.gradle.kts`kotlin
Das ist der vollständige Build-File, den ich in dieser Lektion durchgehe — alles, was später dazukommt, wird darauf aufbauen.
- B4L02-0021.2 · `src/main/kotlin/com/example/api/Application.kt`kotlin
Die application-Konfiguration zeigt auf die JVM-Klasse, die Gradle als Entry Point ausführen soll. Wichtig: das ist nicht der Klassen-Pfad in deinem Kotlin-Code, sondern die kompilierte JVM-Klasse — Kotlin hängt an Top-L
- B4L02-0032.1 · Einfache Endpunktekotlin
Das Routing in Ktor ist ein Kotlin-Builder. Du baust dir einen Baum aus route { ... }-Blöcken auf, und an den Blättern hängen get, post, put, delete und Co.
- B4L02-0042.2 · Verschachteltes Routingkotlin
Wenn du mehrere Endpunkte unter einem gemeinsamen Pfad-Prefix hast, fasst du sie in einem route(...)-Block zusammen:
- B4L02-0052.3 · Path-Parameter, Query-Parameter, Wildcardskotlin
Das ist deutlich näher an dem, was in Spring Boot ein Controller mit @RequestMapping("/api/v1/users") auf Klassen-Ebene macht — nur eben als verschachtelter Block statt als Annotation-Hierarchie.
- B4L02-0063.1 · Request-Body lesen und validierenkotlin
Drei Pattern, die du in der Praxis ständig brauchst.
- B4L02-0083.3 · Response-Header setzenkotlin
Default ohne expliziten Status ist 200 OK. Du kannst dir import io.ktor.http.HttpStatusCode.Companion. ans Datei-Top setzen, dann schreibst du Created statt HttpStatusCode.Created.
- B4L02-0094.1 · Eine Ressource definierenkotlin
Das eingebaute Routing über String-Pattern (get("/users/{id}")) ist flexibel, aber typunsicher. Wenn du den Pfad umbenennst oder einen Parameter dazunimmst, merkst du das erst zur Laufzeit. Das Resources-Plugin liefert d
- B4L02-0104.2 · Handler dazukotlin
Das ist die Standard-Konvention: eine Top-Level-Klasse für /users, eine genestete Klasse für /users/{id}. Die parent-Referenz hängt die Sub-Ressource an die Eltern-Route.
- B4L02-0114.3 · Query-Parameter über die Ressourcekotlin
Der Handler-Block bekommt die deserialisierte Ressource als Parameter rein, mit typisierten Feldern. resource.id ist ein Long, kein String-mit-!!-Cast. Wenn du im Pattern id: Long schreibst und ein Aufrufer /users/abc sc
- B4L02-0124.4 · Vergleich zu Spring MVCkotlin
In Spring schreibst du:
- B4L02-0134.4 · Vergleich zu Spring MVCkotlin
In Ktor mit Resources-Plugin:
- B4L02-0155 · Application-Konfigurationyaml
src/main/resources/application.yaml:
- B4L02-0165 · Application-Konfigurationkotlin
Damit die Konfiguration tatsächlich gelesen wird, baust du main und Modul-Setup ein bisschen um:
- B4L02-0171 · Projekt vorbereitenkotlin
Im ktor-hello-Projekt:
- B4L02-0182 · Datenmodell und In-Memory-Repositorykotlin
Neue Datei src/main/kotlin/com/example/hello/Books.kt:
- B4L02-0193 · Drei Endpunkte einbauenkotlin
Öffne Application.kt (oder die Datei, in der dein Application.module() lebt) und bau das Routing wie folgt um:
- B4L02-0214 · Hochfahren und prüfenbash
Im zweiten Terminal:
- B4L02-0236 · Optional: HTTP-Datei in IntelliJhttp
In IntelliJ Ultimate kannst du eine .http-Datei anlegen, in der alle Aufrufe stehen — bequemer als curl in der Shell. Datei requests/books.http:
Lesson 3
20 snippets- B4L03-0011 · Was ein Ktor-Plugin istkotlin
Du musst das nicht selbst bauen — Ktor bringt für alles, was du in den ersten Wochen brauchst, fertige Plugins mit. Du installierst sie, gibst ihnen Konfiguration, und sie hängen sich in die Request-Pipeline ein.
- B4L03-002Default-Konfigurationkotlin
Damit call.receive<T>() und call.respond(obj) JSON sprechen können. Ohne ContentNegotiation kennt Ktor nur Strings und Byte-Arrays.
- B4L03-003Default-Konfigurationkotlin
Pro Request eine Log-Zeile mit Methode, Pfad, Statuscode, Dauer. Plus optional MDC-Felder (Request-ID, User-ID, Tenant), die du dann in jeder Log-Zeile des Handlers automatisch siehst.
- B4L03-004Default-Konfigurationxml
Damit die requestId in jeder Log-Zeile sichtbar wird, brauchst du ein Logback-Pattern wie:
- B4L03-005Default-Konfigurationkotlin
Dein Backend läuft auf api.example.com, dein SPA auf app.example.com. Ohne CORS-Header lehnt der Browser deinen Request mit „blocked by CORS policy" ab.
- B4L03-006Default-Konfigurationkotlin
In Spring nutzt du @ControllerAdvice und @ExceptionHandler, um Exceptions zentral in HTTP-Responses umzuwandeln (siehe B2/L11). In Ktor heißt das Pendant StatusPages.
- B4L03-007Default-Konfigurationkotlin
JSON-Responses bei 1–500 KB profitieren stark von Compression. Mobile-Clients mit knappem Datenvolumen, langsame Netze, hohe Latenz — überall lohnt sich der Trade-off CPU-für-Bandbreite.
- B4L03-008Default-Konfigurationkotlin
Wenn dein Backend öffentlich erreichbar ist, willst du, dass ein Aufrufer dir nicht mit 10.000 Requests pro Sekunde den Dienst lahmlegt. Auch ohne böse Absicht — ein verbuggter Mobile-Client in einer Retry-Schleife reich
- B4L03-0098 · Reihenfolge in der Praxiskotlin
Wenn ich ein neues Ktor-Projekt starte, sieht der Plugin-Teil meistens so aus:
- B4L03-0101 · Build-File ergänzenkotlin
Vier neue Dependencies in build.gradle.kts:
- B4L03-0112 · `logback.xml` anlegenxml
Datei src/main/resources/logback.xml:
- B4L03-0123 · ContentNegotiation feinjustierenkotlin
Im Application.module() den bestehenden ContentNegotiation-Block ersetzen:
- B4L03-0144 · CallLogging mit Request-ID einbauenkotlin
Im Modul (vor routing { ... }):
- B4L03-0175 · CORS für lokales Frontend aktivierenkotlin
Bonus: Schick einen Health-Check-Request (falls du /health aus L02-Lektion noch drin hast) und vergewissere dich, dass diese Zeile nicht geloggt wird — der filter-Block schließt sie aus.
- B4L03-0185 · CORS für lokales Frontend aktivierenbash
Server neu starten. Test mit einem simulierten Preflight-Request:
- B4L03-019Preflight: OPTIONS mit Origin-Headertext
Erwartung im Response-Header:
- B4L03-0216 · StatusPages für saubere Error-Responseskotlin
Im Modul (vor routing { ... }):
- B4L03-0226 · StatusPages für saubere Error-Responseskotlin
Im BookRepository-basierten Routing den null-Branch durch eine Exception ersetzen, damit der zentrale Handler greift:
- B4L03-0236 · StatusPages für saubere Error-Responsesbash
call.respond(HttpStatusCode.NoContent)
- B4L03-0247 · Reihenfolge prüfenkotlin
Schau dir deinen Application.module()-Block am Ende an. Die Reihenfolge sollte ungefähr so sein:
Lesson 4
14 snippets- B4L04-001Was sich gegenüber 3.3 ändertkotlin
In Ktor 3.3 sah ein typischer Endpoint so aus:
- B4L04-002Was sich gegenüber 3.3 ändertkotlin
In 3.4 fügst du dem gleichen Endpoint einen openapi-Block hinzu:
- B4L04-003Setupkotlin
Der Plugin sitzt im Gradle-Block:
- B4L04-004Setupkotlin
Im Code aktivierst du die Auslieferung:
- B4L04-005Wie das Schema entstehtkotlin
Annotationen.
- B4L04-006Wie das Schema entstehtjson
Das wird zu:
- B4L04-007Beschreibungen, die helfenkotlin
Eine gut beschriebene Operation hat vier Dinge:
- B4L04-009Type-Safe Resources und OpenAPIkotlin
schreibst du:
- B4L04-010Vergleich zu SpringDocjava
In Spring Boot (B3/L13) machst du dasselbe mit SpringDoc OpenAPI:
- B4L04-011Was du im Build-Pipeline ändern solltestkotlin
Profile-Check:
- B4L04-012Schritt 1 — Plugin aktivierenkotlin
In build.gradle.kts:
- B4L04-013Schritt 1 — Plugin aktivierenkotlin
In Application.kt:
- B4L04-014Schritt 2 — Endpoints dokumentierenkotlin
Beispiel für GET /products:
- B4L04-016Schritt 5 — Production-Härtungkotlin
Pack SwaggerUI hinter einen Profile-Check:
Lesson 5
14 snippets- B4L05-002Setup gegen Keycloakhocon
Konfiguration in application.conf:
- B4L05-003Setup gegen Keycloakkotlin
jwksUri = ${auth.issuer}"/protocol/openid-connect/certs"
- B4L05-004Routen schützenkotlin
Stelle dafür.
- B4L05-005Routen schützenkotlin
Recht), kapsele die Extraktion in eine Extension-Function:
- B4L05-007Rollen und Scopes ziehenkotlin
spezifische Rollen. Beides liest du aus dem Payload:
- B4L05-008Rollen und Scopes ziehenkotlin
Ein Authorization-Helper:
- B4L05-009Rollen und Scopes ziehenkotlin
throw ForbiddenException()
- B4L05-011Schritt 1 — Keycloak per Docker-Compose hochziehenyaml
docker-compose.yml im Projekt-Root:
- B4L05-012Schritt 3 — Token holen und ansehenbash
6. User tester anlegen, Passwort setzen (Credentials-Tab), Rolle product-admin zuweisen (Role mapping).
- B4L05-014Schritt 4 — Auth in Ktor einbauenhocon
In application.conf:
- B4L05-015Schritt 4 — Auth in Ktor einbauenkotlin
einbauen und aufrufen:
- B4L05-016Schritt 5 — Geschützte /me-Routekotlin
productRoutes()
- B4L05-018Schritt 6 — Role-Check auf einer Write-Routekotlin
In deinen productRoutes() die DELETE-Route absichern:
- B4L05-020Schritt 7 — Test mit Test-Tokenkotlin
In src/test/kotlin/ProductAuthTest.kt:
Lesson 6
11 snippets- B4L06-0011 · Der Default: jede Route ist eine Coroutinekotlin
In Ktor ist das anders. Jeder Route-Handler ist per Definition eine suspend fun:
- B4L06-0033 · Parallele Sub-Operationen mit `coroutineScope`kotlin
Der Standard-Case: dein Endpoint braucht Daten aus drei Quellen — User, Orders, Notifications — und du willst die parallel laden, nicht sequentiell.
- B4L06-0054 · Cancellation-Pfade — was wird abgebrochen, was nichtkotlin
Fall B — Schreib-Operation, die du nicht abbrechen willst: Du loggst einen Audit-Event in die DB, nachdem du die Response schon geschickt hast — und der Client-Disconnect soll das Log nicht verhindern.
- B4L06-0065 · Dispatcher-Wechsel für blockierende Callskotlin
Die Standard-Lösung: withContext(Dispatchers.IO).
- B4L06-0076 · Der Ktor-`HttpClient` ist Teil derselben Weltkotlin
Wenn du in einem Ktor-Service externe HTTP-Calls machst, benutzt du den Ktor-eigenen HttpClient (Details kommen in B4/L10). Der ist genauso suspend-basiert:
- B4L06-0081 · Datenmodelle und Serviceskotlin
Lege an: src/main/kotlin/com/example/companion/aggregate/AggregateModels.kt
- B4L06-0091 · Datenmodelle und Serviceskotlin
Lege an: src/main/kotlin/com/example/companion/aggregate/AggregateServices.kt
- B4L06-0102 · Route registrierenkotlin
Erweitere deine routing { ... }-Konfiguration:
- B4L06-0123 · Cancellation provozierenkotlin
Erweitere UserService so, dass die delay aufgesplittet wird und du Logging einbaust:
- B4L06-0154 · `NonCancellable`-Audit-Writekotlin
Ergänze einen Audit-Service, der nach dem respond läuft und vom Client-Disconnect nicht beeinflusst werden soll:
- B4L06-0164 · `NonCancellable`-Audit-Writekotlin
Erweitere die Route:
Lesson 7
15 snippets- B4L07-0011 · Was `testApplication` ist und warum es passtkotlin
testApplication { ... } macht in Ktor dasselbe. Du startest die App ohne Netty, ohne offenen Port, ohne Netzwerk. Die Requests laufen über einen internen Mock-Transport, die App selbst läuft mit ihrer realen Plugin-Kette
- B4L07-0022 · Setup im Build-Skriptkotlin
build.gradle.kts (gradle-kotlin-dsl):
- B4L07-0033 · `ContentNegotiation` im Test — Body-Deserialisierungkotlin
Wenn dein Endpoint JSON zurückgibt und du den Body als typisierten Wert haben willst, brauchst du ContentNegotiation-Plugin auf dem Test-Client — separat von dem auf der App-Seite. Der Default-client aus testApplication
- B4L07-0054 · Service-Layer mocken mit MockKkotlin
Voraussetzung: dein Service ist über DI injectable. Im Ktor-Stack ist das typischerweise Koin (Details kommen in B4/L08+). Mit Koin baust du die Test-Module um:
- B4L07-0065 · In-Memory-Persistence — SQLDelight als Vorgriffkotlin
Wenn dein Service auf eine Datenbank zugreift und du das nicht mocken, sondern echt durchtesten willst, brauchst du eine in-memory Persistenz. Mit SQLDelight (kommt in B4/L11):
- B4L07-0076 · Authentication-Tests — Test-JWT selbst bauenkotlin
Wenn deine Routes mit JWT geschützt sind (siehe B4/L05), brauchst du im Test ein gültiges Token. Du baust es mit derselben Library, mit der die App es validiert — auth0/java-jwt:
- B4L07-0086 · Authentication-Tests — Test-JWT selbst bauenkotlin
In der App-Konfiguration (Test-Variante!) registrierst du den authentication { jwt { ... } }-Block mit demselben Secret, Audience und Issuer — sonst lehnt der JWTVerifier das Test-Token ab.
- B4L07-0091 · Dependencieskotlin
Falls noch nicht da, ergänze build.gradle.kts:
- B4L07-0102 · Domain und Servicekotlin
Wenn du die Klassen schon hast, prüf nur die Signaturen. Sonst lege an: src/main/kotlin/com/example/companion/users/UserDto.kt
- B4L07-0122 · Domain und Servicekotlin
src/main/kotlin/com/example/companion/users/UserRoutes.kt
- B4L07-0133 · Test-Klasse skelettierenkotlin
Lege an: src/test/kotlin/com/example/companion/users/UserRoutesTest.kt
- B4L07-0144 · Test 1 — Happy-Pathkotlin
Wichtig: die module()-Funktion in application { ... } zeigt auf deine echte Application.module()-Setup-Funktion. Stell sicher, dass dort userRoutes() registriert wird und das JWT-Auth-Setup die Config-Werte (jwt.secret,
- B4L07-0155 · Test 2 — Not-Foundkotlin
Lass den Test laufen. Wenn er grün ist — Punkt eins durch. Wenn nicht, typische Fehler:
- B4L07-0166 · Test 3 — Unauthorizedkotlin
Das Mock-Setup coEvery { userService.findById(9999) } returns null — und der Route-Handler übersetzt null in 404. Klassischer Pfad.
- B4L07-0177 · Optional — Test 4 — Unauthorized mit falschem Secretkotlin
Hier brauchst du den jsonClient() nicht — du dekodierst den Body nicht. Und du brauchst keinen Mock-Setup für findById, weil der Request nie bis zum Service kommt.
Lesson 8
10 snippets- B4L08-0023 · Source-Sets und Hierarchical Source Setstext
Das Herzstück von KMP ist die Source-Sets-Struktur unter shared/src/:
- B4L08-0034 · Die `kotlin { }`-DSLkotlin
So sieht ein realistisches shared/build.gradle.kts aus:
- B4L08-0047 · Hello, Shared Worldkotlin
Damit du ein Gefühl dafür bekommst, was in der Praxis tatsächlich in commonMain landet, hier ein erstes konkretes Beispiel. Eine data class plus eine Funktion, die auf allen Plattformen lauffähig ist:
- B4L08-0077 · Hello, Shared Worldkotlin
Im Spring-Backend:
- B4L08-0081 · Projekt-Skeletttext
Verzeichnisstruktur:
- B4L08-0091 · Projekt-Skelettkotlin
settings.gradle.kts:
- B4L08-0122 · Das `:shared`-Modulkotlin
shared/build.gradle.kts:
- B4L08-0133 · Code in `commonMain`kotlin
shared/src/commonMain/kotlin/media/topred/shared/Greeting.kt:
- B4L08-0144 · Test in `commonTest`kotlin
shared/src/commonTest/kotlin/media/topred/shared/GreetingTest.kt:
- B4L08-0196 · Smoke-Test pro Plattform (optional, aber empfohlen)kotlin
shared/src/jvmMain/kotlin/media/topred/shared/JvmMain.kt:
Lesson 9
19 snippets- B4L09-001`expect class` — eine Klasse mit plattform-spezifischer Implementierungkotlin
Der Mechanismus funktioniert für drei Sprachelemente:
- B4L09-002`expect class` — eine Klasse mit plattform-spezifischer Implementierungkotlin
fun error(message: String, throwable: Throwable? = null)
- B4L09-003`expect class` — eine Klasse mit plattform-spezifischer Implementierungkotlin
Log.e(TAG, message, throwable)
- B4L09-004`expect class` — eine Klasse mit plattform-spezifischer Implementierungkotlin
NSLog("[ERROR] %@", message)
- B4L09-005`expect class` — eine Klasse mit plattform-spezifischer Implementierungkotlin
Drei Plattformen, ein gemeinsamer Vertrag, drei konkrete Implementierungen. Im commonMain-Code rufst du das so auf:
- B4L09-006`expect fun` — eine Funktion mit plattform-spezifischer Implementierungkotlin
Etwas leichter als eine ganze Klasse, gut für utilities. Beispiel: eine Funktion, die dir den aktuellen Plattform-Namen gibt.
- B4L09-007`expect val` — eine Property mit plattform-spezifischer Initialisierungkotlin
Selten gebraucht, aber sauber für Konstanten, die du in commonMain lesen, aber nicht in commonMain initialisieren kannst:
- B4L09-0117 · Stolperfalle: `expect class` mit Konstruktor-Argumentenkotlin
Eine subtile Sache. Diese Variante ist OK:
- B4L09-0127 · Stolperfalle: `expect class` mit Konstruktor-Argumentenkotlin
Diese hier sieht ähnlich aus, hat aber Tücken:
- B4L09-0147 · Stolperfalle: `expect class` mit Konstruktor-Argumentenkotlin
Variante A: Plattform-spezifische sekundäre Konstruktoren.
- B4L09-0157 · Stolperfalle: `expect class` mit Konstruktor-Argumentenkotlin
Variante B: expect interface plus plattform-spezifische Factory.
- B4L09-0167 · Stolperfalle: `expect class` mit Konstruktor-Argumentenkotlin
expect fun createFileStore(directoryName: String): FileStore
- B4L09-0171 · `expect class Platform` in `commonMain`kotlin
shared/src/commonMain/kotlin/media/topred/shared/Platform.kt:
- B4L09-0182 · `actual class` pro Plattformkotlin
shared/src/androidMain/kotlin/media/topred/shared/Platform.kt:
- B4L09-0192 · `actual class` pro Plattformkotlin
shared/src/iosMain/kotlin/media/topred/shared/Platform.kt:
- B4L09-0202 · `actual class` pro Plattformkotlin
shared/src/jvmMain/kotlin/media/topred/shared/Platform.kt:
- B4L09-0234 · Smoke-Lauf auf JVMkotlin
shared/src/jvmMain/kotlin/media/topred/shared/JvmMain.kt:
- B4L09-0265 · Das Interface-Pendantkotlin
Jetzt das Gegenstück. Datei shared/src/commonMain/kotlin/media/topred/shared/PlatformInfo.kt:
- B4L09-0276 · Test in `commonTest`kotlin
shared/src/commonTest/kotlin/media/topred/shared/PlatformInfoTest.kt:
Lesson 10
16 snippets- B4L10-001Wo lebt der gemeinsame Code?text
In dieser Lektion bauen wir Variante 2. Aufbau im Monorepo:
- B4L10-002DTOs — einmal definiertkotlin
Im :contract-Modul:
- B4L10-003Validation: was gemeinsam, was server-only?kotlin
die zusätzlich zur Konstruktor-Validierung greift:
- B4L10-004Ktor-Client — der HTTP-Adapterkotlin
Engines. Im Shared-Module:
- B4L10-005Engines — pro Plattform die richtige Wahlkotlin
Auswahl per expect/actual (siehe B4/L09):
- B4L10-006Engines — pro Plattform die richtige Wahlkotlin
): HttpClient
- B4L10-007Engines — pro Plattform die richtige Wahlkotlin
): HttpClient = HttpClient(OkHttp, block)
- B4L10-008Engines — pro Plattform die richtige Wahlkotlin
): HttpClient = HttpClient(Darwin, block)
- B4L10-009Content-Negotiation — derselbe JSON-Codec wie der Serverkotlin
Das ist der Boilerplate-Anteil. Ab hier ist die Welt wieder common.
- B4L10-010Content-Negotiation — derselbe JSON-Codec wie der Serverkotlin
Auf der Server-Seite (Spring) konfigurierst du denselben Codec:
- B4L10-011Auth-Plugin — Bearer-Tokens gegen Keycloakkotlin
Punkt 2 macht das Auth-Plugin mit bearer-Provider:
- B4L10-012Repository — der Layer, den deine ViewModels sehenkotlin
Plattformen identisch funktioniert.
- B4L10-013DI mit Koin — Composition Rootkotlin
in commonMain:
- B4L10-014Aufgabe 1 — `:contract`-Modul anlegenkotlin
Definiere darin:
- B4L10-016Aufgabe 4 — Repositorykotlin
Schreibe TicketRepository in commonMain:
- B4L10-017Aufgabe 5 — JVM-Demokotlin
Im jvmMain-Sourceset:
Lesson 11
12 snippets- B4L11-001Setupkotlin
passt — keine NoSuchColumnException zur Laufzeit.
- B4L11-002Schema und Queriessql
Die Datei liegt unter shared/src/commonMain/sqldelight/com/example/mobile/db/Ticket.sq:
- B4L11-003Schema und Querieskotlin
generierter Code unter build/generated/sqldelight/:
- B4L11-007Driver pro Plattformkotlin
NativeSqliteDriver(AppDatabase.Schema, "app.db")
- B4L11-008Repository mit DB-Cache und Network-Roundtripkotlin
willst: jeder Test bekommt eine frische DB ohne Disk-IO.
- B4L11-013Multiplatform-Settings — der Key-Value-Layerkotlin
PropertiesSettings(java.util.Properties().also { / load/save / })
- B4L11-015Keychain via Multiplatform-Settingskotlin
androidx.security:security-crypto:
- B4L11-016SQLCipher für die SQLite-Dateikotlin
unterstützt das via Drop-In-Driver-Replacement:
- B4L11-019Migrationenkotlin
Im Gradle-Config setzt du:
- B4L11-020Aufgabe 2 — Schema schreibensql
Datei shared/src/commonMain/sqldelight/com/example/mobile/db/Ticket.sq:
- B4L11-022Aufgabe 4 — Repository mit Flowkotlin
Schreibe TicketRepository neu (oder erweitere die Version aus
- B4L11-023Aufgabe 5 — JVM-Testkotlin
shared/src/commonTest/kotlin/com/example/mobile/data/TicketRepositoryTest.kt:
Lesson 12
18 snippets- B4L12-002Composable-Funktionenkotlin
Composition-Tree:
- B4L12-003State und `remember`kotlin
Composable-Tree einen Wert hält, der Recompositions überlebt.
- B4L12-005State Hoistingkotlin
State eine Ebene höher:
- B4L12-008Modifier-Chainkotlin
Modifier sind zusammensetzbar und werden in Reihenfolge angewandt:
- B4L12-009Material 3 als Default-Themekotlin
Du wickelst deine Root-Composable in MaterialTheme:
- B4L12-011`@Preview` — Live-Vorschaukotlin
Composables direkt in der IDE, ohne Emulator zu starten.
- B4L12-012`commonMain` als Compose-Heimatkotlin
sie plattform-unabhängig sind. Das Gradle-Setup im Shared-Module:
- B4L12-013`commonMain` als Compose-Heimatkotlin
oder iOS-Entry-Point auf:
- B4L12-014Aufgabe 1 — Compose-Plugins in `shared/`kotlin
In shared/build.gradle.kts:
- B4L12-015Aufgabe 1 — Compose-Plugins in `shared/`kotlin
Dependencies in commonMain:
- B4L12-016Aufgabe 2 — Counter-Composable in `commonMain`kotlin
Neue Datei shared/src/commonMain/kotlin/com/example/mobile/ui/Counter.kt:
- B4L12-017Aufgabe 3 — App-Wrapper mit Material 3kotlin
In derselben Datei oder einer neuen App.kt:
- B4L12-018Aufgabe 4 — Preview im Shared-Modulekotlin
und Android aufrufen.
- B4L12-019Aufgabe 5 — Android-Host-Modul (vorläufig)kotlin
mit build.gradle.kts:
- B4L12-020Aufgabe 5 — Android-Host-Modul (vorläufig)kotlin
Und eine MainActivity:
- B4L12-021Aufgabe 6 — Beobachtung: State-Hoisting in Aktionkotlin
Refactore CounterScreen so, dass der State eine Ebene höher
- B4L12-022Aufgabe 6 — Beobachtung: State-Hoisting in Aktionkotlin
durchprobieren:
- B4L12-023Aufgabe 7 — Bonus · Recomposition sichtbar machenkotlin
Füge oben in CounterContent ein einmaliges Log hinzu:
Lesson 13
25 snippets- B4L13-001Modul-Layout im Monorepotext
In einem KMP-Projekt liegt der Android-Host typischerweise neben
- B4L13-002`build.gradle.kts`kotlin
expect/actual-Implementierungen aus androidMain.
- B4L13-003`MainActivity` und `setContent`kotlin
android { ... }.
- B4L13-004`AndroidManifest.xml`xml
ist plattformunabhängig.
- B4L13-005Routes als `@Serializable`-Klassenkotlin
generiert daraus alles Nötige.
- B4L13-006`NavHost`kotlin
Argumente in den Back-Stack zu packen.
- B4L13-007Wo der NavHost sitztkotlin
besteht, sitzt der NavHost im Body des Scaffold:
- B4L13-008ViewModels in `commonMain`kotlin
auf beiden Plattformen verwenden.
- B4L13-010ViewModel in Composable konsumierenkotlin
wir in L14 zeigen.
- B4L13-011ViewModel mit Konstruktor-Parameternkotlin
benutzen. Mit Koin (DI-Setup aus B4/L10) reicht:
- B4L13-012Lifecycle-Effects in Composekotlin
LifecycleEventEffect:
- B4L13-013`LaunchedEffect` für Coroutine-Side-Effectskotlin
laden" umzusetzen.
- B4L13-014`DisposableEffect` für Cleanupkotlin
LaunchedEffect(Unit) { ... }.
- B4L13-015Permissions in Composekotlin
androidx.activity:activity-compose bringt:
- B4L13-016Edge-to-Edge und Status-Barkotlin
Content nicht unter die Bars rutscht, brauchst du Insets:
- B4L13-018Aufgabe 1 — `app-android/`-Modul anlegenkotlin
app-android/build.gradle.kts (Kurzform — Vollversion siehe
- B4L13-020Aufgabe 2 — ViewModel in `commonMain`kotlin
Datei shared/src/commonMain/kotlin/com/example/mobile/ui/TicketListViewModel.kt:
- B4L13-021Aufgabe 3 — Routes definierenkotlin
In shared/src/commonMain/kotlin/com/example/mobile/ui/Routes.kt:
- B4L13-022Aufgabe 4 — Composable Screenskotlin
shared/src/commonMain/kotlin/com/example/mobile/ui/TicketListScreen.kt:
- B4L13-023Aufgabe 4 — Composable Screenskotlin
Und shared/src/commonMain/kotlin/com/example/mobile/ui/TicketDetailScreen.kt:
- B4L13-024Aufgabe 5 — `App()` mit NavHostkotlin
shared/src/commonMain/kotlin/com/example/mobile/ui/App.kt:
- B4L13-025Aufgabe 6 — `MainActivity` und Manifestkotlin
app-android/src/main/kotlin/com/example/mobile/android/MainActivity.kt:
- B4L13-026Aufgabe 6 — `MainActivity` und Manifestxml
app-android/src/main/AndroidManifest.xml:
- B4L13-028Aufgabe 8 — Bonus · ViewModel-Factory ohne DIkotlin
ViewModelProvider.Factory, die das Repository injiziert:
- B4L13-029Aufgabe 8 — Bonus · ViewModel-Factory ohne DIkotlin
In der Composable:
Lesson 14
9 snippets- B4L14-001Setup im Monorepotext
Wir gehen von der Struktur aus, die seit L08 läuft:
- B4L14-002Setup im Monorepokotlin
Output als XCFramework, damit Xcode es einbinden kann.
- B4L14-003Den Compose-UIViewController bauenkotlin
das App()-Composable in einen ComposeUIViewController
- B4L14-006Hosting in SwiftUIswift
ist), wickelst du ihn in einen UIViewControllerRepresentable:
- B4L14-007Hosting in UIKitswift
als Root oder als Child setzen:
- B4L14-008Plattform-Themes und Layout-Directionkotlin
paar nützliche CompositionLocal-Werte mit:
- B4L14-009Plattform-Themes und Layout-Directionkotlin
expect/actual aus L09:
- B4L14-010Wrapper-Pattern für Swift-First-APIskotlin
schmale Wrapper-Funktionen, die ein idiomatisches Obj-C/Swift-API
- B4L14-015Aufgabe 5 — ContentView.swift mit ComposeViewswift
app-ios/iosApp/ContentView.swift:
Lesson 15
11 snippets- B4L15-001Setupkotlin
Compose-Plugin obendrauf:
- B4L15-002Die Entry-Functionkotlin
Top-Level-Functions in L14.
- B4L15-004Window, DialogWindow, Traykotlin
Ein typisches Service-Companion-Setup hat alle drei:
- B4L15-005File-Dialoge und OS-Integrationkotlin
AWT-FileDialog, das auf jedem OS den nativen System-Picker
- B4L15-006File-Dialoge und OS-Integrationkotlin
zu setzen, damit das App-Verhalten nativ wirkt:
- B4L15-007Build-Pipelinebash
Drei Gradle-Tasks decken den Tag ab:
- B4L15-008Resources und Iconskotlin
App-Icon im Distributable:
- B4L15-011Aufgabe 2 — Build-Skriptkotlin
app-desktop/build.gradle.kts:
- B4L15-012Aufgabe 3 — Main.ktkotlin
app-desktop/src/main/kotlin/com/example/mobile/Main.kt:
- B4L15-015Aufgabe 5 — Tray und Toggle-Windowkotlin
Ergänze in Main.kt:
- B4L15-016Aufgabe 6 — File-Dialogkotlin
Button, der einen nativen File-Picker öffnet:
Lesson 16
25 snippets- B4L16-001ColorSchemekotlin
Compose entscheidet pro Theme-Aufruf, welches gerade greift.
- B4L16-002Dynamic Color (nur Android)kotlin
Rest fällt zurück auf das fest definierte Schema:
- B4L16-003Typographykotlin
Label Small (11sp). Du überschreibst nur, was du wirklich
- B4L16-004Shapeskotlin
Cards → medium, Dialogs → large). Eckenradien definierst du
- B4L16-005Setupkotlin
In der Gradle-Datei des Shared-Moduls:
- B4L16-006Setuptext
Der Resources-Folder liegt im Common-Source-Set:
- B4L16-007Bilderkotlin
Common-Source-Set und ist auf allen Plattformen zugreifbar.
- B4L16-008Stringsxml
sind Android-spezifisch — auf Multiplatform-Projekten nimmst
- B4L16-009Stringskotlin
</resources>
- B4L16-010Fontskotlin
den Platzhalter.
- B4L16-014Folder-Suffixexml
auch fehlt, den aus values/.
- B4L16-015User-Wahl überschreibenkotlin
ein In-App-Switch braucht ein bisschen Eigenarbeit.
- B4L16-017Pluralsxml
erwartet eine plurals-Datei pro Locale:
- B4L16-018Pluralskotlin
<item quantity="other">%1$d Aufträge</item>
- B4L16-020Datums-Formatierung mit `kotlinx-datetime`kotlin
kotlinx-datetime, das du im Backend (Spring) ohnehin schon
- B4L16-022Aufgabe 1 — Theme mit Brand-Farbenkotlin
Light- und Dark-Variante:
- B4L16-023Aufgabe 1 — Theme mit Brand-Farbenkotlin
isSystemInDarkTheme() als Default nimmt und beide Schemen
- B4L16-024Aufgabe 1 — Theme mit Brand-Farbenkotlin
In App.kt wrappst du den ganzen Content:
- B4L16-025Aufgabe 2 — Logo als Compose-Resourcekotlin
Aktiviere im Gradle-File die Resource-Generierung:
- B4L16-026Aufgabe 2 — Logo als Compose-Resourcekotlin
Im UI bindest du das Logo ein:
- B4L16-027Aufgabe 3 — Strings in zwei Sprachenxml
Lege zwei String-Files an:
- B4L16-028Aufgabe 3 — Strings in zwei Sprachenxml
</resources>
- B4L16-029Aufgabe 3 — Strings in zwei Sprachenkotlin
stringResource(...):
- B4L16-030Aufgabe 4 — Plural-Form für Ticketsxml
Erweitere die strings.xml um eine Plural-Resource:
- B4L16-031Aufgabe 4 — Plural-Form für Ticketskotlin
Tickets ausgibt:
Lesson 17
26 snippets- B4L17-001Monorepo-Strukturtext
bei jedem neuen Projekt zwei Tage Setup-Arbeit.
- B4L17-002Monorepo-Strukturkotlin
settings.gradle.kts enthält alle Module:
- B4L17-003Version-Catalogs durchgängigtoml
gradle/libs.versions.toml:
- B4L17-004Backend-Erweiterung — `Ticket`-Aggregatekotlin
backend/src/main/kotlin/com/example/companion/ticket/Ticket.kt:
- B4L17-005Backend-Erweiterung — `Ticket`-Aggregatekotlin
Das Repository ist trivial — die Stärke von Data JDBC (B2/L07):
- B4L17-006Backend-Erweiterung — `Ticket`-Aggregatekotlin
Der Controller nimmt die fünf Endpoints, die das Mobile braucht:
- B4L17-007Backend-Erweiterung — `Ticket`-Aggregatesql
Flyway-Migration V2createtickets.sql:
- B4L17-008Contract-Modul — gemeinsame DTOskotlin
contract/build.gradle.kts:
- B4L17-009Contract-Modul — gemeinsame DTOskotlin
contract/src/commonMain/kotlin/com/example/companion/contract/TicketDto.kt:
- B4L17-010Shared-Module — Repository plus Serviceskotlin
shared/build.gradle.kts:
- B4L17-011Shared-Module — Repository plus Serviceskotlin
TicketRepository als Ktor-Client (B4/L10):
- B4L17-012Shared-Module — Repository plus Serviceskotlin
Offline-Schreiben und das Hochsyncen:
- B4L17-013Shared-Module — Repository plus Serviceskotlin
Das Koin-Modul bündelt die Abhängigkeiten:
- B4L17-014Schema-Erweiterung für Sync-Feldersql
Domain-Daten. Tickets.sq (SQLDelight, B4/L11):
- B4L17-015Read-Pathkotlin
ist die App auch offline benutzbar — ohne Wenn und Aber.
- B4L17-017Write-Path — Optimistic Local Writekotlin
Hintergrund pusht ein Worker das Ticket ans Backend.
- B4L17-018Optimistic Locking — was der Server prüftkotlin
zuletzt vom Server gesehen hat. Backend prüft:
- B4L17-020Tombstones für Deleteskotlin
bevor er den Pull macht.
- B4L17-021Polling vs. Pushkotlin
fünf Minuten sync() aufruft, solange die App im Vordergrund
- B4L17-022Build-Pipelinebash
Alles läuft über Gradle, alles aus dem Wurzelverzeichnis:
- B4L17-023Docker-Compose für lokales Setupyaml
docker-compose.yml:
- B4L17-025Aufgabe 1 — Monorepo-Struktur anlegentext
Erstelle das Verzeichnis service-companion/ mit folgenden
- B4L17-026Aufgabe 1 — Monorepo-Struktur anlegenkotlin
settings.gradle.kts registriert alle Module:
- B4L17-0284.4 Koin-Modulkotlin
ist DELETE, danach purgeDeleted.
- B4L17-0295.2 ViewModels mit Koinkotlin
service.changeStatus(id, newStatus).
- B4L17-031Aufgabe 6 — Docker-Compose und Keycloakyaml
backend/src/main/resources/application.yml zeigt auf den
Lesson 18
11 snippets- B4L18-001Keystore anlegenbash
Einmalig auf deinem Entwicklungsrechner:
- B4L18-002Signing-Config in `build.gradle.kts`kotlin
im CI aus den Action-Secrets:
- B4L18-003R8 und ProGuardproguard
@Serializable-Klassen ihre Companion-Objekte behalten:
- B4L18-004Versioningkotlin
Wochen später vergisst du, ob du den Wert schon hochgezählt hast.
- B4L18-006Upload automatisierenkotlin
JSON-Key und referenzierst ihn aus dem Build:
- B4L18-007Build im Terminalbash
dem Shell-Skript. Der Kern-Aufruf:
- B4L18-010Fastlane oder Gradle pur?ruby
Eine Fastfile für beides:
- B4L18-011CI mit GitHub Actionsyaml
Die Datei .github/workflows/release.yml:
- B4L18-012Aufgabe 1 — Android-Keystore und Signing-Configbash
das verschlüsselte Backup.
- B4L18-021Aufgabe 4 — iOS-Build via xcodebuildxml
Xcode-Workspace. Erzeuge eine ExportOptions.plist auf gleicher
- B4L18-022Aufgabe 4 — iOS-Build via xcodebuildbash
Lokaler Build:
Lesson 19
19 snippets- B4L19-001Dependencykotlin
mitgeliefert.
- B4L19-002Dependencykotlin
zeigen den iOS-Pod-Block:
- B4L19-003Initialisierungkotlin
Im commonMain definierst du eine Hilfsfunktion:
- B4L19-004Aufruf auf Androidkotlin
1.0 bleiben für vollständige Sicht.
- B4L19-006Aufruf auf iOSswift
In Swift, im AppDelegate oder direkt am App-Start:
- B4L19-007Crashes fangenkotlin
Eigene Exceptions schickst du explizit:
- B4L19-008Breadcrumbskotlin
domänen-spezifische Events:
- B4L19-011iOSswift
in Swift einbinden:
- B4L19-012Mobile-Seite: `traceparent`-Header setzenkotlin
Header automatisch ein:
- B4L19-013Backend-Seite: Header aufnehmenkotlin
DSN-Projekt), erscheinen Mobile-Tap und Backend-SQL im gleichen
- B4L19-0141. Opt-In statt Opt-Outkotlin
App-Start einen Dialog zeigen:
- B4L19-0154. PII-Strippingkotlin
identifizierende Daten heraus:
- B4L19-016Aufgabe 1 — Dependency und Initkotlin
In shared/build.gradle.kts ergänzt du:
- B4L19-018Aufgabe 1 — Dependency und Initswift
Auf iOS in iOSApp.swift:
- B4L19-019Aufgabe 2 — Crash provozierenkotlin
Baue in den TicketListScreen einen Debug-Button:
- B4L19-020Aufgabe 3 — Custom Error mit Kontextkotlin
Im TicketRepository.sync() wrappst du den API-Aufruf:
- B4L19-021Aufgabe 4 — Backend-Brückekotlin
Sentry-Ktor-Plugin oder propagierst traceparent manuell:
- B4L19-022Aufgabe 5 — DSGVO-Hookskotlin
Erweitere Telemetry.init:
- B4L19-023Aufgabe 6 (optional) — Opt-In-Dialogkotlin
Wahl über Settings (B4/L11):
Lesson 20
22 snippets- B4L20-001In-App-Versionscheckkotlin
Backend: ist meine Version noch supported?
- B4L20-002In-App-Versionscheckkotlin
Backend-Seite (Spring), ein einfacher Endpoint:
- B4L20-003Force-Update vs. Soft-Updatekotlin
Das UI reagiert dann unterschiedlich:
- B4L20-005Force-Update vs. Soft-Updatekotlin
expect fun openStore()
- B4L20-006Native In-App-Updateskotlin
Store verlinken.
- B4L20-007OTA-Updates für Native-Code? Praktisch: nein.kotlin
und Sentry Feature Flags sind die etablierten Lösungen.
- B4L20-008Setup Androidkotlin
Im Code registrierst du den FCM-Service:
- B4L20-010Setup Androidkotlin
Android 13+ verlangt zusätzlich die Notification-Permission zur
- B4L20-011Setup iOSswift
einbinden. GoogleService-Info.plist ins iOS-Target.
- B4L20-013Backend-Seitekotlin
Einmal beim Application-Start initialisieren:
- B4L20-014Backend-Seitekotlin
Token vom Mobile-Client landet in deiner Datenbank:
- B4L20-015Backend-Seitekotlin
Push senden:
- B4L20-016Android Network Security Configxml
über die Network-Security-Config:
- B4L20-018Pinning in Ktorkotlin
expect fun pinnedHttpClient(): HttpClient
- B4L20-020Monitoring im Solo-Setupyaml
Spring-Actuator bringt das mit.
- B4L20-021Aufgabe 1 — Version-Check-Endpoint im Backendkotlin
Im Spring-Backend ergänzt du:
- B4L20-024Aufgabe 2 — Mobile-Version-Checkkotlin
Im AppScreen rufst du den Check beim App-Start auf:
- B4L20-025Aufgabe 2 — Mobile-Version-Checkkotlin
ForceUpdateScreen:
- B4L20-026Androidkotlin
app-android/build.gradle.kts:
- B4L20-029Aufgabe 4 — Push vom Backend aus sendenkotlin
Token-Endpoint:
- B4L20-032Aufgabe 5 — Token-Cleanupkotlin
aufgeräumt werden:
- B4L20-034Aufgabe 6 — Network-Security-Config sauber trennenxml
Die Debug-Variante erlaubt Cleartext für lokale Backends:
Book 5 — Kotlin in the Microsoft Stack
274 snippetsLesson 1
2 snippets- B5L01-0014 · M365 CLI als Sanity-Checkbash
Damit du gleich weißt, dass dein Tenant lebendig ist und du programmatisch dranreichst, installier einmal das M365 CLI. Das ist eine Node-basierte Kommandozeile, mit der du die wichtigsten M365-Operationen scripten kanns
- B5L01-0035 · Notizen zentral ablegenmarkdown
In notes.md notiere (in einer Form, die du in den nächsten Lektionen wiederfindest):
Lesson 3
15 snippets- B5L03-0012.1 · Client Credentials (App-Only)kotlin
Code-Form (Vorschau):
- B5L03-0022.3 · On-Behalf-Of (OBO, Middle-Tier-API)kotlin
Voraussetzung: Deine API-Registration muss Graph als „API permissions" enthalten, mit den nötigen Delegated-Scopes. Außerdem muss im Portal unter „Expose an API" eine Scope für deine eigene API definiert sein (api://{cli
- B5L03-0033.1 · Gradle Kotlin DSLkotlin
Ich zeig dir den vollständigen Setup für den Client-Credentials-Flow. Das ist der häufigste Fall in Backend-Services und der, mit dem wir in den nächsten Lektionen Graph-Aufrufe machen.
- B5L03-0043.2 · `application.yml`yaml
useJUnitPlatform()
- B5L03-0053.3 · Konfigurations-Klassekotlin
Die Werte kommen aus Environment-Variablen, nicht hartcodiert. Beim Start setzt du sie über export AZURETENANTID=... oder lädst die .env mit direnv oder einer Run-Configuration in IntelliJ. Niemals Secrets ins YAML schre
- B5L03-0063.3 · Konfigurations-Klassekotlin
val clientSecret: String
- B5L03-0073.4 · `EntraTokenService` — die Kern-Klassekotlin
runApplication<MsStackApplication>(args)
- B5L03-0084 · Token-Cache persistent machen (optional)kotlin
Wenn du das Token-Caching über JVM-Restarts oder über Pod-Restarts in Kubernetes hinweg persistent haben willst, implementier ITokenCacheAccessAspect:
- B5L03-0105 · Resource Server gegen Entra IDkotlin
Spring Security 7 zieht aus der Issuer-URL automatisch die JWKS-Schlüssel und validiert eingehende Bearer-Tokens. Eine minimale Security-Config sieht so aus:
- B5L03-0123 · `application.yml`yaml
Lösch das application.properties, das der Initializr angelegt hat (oder belass es leer), und erstell src/main/resources/application.yml:
- B5L03-0134 · `AzureProperties` und Application-Klassekotlin
src/main/kotlin/com/example/msstack/config/AzureProperties.kt:
- B5L03-0144 · `AzureProperties` und Application-Klassekotlin
Die MsStackApplication.kt, die der Initializr generiert hat, um @EnableConfigurationProperties ergänzen:
- B5L03-0155 · `EntraTokenService` schreibenkotlin
src/main/kotlin/com/example/msstack/auth/EntraTokenService.kt:
- B5L03-0166 · Security-Config mit Test-Endpoint offenkotlin
src/main/kotlin/com/example/msstack/security/SecurityConfig.kt:
- B5L03-0177 · Test-Controllerkotlin
src/main/kotlin/com/example/msstack/web/TokenTestController.kt:
Lesson 4
17 snippets- B5L04-0012 · Setup — `GraphServiceClient` als Spring-Beankotlin
src/main/kotlin/com/example/msstack/graph/GraphConfig.kt:
- B5L04-0022 · Setup — `GraphServiceClient` als Spring-Beankotlin
Box am Rand — Connection-Pool steuern: Das SDK nutzt intern OkHttp. Default-Pool reicht für die meisten Anwendungen. Wenn du in einer Lambda-/Container-Welt sehr viele kurze Calls absetzt, lohnt sich ein dedizierter OkHt
- B5L04-0043.2 · Query-Parameter — `$top`, `$select`, `$filter`kotlin
OData-Query-Parameter setzt du über eine Konfigurations-Lambda:
- B5L04-0074 · Pagination — wenn die Liste länger ist als eine Seitekotlin
Das SDK abstrahiert das mit PageIterator:
- B5L04-0084 · Pagination — wenn die Liste länger ist als eine Seitekotlin
Wer das öfter braucht, schreibt sich einen Extension-Wrapper:
- B5L04-0095 · Error-Handlingkotlin
Default-Behandlung in Spring:
- B5L04-0107 · Konkretes Beispiel — `GraphService.kt`kotlin
So sieht ein erster sauberer Service aus, der eine User-Liste mit DTO-Mapping zurückgibt:
- B5L04-0117 · Konkretes Beispiel — `GraphService.kt`kotlin
Und der zugehörige Controller:
- B5L04-0138 · Vorschau — Binärinhalte streamenkotlin
Sobald du Files oder Profilbilder anfasst, geht es um Streams statt JSON. Das SDK liefert InputStream-basierte Endpunkte:
- B5L04-0152 · `GraphConfig.kt`kotlin
src/main/kotlin/com/example/msstack/graph/GraphConfig.kt:
- B5L04-0163 · `GraphService.kt` mit `listFirstUsers`kotlin
src/main/kotlin/com/example/msstack/graph/GraphService.kt:
- B5L04-0174 · `UserController.kt`kotlin
src/main/kotlin/com/example/msstack/web/UserController.kt:
- B5L04-0185 · Security-Config aktualisierenkotlin
In SecurityConfig.kt den User-Endpoint vorerst auch ohne Auth zugänglich machen (in L11 schützen wir ihn):
- B5L04-0196 · `ApiException`-Handlerkotlin
src/main/kotlin/com/example/msstack/graph/GraphExceptionHandler.kt:
- B5L04-0238 · Optional — Profilbild als Stream-Endpointkotlin
Erweitere GraphService.kt:
- B5L04-0248 · Optional — Profilbild als Stream-Endpointkotlin
Erweitere UserController.kt:
- B5L04-0258 · Optional — Profilbild als Stream-Endpointbash
.body(bytes)
Lesson 5
18 snippets- B5L05-0012.2 · OData-Filtertext
Graph spricht OData. Die wichtigsten Query-Parameter, die du häufiger als jeden anderen Graph-Code-Block tippen wirst:
- B5L05-0022.2 · OData-Filterkotlin
Aus dem SDK heraus formulierst du das mit den requestConfiguration-Lambdas:
- B5L05-0032.3 · `bodyPreview` versus `body.content`kotlin
Message.body ist ein ItemBody-Objekt mit contentType (HTML oder TEXT) und content (der eigentliche Body). Wenn du body lädst, ziehst du potenziell ein paar Megabyte HTML pro Mail — Bilder als Inline-Attachments machen da
- B5L05-0043 · Mails sendenkotlin
Zwei Wege. POST /me/sendMail schickt direkt raus, ohne Draft anzulegen. POST /me/messages erstellt einen Draft, den du nochmal anfassen und dann mit POST /me/messages/{id}/send rausjagen kannst. Für automatisierte Versen
- B5L05-0053.1 · Anhängekotlin
Kleine Anhänge (< 3 MB) hängst du als FileAttachment direkt an die Message. Größere brauchen eine Upload-Session, das machen wir in L06 für OneDrive ausführlich — bei Mail-Anhängen gilt dasselbe Pattern, nur über /me/mes
- B5L05-0064.1 · Endpoints und Konzeptekotlin
calendarView ist subtiler als events. events gibt dir die Master-Events, also bei einem wöchentlichen Termin einen Eintrag mit recurrence-Property. calendarView expandiert diese Wiederholungen — wenn du nach allen Termin
- B5L05-0074.2 · Recurring Eventskotlin
Ein wiederkehrender Termin hat eine recurrence-Property mit zwei Teilen:
- B5L05-0084.3 · `findMeetingTimes`kotlin
Eine der Endpunkt-Funktionen, die im Alltag selten, aber dann sehr nützlich sind. Du gibst eine Liste von Attendees und einen Zeitrahmen mit, Graph schaut in alle Kalender und schlägt freie Slots vor. Gut für Booking-Too
- B5L05-0095 · Tasks — drei Weltenkotlin
Microsoft To-Do ist das aktuelle persönliche Task-System. Listen, in den Listen Tasks, an Tasks Subtasks (checklistItems), Erinnerungen, Fälligkeitsdaten. Alles für „Mirko, kauf noch Milch" — nicht für „das Marketing-Tea
- B5L05-0105 · Tasks — drei Weltenkotlin
Microsoft Planner ist Team-Aufgaben. Ein Plan gehört zu einer Microsoft-365-Group (also einem Team in Teams). Jeder Plan hat Buckets (entsprechen Kanban-Spalten), in den Buckets sitzen Tasks. Tasks haben Assignees (mehre
- B5L05-0116 · Service-Layer in Kotlinkotlin
Das, was im Kunden-Projekt am Ende stehen sollte: ein MailService und ein CalendarService, der die Graph-Calls kapselt und domänenspezifische DTOs zurückgibt. Der Controller spricht den Service, der Service spricht Graph
- B5L05-0126 · Service-Layer in Kotlinkotlin
Der Controller dazu:
- B5L05-0132 · DTOs anlegenkotlin
In com.example.graphapp.mail:
- B5L05-0142 · DTOs anlegenkotlin
In com.example.graphapp.calendar:
- B5L05-0153 · `MailService` schreibenkotlin
Im Package com.example.graphapp.mail:
- B5L05-0174 · `CalendarService` schreibenkotlin
Im Service liest du das über @Value("\${graph.test-user-upn}") ein und ersetzt graph.me() durch graph.users().byUserId(testUserUpn).
- B5L05-0185 · Controller verdrahtenkotlin
return LocalDateTime.parse(dt.dateTime).atZone(zone).toOffsetDateTime()
- B5L05-0196 · Testenbash
Mit curl (oder Postman/IntelliJ-HTTP-Client) gegen die laufende App:
Lesson 6
7 snippets- B5L06-0012.1 · Root-Children listenkotlin
Das einfachste Pattern: alle Items im Wurzelverzeichnis des persönlichen Drives.
- B5L06-0074.1 · Kleine Dateien — `PUT /content`kotlin
Alles unter 4 MB geht in einem einzigen Request:
- B5L06-0084.2 · Große Dateien — Upload-Sessionkotlin
3. Letzter erfolgreicher Chunk liefert das fertige DriveItem.
- B5L06-0147 · Dataverse — der Seitenausflugkotlin
In Kotlin/Java gibt es kein offizielles Dataverse-SDK. Du baust HTTP-Calls mit OkHttp oder dem Spring-WebClient — und ein zweites MSAL-Setup für den eigenen Scope:
- B5L06-0152 · DTOskotlin
In com.example.graphapp.files:
- B5L06-0163 · `FilesService` schreibenkotlin
val modifiedAt: OffsetDateTime,
- B5L06-0174 · Controller mit drei Routenkotlin
modifiedAt = lastModifiedDateTime ?: OffsetDateTime.now(),
Lesson 7
15 snippets- B5L07-0012 · Eine Subscription anlegenjson
POST /subscriptions mit folgendem Body:
- B5L07-0022 · Eine Subscription anlegenkotlin
Aus dem SDK:
- B5L07-0043 · Der Validation-Handshakekotlin
In Spring sieht das so aus:
- B5L07-0054 · Notifications verarbeitenjson
Wenn der Handshake durchgegangen ist, schickt Graph dir Notifications. Format:
- B5L07-0064 · Notifications verarbeitenkotlin
Service-Seite:
- B5L07-0074 · Notifications verarbeitenkotlin
Im Controller:
- B5L07-0085 · Die 10-Sekunden-Regelkotlin
In Buch 5 nehme ich @Async, weil es ohne weitere Infrastruktur funktioniert und für die meisten Kundenszenarien reicht. Für Production mit Hochlast ist die Outbox-Variante sauberer.
- B5L07-0096 · Lifecycle-Notificationskotlin
Subscription mit Lifecycle-URL:
- B5L07-0107 · Renewalkotlin
Subscriptions laufen ab. Mail/Kalender nach maximal 70 Stunden, Teams-Chats nach 60 Minuten. Wer eine Subscription am Leben halten will, schickt vor Ablauf ein PATCH /subscriptions/{id} mit neuem expirationDateTime:
- B5L07-0117 · Renewalkotlin
Das machst du am sinnvollsten mit einem Scheduler:
- B5L07-0128 · Verschlüsselte Notifications (Teams)kotlin
4. In deinem Notification-Handler dekodierst du das mit dem Private Key.
- B5L07-0172 · Webhook-Controller anlegenkotlin
In com.example.graphapp.webhook:
- B5L07-0193 · Async-Konfiguration und Processorkotlin
client-state: "${SUBSCRIPTIONSECRET:dev-default-secret}"
- B5L07-0204 · Subscription-Bootstrapkotlin
Einen kleinen Endpoint, mit dem du die Subscription erstellst (statt das in @PostConstruct zu machen — dann hast du die Kontrolle, wann es passiert):
- B5L07-0277 · Bonus — Auto-Renewal mit Schedulerkotlin
Bau einen @Scheduled-Job, der alle 15 Minuten alle Subscriptions im Tenant durchgeht und die, die in unter 30 Minuten ablaufen, automatisch verlängert. Logge jedes Renewal.
Lesson 8
22 snippets- B5L08-0011 · Warum Batchkotlin
Stell dir vor: dein Kunde hat 250 Mitarbeitende. Für ein Dashboard willst du von jedem das Profilbild, den Manager und die Abteilung holen. Naiv:
- B5L08-0022 · Die `$batch`-Anatomiejson
Du schickst an POST https://graph.microsoft.com/v1.0/$batch ein JSON mit dieser Form:
- B5L08-0032 · Die `$batch`-Anatomiejson
Die Antwort sieht spiegelbildlich aus:
- B5L08-0043 · Batch im Graph SDKkotlin
Das SDK v6.x hat einen BatchRequestContentCollection-Builder, der dir die JSON-Konstruktion abnimmt. Stand 2026 ist die API noch ein bisschen kratzig, weil sie aus der Java-SDK-Tradition kommt — du baust die einzelnen Su
- B5L08-0075.1 · Retry mit Retry-After-Respektyaml
Resilience4j hat eine Retry-Konfiguration mit intervalFunction. Du gibst eine Funktion mit, die aus „Exception + bisheriger Attempt-Count" einen Wartezeitraum berechnet:
- B5L08-0095.1 · Retry mit Retry-After-Respektkotlin
Dann schreibst du eine Funktion, die Graph-429er in diese Exception umwandelt:
- B5L08-0105.1 · Retry mit Retry-After-Respektkotlin
Und die Retry-Konfiguration baut darauf auf:
- B5L08-0115.2 · Circuit Breakeryaml
Wenn die Graph-API für 30 Sekunden tot ist, willst du nicht jeden Request 30 Sekunden lang warten lassen, der dann doch in eine Timeout-Exception läuft. Circuit Breaker macht den Pfad „aus" nach n Fehlern und reicht sofo
- B5L08-0125.2 · Circuit Breakerkotlin
Übersetzung: schau auf die letzten 10 Calls. Wenn mehr als 50 % davon fehlschlugen, schalt den Breaker offen — alle weiteren Calls werden für 30 Sekunden sofort abgelehnt. Nach 30 Sekunden schalt halb-offen, lass 3 Test-
- B5L08-0135.3 · Bulkheadyaml
Wenn dein Backend 100 parallele Web-Requests bedient und jeder einen Graph-Call macht, würdest du 100 parallele HTTP-Connections auf Graph aufmachen. Das ist für deinen Connection-Pool meist zu viel, und Graph mag das au
- B5L08-0156 · Komplette `ResilientGraphService` als Beispielkotlin
Mit Spring-Annotations geht es kompakter, aber auch ein bisschen magischer. Hier die Variante über @Retry, @CircuitBreaker, @Bulkhead-Annotations:
- B5L08-0161 · Dependencies ergänzenkotlin
In build.gradle.kts:
- B5L08-0172 · DTOs und Exceptionkotlin
In com.example.graphapp.bulk:
- B5L08-0183 · Sequenzielle Variantekotlin
) : RuntimeException(message)
- B5L08-0194 · Batch-Variantekotlin
Im selben Service:
- B5L08-0205 · Resilience4j konfigurierenyaml
In application.yml:
- B5L08-0215 · Resilience4j konfigurierenkotlin
Eine Custom-Retry-Config, die Retry-After respektiert:
- B5L08-0225 · Resilience4j konfigurierenkotlin
throw ThrottledException(retryAfter, "Throttled, retry after ${retryAfter}s")
- B5L08-0236 · Controller mit Zeitmessungkotlin
withGraphErrorHandling { delegate.loadBatched(upns) }
- B5L08-0247 · Testenbash
Du brauchst eine Liste von UPNs aus deinem Tenant. Wenn dir 50 Test-User fehlen, nimm im Notfall 20 reale Accounts plus 30 erfundene — die 30 Misses sind für den Zeitvergleich nicht schlimm, weil Graph trotzdem eine Antw
- B5L08-0257 · Testenmarkdown
Schreib die Werte in notes.md:
- B5L08-0268 · 429 künstlich produzieren (Bonus)kotlin
Schreib einen Test mit MockK, der einen HTTP-Client mockt, beim ersten Call ein 429 mit Retry-After: 3 zurückgibt und beim zweiten Call eine echte Antwort. Prüf, dass das Retry wirklich 3 Sekunden wartet (mit Thread.slee
Lesson 9
12 snippets- B5L09-0023 · ActivityHandler — das zentrale Patternkotlin
Eine minimale Subklasse in Kotlin:
- B5L09-0044 · Der Adapter und der Spring-Endpointkotlin
Damit Spring die eingehende POST-Anfrage von Azure Bot Service an deinen Bot weitergibt, brauchst du zwei Beans und einen Controller.
- B5L09-0054 · Der Adapter und der Spring-Endpointkotlin
Der Controller:
- B5L09-0086 · State — Conversation und Userkotlin
Ein State-Property anlegen und nutzen sieht so aus:
- B5L09-0101 · Projekt-Skelett anlegenkotlin
In build.gradle.kts ergänzt du die SDK-Dependencies:
- B5L09-0112 · application.yml für lokalen Betriebyaml
Lösch die generierte application.properties und leg src/main/resources/application.yml an:
- B5L09-0123 · EchoBot-Klasse anlegenkotlin
Datei src/main/kotlin/com/example/teams/bot/EchoBot.kt:
- B5L09-0134 · Spring-Config für den Adapterkotlin
Datei src/main/kotlin/com/example/teams/bot/config/BotConfig.kt:
- B5L09-0145 · Controller mit /api/messageskotlin
Datei src/main/kotlin/com/example/teams/bot/web/BotController.kt:
- B5L09-0178 · Bonus — ConversationState einbauenkotlin
Erweitere BotConfig.kt:
- B5L09-0188 · Bonus — ConversationState einbauenkotlin
Erweitere EchoBot.kt:
- B5L09-019D · Häufige Stolperfallenkotlin
Jackson-Version-Konflikt. Das bot-builder-JAR zieht eine alte Jackson-Variante mit. Wenn du NoSuchMethodError bei com.fasterxml.jackson.databind. siehst, ergänze in build.gradle.kts:
Lesson 10
15 snippets- B5L10-0012 · Schema 1.6 — die Bausteinejson
Eine minimale Card sieht so aus:
- B5L10-0024 · Eine erste Card sendenkotlin
Im Bot Framework SDK sendest du keine Card direkt, sondern eine Activity mit Attachment. Das Attachment trägt die Card als JSON-Object im content-Feld und einen Content-Type-String, der den Card-Renderer in Teams aktivie
- B5L10-0045 · Submit verarbeitenjson
Wenn der User „Approve" drückt, schickt Teams eine neue Activity an deinen Bot. Sie sieht aus wie eine normale message-Activity, hat aber kein text-Feld — stattdessen ist value gesetzt:
- B5L10-0055 · Submit verarbeitenkotlin
In onMessageActivity erkennst du das so:
- B5L10-0066 · Card-Refresh und Card-Updatekotlin
Sauberer ist: die ursprüngliche Card nach dem Klick durch eine Status-Card ersetzen. Das SDK kann das über turnContext.updateActivity(...). Die activityId der ursprünglichen Card-Aktivität kommt beim Submit als activity.
- B5L10-0077 · Eine Mini-DSL für Cardskotlin
Wenn du nach drei Cards merkst, dass die String-Templates aus Kapitel 4 nicht mehr handhabbar sind, ist es Zeit für eine kleine Kotlin-DSL. Lambdas-mit-Receiver (B1/L09) sind genau das Werkzeug dafür.
- B5L10-0087 · Eine Mini-DSL für Cardskotlin
Damit wird die Approval-Card zu:
- B5L10-0091 · Bot-Klasse umbenennen und erweiternkotlin
Bisher hast du EchoBot.kt. Damit die nächsten Übungen sauber aufeinander aufbauen, benenn die Klasse in ApprovalBot.kt um (im Refactor-Menü, damit der @Component weiter funktioniert). Die Datei sollte am Ende dieses Schr
- B5L10-0102 · Approval-Card als JSON-Templatejson
Leg eine Datei src/main/resources/cards/approval.json an:
- B5L10-0113 · Methode zum Senden der Cardkotlin
Ergänze in ApprovalBot.kt:
- B5L10-0124 · Submit-Handlerkotlin
Erst eine kleine Data-Class für das typsichere Mapping:
- B5L10-0134 · Submit-Handlerkotlin
Dann der Handler in ApprovalBot.kt:
- B5L10-0146 · Card nach dem Klick durch Status-Card ersetzenjson
Bau eine zweite Card-Datei src/main/resources/cards/status.json:
- B5L10-0156 · Card nach dem Klick durch Status-Card ersetzenkotlin
Passt handleCardSubmit so an, dass es updateActivity benutzt:
- B5L10-0167 · Bonus — Kotlin-Mini-DSL bauenkotlin
Wenn du genug von String-Templates hast, bau dir die kleine DSL aus Lektion 10/Sektion 7 nach. Erweitere sie um container { }, factSet { fact(title, value) } und actions { submit(...); openUrl(...) }. Wenn du fertig bist
Lesson 11
15 snippets- B5L11-0012 · Das Tab-Manifestjson
Ein minimales Manifest, das nur einen Personal-Tab enthält, sieht so aus:
- B5L11-0034 · Das Tab-Frontend — minimal, in Vanilla JShtml
Datei src/main/resources/static/tab/index.html:
- B5L11-0045 · On-Behalf-Of-Flow im Spring-Backendkotlin
Den MSAL4J-Setup hast du in B5/L03 schon gesehen. Ergänz jetzt eine OBO-spezifische Methode.
- B5L11-0055 · On-Behalf-Of-Flow im Spring-Backendkotlin
Und der Endpoint, der Tab aufruft:
- B5L11-0065 · On-Behalf-Of-Flow im Spring-Backendkotlin
Spring-Security-Setup für den Resource-Server hat sich in Spring Security 7.0 gegenüber 6.x leicht verändert — die SecurityFilterChain-Konfiguration läuft jetzt über Lambda-DSL als Default:
- B5L11-0097 · Was im Backend passiert, wenn der Tab nicht weiß, was es willjavascript
Skelett im Tab-Frontend:
- B5L11-0108 · SharePoint-Files als Bonus-Use-Casekotlin
Sobald der Tab das Profil anzeigt, ist der Schritt klein zu beliebigen anderen Graph-Endpoints. Eine SharePoint-Site mit ihren Files anzuzeigen, sieht im Backend so aus:
- B5L11-0111 · Neues Spring-Boot-Projekt anlegenkotlin
In build.gradle.kts ergänzen:
- B5L11-0144 · application.ymlyaml
src/main/resources/application.yml:
- B5L11-0155 · Properties-Klassekotlin
src/main/kotlin/com/example/teams/tab/config/AzureAdProperties.kt:
- B5L11-0166 · MSAL4J-Client als Beankotlin
src/main/kotlin/com/example/teams/tab/config/MsalConfig.kt:
- B5L11-0177 · OBO-Servicekotlin
src/main/kotlin/com/example/teams/tab/auth/OboTokenService.kt:
- B5L11-0188 · Security-Configkotlin
src/main/kotlin/com/example/teams/tab/config/SecurityConfig.kt:
- B5L11-0199 · Profile-Endpointkotlin
src/main/kotlin/com/example/teams/tab/web/MeController.kt:
- B5L11-02011 · Tab-Manifest und App-Bundlejson
manifest.json:
Lesson 12
14 snippets- B5L12-0012 · `TeamsActivityHandler` als Erweiterungkotlin
Du erbst statt von ActivityHandler jetzt von TeamsActivityHandler und überschreibst, was du brauchst. Alle bisherigen Hooks (onMessageActivity, onMembersAdded) funktionieren weiter.
- B5L12-0023 · Search Command — Auftragssuchejson
Die zugehörige Activity sieht so aus:
- B5L12-0033 · Search Command — Auftragssuchekotlin
Im Handler antwortest du mit einer MessagingExtensionResponse:
- B5L12-0044 · Action Command — neue Aufgabe via Task-Modulekotlin
2. composeExtension/submitAction — wenn der User im Task-Module Submit drückt. Bot kann dann mit einer Card antworten, die im Chat landet.
- B5L12-0055 · Link Unfurlingkotlin
Wenn ein User in einem Chat eine URL postet, die zu einer Domain gehört, die im Bot-Manifest als „messageHandlers" → „link" gelistet ist, schickt Teams dem Bot eine composeExtension/queryLink-Activity. Du antwortest mit
- B5L12-0065 · Link Unfurlingjson
Die Domain trägt du im Manifest ein:
- B5L12-0076 · Manifest-Erweiterung für Message Extensionsjson
Du baust auf dem Tab-Manifest aus L11 weiter. Ergänz im manifest.json:
- B5L12-0087 · Zusammenspiel mit Authkotlin
Search- und Action-Commands können SSO nutzen — sehr ähnlich zu Tabs aus L11. Im Handler prüfst du, ob ein Token vorhanden ist; wenn nicht, antwortest du mit type: "auth" und einer Auth-URL, Teams öffnet ein Popup.
- B5L12-0091 · Bot auf `TeamsActivityHandler` umstellenkotlin
ApprovalBot.kt (oder wie deine Klasse aus L10 jetzt heißt) öffnen. Basisklasse austauschen:
- B5L12-0102 · Fake-Services anlegenkotlin
src/main/kotlin/com/example/teams/bot/orders/OrderService.kt:
- B5L12-0112 · Fake-Services anlegenkotlin
TodoService.kt:
- B5L12-0123 · Search-Handler implementierenkotlin
In TeamsBot.kt:
- B5L12-0134 · Action-Handler implementierenkotlin
HeroCard.toAttachment() ist eine SDK-Helper-Methode, die dir das Plumbing fürs Preview-Card-Format spart.
- B5L12-0145 · App-Manifest aktualisierenjson
Das Bot-Manifest aus L09 + L11 ergänzt du. Wenn du noch keins hast (weil du den Bot bisher nur im Emulator getestet hast), bau eins analog zu L11. Wichtig sind die zwei neuen Blöcke:
Lesson 13
14 snippets- B5L13-0023 · OpenAPI 3 aus Spring mit Springdockotlin
Dependency im Gradle Kotlin DSL:
- B5L13-0033.1 · OpenAPI-Basis-Konfigurationkotlin
Default-Konfiguration reicht für ein erstes Connector-Setup, aber für Power Platform lohnt es sich, ein paar Dinge explizit zu machen.
- B5L13-0043.2 · Endpunkte mit ordentlichen Annotationskotlin
Wichtig: Power Platform liest aus dem servers[]-Block den Host für den Connector. Wenn du dort http://localhost:8080 stehen lässt, baut die Power Platform genau das in den Connector hinein — und der ist dann für jeden an
- B5L13-0064.1 · `x-ms-summary` — was End-User im Designer sehenjson
summary aus deiner @Operation-Annotation wird in der OpenAPI-Spec auf das summary-Feld der Operation gemappt. Power Platform zeigt aber im Action-Picker bevorzugt x-ms-summary an. Wenn du beide setzt, gewinnt x-ms-summar
- B5L13-0074.3 · `x-ms-dynamic-values` — Dropdowns aus deiner API füllenjson
Das ist die Extension, die Custom Connectoren wirklich nützlich macht. Statt dass dein End-User einen Status manuell eintippen muss („Hauptsache klein-geschrieben"), kannst du der Power Platform sagen: „Frag meinen API-E
- B5L13-0084.3 · `x-ms-dynamic-values` — Dropdowns aus deiner API füllenkotlin
Im Backend baust du dazu einen Helper-Endpoint, der die erlaubten Werte als Liste ausliefert:
- B5L13-0095.1 · API Keykotlin
Der einfachste Fall. Power Platform fragt den End-User einmal nach einem API-Key (Header oder Query-Param), speichert den verschlüsselt am Connection-Objekt und schickt ihn bei jedem Call mit. Im Backend prüfst du den Ke
- B5L13-0105.1 · API Keyjson
In der OpenAPI-Spec deklarierst du das als securityScheme:
- B5L13-0115.2 · OAuth 2.0 gegen Entra IDjson
Das Setup spiegelt direkt das, was du in B5/L02 und L03 für die App-Registration aufgebaut hast. Die Connection-Konfig:
- B5L13-0162 · Orders-Endpunkte anlegen oder ausbauenkotlin
Wenn du noch keinen OrderController hast, leg einen an. Mindestens diese zwei Endpunkte:
- B5L13-0186 · `x-ms-summary` ergänzenyaml
Such die Stelle listOrders und ergänze x-ms-summary und x-ms-visibility:
- B5L13-0196 · `x-ms-summary` ergänzenyaml
Und am Parameter status:
- B5L13-0207 · Bonus — Dynamic Valueskotlin
Erweiter den OrderController um den Helper-Endpoint für Status-Werte:
- B5L13-0217 · Bonus — Dynamic Valuesyaml
Spec neu exportieren, Connector im UI aktualisieren (oben Update connector wiederholt das Import-Procedere). Dann im Swagger Editor bei status das Dynamic-Values-Pattern ergänzen:
Lesson 14
15 snippets- B5L14-0013 · Polling-Trigger im Backendkotlin
Der Endpoint, den Power Automate pollt, muss zwei Dinge können: er muss die neuen Items seit dem letzten Poll liefern, und er muss einen Marker zurückgeben, den Power Automate beim nächsten Poll wieder mitschickt. Micros
- B5L14-0023 · Polling-Trigger im Backendyaml
Die OpenAPI-Spec ergänzt du mit Microsoft-Trigger-Extensions:
- B5L14-0034.1 · Datenmodell für Subscriptionskotlin
Das Subscribe/Unsubscribe-Pattern ist etwas mehr Arbeit, aber spätestens bei mehr als „alle paar Minuten kommt eine Order" lohnt es sich.
- B5L14-0044.2 · Subscribe-Endpointkotlin
Persistiert in Postgres, einfache Tabelle. Spring Data JDBC reicht.
- B5L14-0054.3 · Unsubscribe-Endpointkotlin
data class SubscribeResponse(val subscriptionId: String)
- B5L14-0064.4 · OpenAPI-Spec — die `x-ms-trigger`-Extensionsyaml
In der Spec deklarierst du die beiden Endpunkte als zusammengehöriges Subscribe-Pair:
- B5L14-0074.5 · Backend ruft die Callback-URLkotlin
Wenn im Backend eine Order erstellt wird, lade alle Subscriptions zum Event-Typ und mach pro Subscription einen HTTP-POST an die Callback-URL.
- B5L14-0085 · Flow-Anatomietext
Aus Sicht eines End-Users, der den Designer öffnet, sieht ein typischer Flow so aus:
- B5L14-0101 · Subscription-Persistenz im Backendsql
Leg eine neue Tabelle webhooksubscriptions an. Flyway-Migration:
- B5L14-0111 · Subscription-Persistenz im Backendkotlin
Dazu Entity und Repository als Spring Data JDBC:
- B5L14-0121 · Subscription-Persistenz im Backendkotlin
@Column("createdat") val createdAt: Instant
- B5L14-0132 · Subscribe/Unsubscribe-Controllerkotlin
fun findByEventType(eventType: String): List<Subscription>
- B5L14-0143 · Dispatcher und Trigger im Order-Servicekotlin
data class SubscribeResponse(val subscriptionId: String)
- B5L14-0154 · Simulator-Endpointkotlin
Damit du Orders erzeugen kannst, ohne in die Datenbank zu schreiben:
- B5L14-0175 · Spec exportieren und Connector aktualisierenyaml
Im Power-Automate-Web-UI gehst du in den vorhandenen Orders API (Local Dev)-Connector, klickst Update connector → Import from OpenAPI file, lädst die neue Spec hoch und ergänzt im Definition-Tab → Swagger-Editor die Trig
Lesson 15
4 snippets- B5L15-0074.4 · Button — neue Order anlegenexcel
Auf einem Button im Detail- oder Listen-Screen:
- B5L15-0121 · Backend um `createOrder` und Pagination ergänzenkotlin
Falls noch nicht da, leg den POST-Endpoint und einen paginiert antwortenden GET-Endpoint an:
- B5L15-0205 · FormScreen — neue Order anlegenexcel
Neuer Screen FormScreen, Blank.
- B5L15-0249 · Bonus 2 — Authenticated Identity beobachtenkotlin
Falls dein Connector OAuth gegen Entra ID nutzt, leg im Backend einen Endpoint an, der den Authentifizierungs-Kontext zurückgibt:
Lesson 16
19 snippets- B5L16-0016 · Bicep statt ARM-Templatesbicep
Ein minimales Bicep-File für eine Container App sieht so aus:
- B5L16-0027 · azd — die End-to-End-Pipelineyaml
Eine azure.yaml für unser Projekt:
- B5L16-0038.2 · Graph-Zugriff über Managed Identitybicep
Variante zwei läuft so: in deinem Bicep weist du der Managed Identity die Graph-App-Permissions direkt zu. Das geht über Microsoft.Graph/appRoleAssignments. Stand Mai 2026 ist das eine eigene Resource-Provider-Welt — bis
- B5L16-0048.2 · Graph-Zugriff über Managed Identitykotlin
In der App liest du dann das Token einfach so:
- B5L16-0069.2 · Eigenes Multi-Stage-Dockerfiledockerfile
Für volle Kontrolle ein klassisches Multi-Stage Dockerfile:
- B5L16-00810 · Key Vault statt Env-Varsyaml
implementation("com.azure.spring:spring-cloud-azure-starter-keyvault-secrets:5.18.0")
- B5L16-009application-prod.ymlbicep
Die Container App braucht eine Rollenzuweisung — Key Vault Secrets User auf den Vault — damit ihre Managed Identity die Secrets lesen darf. Das machst du wieder in Bicep:
- B5L16-01011 · Logging und Monitoringbicep
In der Container App reicht es, drei Env-Vars zu setzen:
- B5L16-01212 · GitHub Actions Pipelineyaml
Sobald azd up lokal läuft, ist die CI-Pipeline trivial. azure/login mit OIDC-Federated-Identity (kein Client Secret in den GitHub Secrets), dann azd up:
- B5L16-0131 · Tooling-Setupbash
Zuerst die zwei Azure-Tools installieren, falls noch nicht da:
- B5L16-0173 · Dockerfile schreibendockerfile
Im Projekt-Root ein Multi-Stage Dockerfile anlegen:
- B5L16-018Dockerfilebash
Lokal bauen und kurz prüfen, dass das Image hochkommt:
- B5L16-0204 · azd-Skelett anlegenyaml
Überschreib die azure.yaml mit:
- B5L16-0215 · Bicep-Templatebicep
Ersetz den Inhalt von infra/main.bicep mit:
- B5L16-0225 · Bicep-Templatebicep
Und parallel infra/resources.bicep:
- B5L16-0236 · Parameter-Filejson
infra/main.parameters.json:
- B5L16-02911 · Bonus — Managed Identity statt Client Secretkotlin
In deinem GraphConfig.kt ein zweites Bean für Production, das ManagedIdentityCredentialBuilder nutzt:
- B5L16-03011 · Bonus — Managed Identity statt Client Secretpowershell
4. „Permissions" → das geht nicht direkt im Portal, muss über PowerShell laufen:
- B5L16-03112 · Bonus — GitHub Actionsbash
Wenn du das Projekt in ein GitHub-Repo committest, kannst du die Pipeline aus §12 der Lektion übernehmen. Vorher musst du die Federated-Identity einrichten:
Lesson 17
12 snippets- B5L17-0013 · JSON-RPC 2.0 als Wire-Formatjson
Ein typischer Request sieht so aus:
- B5L17-0023 · JSON-RPC 2.0 als Wire-Formatjson
"arguments": { "top": 10 }
- B5L17-0033 · JSON-RPC 2.0 als Wire-Formatjson
"isError": false
- B5L17-0045.1 · Toolsjson
Beispiel-Eintrag in tools/list:
- B5L17-0055.3 · Promptsjson
Beispiel — ein O365-Server könnte einen Prompt mailsummarydaily haben:
- B5L17-0076 · Capabilities und Handshakejson
Beim Verbindungsaufbau machen Client und Server einen Capability-Exchange. Der Client sagt: „Ich verstehe Tools, Resources und Prompts." Der Server sagt: „Ich liefere Tools (mit Change-Notifications) und Resources, aber
- B5L17-00810 · Claude-Desktop-Konfigurationjson
Auf macOS liegt die Config unter ~/Library/Application Support/Claude/claudedesktopconfig.json. Sie sieht so aus:
- B5L17-0113 · Tools-Liste anschauenjson
Klick auf eines der Tools, z.B. listdirectory. Du siehst das Input-Schema:
- B5L17-0124 · Tool aufrufenjson
Klick „Call Tool". Du bekommst eine Response:
- B5L17-0135 · Eine Resource lesenjson
Wenn der Server sie zurückgibt, siehst du den Inhalt im Response-Bereich. Format:
- B5L17-0156 · Server in Claude Desktop konfigurierenjson
touch "$HOME/Library/Application Support/Claude/claudedesktopconfig.json"
- B5L17-0168 · Ein zweiter Server — Postgresjson
In claudedesktopconfig.json ergänzen:
Lesson 18
23 snippets- B5L18-0012 · Projekt-Setupkotlin
Ein neues Gradle-Projekt mit Kotlin und Spring Boot 4.0.6:
- B5L18-0022.1 · `application.yml`yaml
Wichtige Punkte:
- B5L18-0032.2 · Main-Classkotlin
2. Logging auf File, nicht auf Console — wenn Logback auf stdout schreibt, mischt es Log-Zeilen mit JSON-RPC-Frames, und der MCP-Client crasht. Datei-Output oder stderr-Output sind die zwei sinnvollen Wege.
- B5L18-0043 · Erstes Tool — Echokotlin
Das einfachste mögliche Tool: kriegt einen String, gibt ihn zurück. Nutzlos in der Sache, aber perfekt zum Aufzeigen der Mechanik.
- B5L18-0053 · Erstes Tool — Echokotlin
Damit der Starter die Tools überhaupt registriert, brauchst du noch eine Configuration-Klasse, die die Tool-Liste bekanntmacht:
- B5L18-007Output: build/libs/mcp-server-demo.jarjson
Nach „Connect" siehst du:
- B5L18-0085 · Zweites Tool — strukturierter Outputkotlin
Echo ist langweilig. Jetzt ein Tool, das eine Datenklasse zurückgibt — damit du siehst, wie Spring AI die Serialisierung handhabt.
- B5L18-0095 · Zweites Tool — strukturierter Outputkotlin
In ToolsConfiguration einbinden:
- B5L18-0105 · Zweites Tool — strukturierter Outputjson
Spring AI generiert für die CurrentTime-Klasse automatisch ein JSON-Schema. Im Inspector siehst du im Output-Bereich:
- B5L18-0116 · Drittes Tool — Schema-Validationkotlin
Jetzt eins, das Constraints prüft. Ein RandomNumber-Tool mit Min/Max-Input.
- B5L18-0138 · Im Claude Desktop einbindenjson
In ~/Library/Application Support/Claude/claudedesktopconfig.json:
- B5L18-0149 · HTTP-Transport für deployed Serveryaml
In der application.yml:
- B5L18-016Optional: Auth-Konfiguration über Spring Security danebenjson
Im Client (z.B. Claude Desktop) konfigurierst du den Server jetzt mit url statt command:
- B5L18-01710 · Testingkotlin
Der Spring-AI-Starter liefert spring-ai-mcp-test als Testing-Helper. Damit kannst du Tool-Calls direkt unit-testen, ohne dass du den ganzen stdio-Lifecycle aufziehen musst.
- B5L18-01810 · Testingkotlin
Für Integration-Testing gegen den vollen MCP-Stack gibt es McpTestServerExtension:
- B5L18-0192 · `build.gradle.kts` anpassenkotlin
Öffne die build.gradle.kts und ergänze die Spring-AI-Dependency:
- B5L18-0203 · `application.yml`yaml
In src/main/resources/application.yml:
- B5L18-0214 · Tool 1 — Echokotlin
src/main/kotlin/com/example/mcp/EchoTool.kt:
- B5L18-0225 · Tool 2 — CurrentTimekotlin
src/main/kotlin/com/example/mcp/TimeTool.kt:
- B5L18-0236 · Tool 3 — RandomNumberkotlin
src/main/kotlin/com/example/mcp/RandomTool.kt:
- B5L18-0247 · ToolCallbackProvider-Beankotlin
src/main/kotlin/com/example/mcp/ToolsConfiguration.kt:
- B5L18-02910 · In Claude Desktop einbindenjson
~/Library/Application Support/Claude/claudedesktopconfig.json ergänzen:
- B5L18-03012 · Bonus — Unit-Testskotlin
src/test/kotlin/com/example/mcp/RandomToolTest.kt:
Lesson 19
35 snippets- B5L19-001Architektur — Überblicktext
Das ist die Form, in der ein Kunden-MCP-Server in der M365-Welt heute aussieht. Nicht als lokaler Spielzeug-Server auf der Entwickler-Maschine, sondern als deploybarer Service, den ein KI-Agent jederzeit erreicht.
- B5L19-002Projekt-Setuptext
Frisches Repo o365-mcp-server. Layout:
- B5L19-003`build.gradle.kts`kotlin
└── SecurityConfig.kt
- B5L19-004`application.yml`yaml
Worauf zu achten ist:
- B5L19-005`GraphConfig.kt`kotlin
Aus L04 hast du den GraphService mit getMe(), getMessages(), etc. Hier ziehen wir den weiter aus und ergänzen die Teams-Calls.
- B5L19-006`GraphService.kt`kotlin
Zwei Profile, ein Bean. Lokal mit Client Secret, in Azure mit Managed Identity. Das ist exakt das Muster aus L16.
- B5L19-007MailTools.ktkotlin
Das ist viel Code. Beim Lesen fokussier auf die fünf Methoden, die wir gleich als Tools exponieren — die Mechanik kennst du im Detail aus L05 (Mail/Kalender) und L09 (Teams).
- B5L19-008`CallerResolver.kt`kotlin
3. Konversion HTML → Text ist explizit gemacht. bodyText brauchen LLMs, weil HTML im Token-Kontext teuer ist und mit Tags zugemüllt. Wer den Originalinhalt braucht, holt bodyHtml.
- B5L19-009CalendarTools.ktkotlin
block() ist hier okay, weil wir aus einer Tool-Methode kommen, die im Request-Scope läuft und der Reactor-Pipeline ihre Zeit gibt. Für reinste Reactor-Architektur könnten wir das in einen Mono<String>-Return umbauen — Sp
- B5L19-010TeamsTools.ktkotlin
.mapNotNull { a -> a.attendee?.emailAddress?.address?.let { "$it: ${a.availability}" } },
- B5L19-011Tool-Registrationkotlin
src/main/kotlin/com/example/o365mcp/ToolsConfiguration.kt:
- B5L19-012Resourceskotlin
Tools sind das Wichtige, Resources das Bonus-Feature. Drei lesbare URIs:
- B5L19-013Security — Resource Server gegen Entra IDkotlin
Resources sind selten der Kostentreiber. Was den LLM weiterbringt, sind die Tools — Resources sind nice-to-have, wenn der LLM auf eine spezifische URI verweisen will, ohne die Daten in den Tool-Output zu kopieren.
- B5L19-014`infra/main.bicep`bicep
Größtenteils wie in L16. Hier nur die Erweiterungen, das Skelett kennst du.
- B5L19-015`infra/graph-permissions.bicep`bicep
output AZURETENANTID string = tenantId
- B5L19-016Deploymentyaml
Der issuer-uri und audiences in der application.yml referenzieren genau diese App-Reg.
- B5L19-017Deploymentdockerfile
Dockerfile analog zu L16:
- B5L19-020Tests gegen den deployed Servertext
Ein gültiges Token holen — als User (du selbst), nicht als Service Principal. Im Browser:
- B5L19-021Tests gegen den deployed Serverbash
Quick-Test mit curl und einem bereits gezogenen Token:
- B5L19-022Claude Desktop einbindenjson
Du solltest deine sechs Tools im Output sehen.
- B5L19-023Cursor einbindenjson
Cursor liest die MCP-Konfig aus ~/.cursor/mcp.json (User-global) oder .cursor/mcp.json (pro Projekt):
- B5L19-025Sicherheit — wer darf was?kotlin
Audit-Logging. Jeder Tool-Call sollte mit User-UPN und Tool-Name geloggt werden. Application Insights kriegt das automatisch via OTel, aber zusätzlich noch eine eigene Logger-Zeile für den Audit-Trail:
- B5L19-0272 · Build-File ergänzenkotlin
Übernimm das build.gradle.kts aus der Lektion. Zusätzliche Punkte zur pom.xml-äquivalenten Liste:
- B5L19-0283 · App-Reg in Entra ID anlegenbash
App-Reg #1 — o365-mcp-server (die geschützte API):
- B5L19-0314 · application.ymlyaml
echo "MCPSERVERAPPID=$APPID"
- B5L19-0335 · GraphConfig, GraphServicekotlin
Eine zusätzliche Methode für die Resource-Contacts:
- B5L19-0346 · Pflicht-Tools (mindestens vier)kotlin
ToolsConfiguration.kt:
- B5L19-0359 · Lokal testen — ohne Authkotlin
Für den ersten lokalen Lauf schalten wir die OAuth-Validation vorübergehend aus, um die Tools mit dem MCP-Inspector zu testen. In SecurityConfig.kt temporär:
- B5L19-03710 · Dockerfilebash
Übernimm das Dockerfile aus der Lektion. Lokal bauen:
- B5L19-03912 · `azd up`bash
Beachte: Kein MSALCLIENTSECRET in der prod-Variante. Managed Identity macht das.
- B5L19-04113 · Graph-Permissions verifizierenbash
Test mit curl. Du brauchst einen User-Token; einfachster Weg:
- B5L19-04314 · Claude Desktop einbindenjson
~/Library/Application Support/Claude/claudedesktopconfig.json:
- B5L19-04416 · `notes.md` schreibenmarkdown
Im Repo-Root eine notes.md anlegen mit:
- B5L19-04517 · Bonus — `list_teams_channels` und `find_meeting_time`kotlin
Wenn dir Zeit bleibt: die zwei zusätzlichen Tools.
- B5L19-04618 · Bonus — Tool-Level-Authorizationkotlin
Trenn mcp.read und mcp.write in der App-Reg-Scope-Liste auf. In Claude Desktop beim ersten Login consent für beide. In der SecurityConfig:
Book 6 — Kotlin for AI Agents
283 snippetsLesson 1
2 snippetsLesson 2
15 snippets- B6L02-0012 · Spring AI 1.1kotlin
Ein minimales Beispiel in Kotlin:
- B6L02-0023 · LangChain4j 1.xkotlin
Ein minimales Beispiel:
- B6L02-0033 · LangChain4j 1.xkotlin
Das AiServices-Pattern wird interessanter, sobald du strukturierte Workflows hast:
- B6L02-0044 · Provider-SDK direktkotlin
OpenAI-SDK direkt:
- B6L02-0065.1 · Spring AIyaml
implementation("org.springframework.ai:spring-ai-starter-model-openai")
- B6L02-007application.ymlkotlin
temperature: 0.0
- B6L02-0095.2 · LangChain4jkotlin
implementation("dev.langchain4j:langchain4j-open-ai:1.0.1")
- B6L02-0101 · Spring-Boot-4-Projekt anlegenkotlin
Im build.gradle.kts ergänze die zwei Framework-Dependencies:
- B6L02-0132 · API-Key konfigurierenyaml
Den Key niemals in application.yml hartkodieren und ins Git committen. In src/main/resources/application.yml ergänzen:
- B6L02-0143 · Klassifikator in Spring AIkotlin
Leg an: src/main/kotlin/com/example/b6frameworks/springai/EmailClassifierSpringAi.kt
- B6L02-0154 · Klassifikator in LangChain4jkotlin
Leg an: src/main/kotlin/com/example/b6frameworks/langchain4j/EmailClassifierLc4j.kt
- B6L02-0165 · Endpoint zum Vergleichkotlin
Leg an: src/main/kotlin/com/example/b6frameworks/ClassificationController.kt
- B6L02-0176 · Testenbash
In einem zweiten Terminal:
- B6L02-0187 · Vergleichs-Notes schreibenmarkdown
Leg im Projekt-Root notes-frameworks.md an. Trag dort ein:
- B6L02-0198 · Bonus — Test mit MockKkotlin
Schreib einen Test für EmailClassifierLc4j, in dem du das ChatLanguageModel mockst. In src/test/kotlin/com/example/b6frameworks/langchain4j/EmailClassifierLc4jTest.kt:
Lesson 3
16 snippets- B6L03-0012 · Spring-AI-Starter für drei Providerkotlin
Im build.gradle.kts ziehst du drei Starter parallel:
- B6L03-0022 · Spring-AI-Starter für drei Provideryaml
In application.yml:
- B6L03-0033 · Drei `ChatClient`-Beans mit `@Qualifier`kotlin
Die Auto-Configuration baut dir OpenAiChatModel, AnthropicChatModel und OllamaChatModel als drei separate Beans. Was du noch brauchst: drei ChatClient-Beans, einer pro Provider.
- B6L03-0043 · Drei `ChatClient`-Beans mit `@Qualifier`kotlin
In einem Service:
- B6L03-0085 · Ollama lokal hochfahrenyaml
Variante 2 — Docker. Sauber containerisiert, läuft auch auf einer Linux-VM, mountet das Modell-Verzeichnis als Volume:
- B6L03-0117 · Standard-Modell vs explizites Modell pro Aufrufkotlin
In der application.yml setzt du ein Default-Modell. Pro Aufruf kannst du das überschreiben:
- B6L03-0128 · Konfiguration für Testskotlin
spring-ai-test-Starter. Spring AI bringt einen Test-Starter mit, der einen Stub-ChatModel als Bean bereitstellt. Du konfigurierst die erwarteten Antworten pro Test:
- B6L03-0131 · Projekt erweitern oder neukotlin
Im build.gradle.kts die drei Spring-AI-Starter ergänzen:
- B6L03-0142 · Ollama via Docker hochfahrenyaml
Im Projekt-Root lege docker-compose.yml an:
- B6L03-0214 · `application.yml` für drei Provideryaml
In src/main/resources/application.yml:
- B6L03-0225 · Drei `ChatClient`-Beanskotlin
src/main/kotlin/com/example/b6providers/config/ChatClientConfig.kt:
- B6L03-0236 · Service und Endpointkotlin
src/main/kotlin/com/example/b6providers/MultiProviderChatService.kt:
- B6L03-0246 · Service und Endpointkotlin
src/main/kotlin/com/example/b6providers/ChatController.kt:
- B6L03-0257 · Drei Provider mit derselben Frage testenbash
In einem zweiten Terminal jeweils dieselbe Frage:
- B6L03-0267 · Drei Provider mit derselben Frage testenmarkdown
Notiere die drei Antworten in notes-provider-vergleich.md:
- B6L03-0278 · Bonus — Mock-Provider für Testskotlin
src/test/kotlin/com/example/b6providers/MultiProviderChatServiceTest.kt:
Lesson 4
17 snippets- B6L04-0021 · System- und User-Message — wer macht waskotlin
Eine schlechte Aufteilung wäre, die Regeln in der User-Message zu wiederholen:
- B6L04-0032 · Prompt-Templates mit Platzhalternkotlin
Wenn deine User-Message eine Vorlage mit eingesetzten Variablen ist, nutzt du PromptTemplate:
- B6L04-0042 · Prompt-Templates mit Platzhalternkotlin
Templates kannst du als Klassen-Konstanten halten oder, wenn sie länger werden, in src/main/resources/prompts/.st-Dateien auslagern und über @Value-Resource-Loading injizieren:
- B6L04-0063 · Few-Shot-Promptingtext
Few-Shot — zwei, drei Beispiele:
- B6L04-0074 · Output-Parsing — vom Text zur Datenklassekotlin
Spring AI's BeanOutputConverter<T> macht alle drei Schritte automatisch.
- B6L04-0084 · Output-Parsing — vom Text zur Datenklassetext
converter.format produziert intern eine Anweisung wie:
- B6L04-0115 · Output-Stabilität — was wirklich wirktyaml
Hebel 1 — temperature=0.0. Setzt die Sampling-Strategie auf „immer den wahrscheinlichsten Token", reduziert Variabilität. Macht das LLM nicht deterministisch (verschiedene Modell-Versionen, GPU-Nichtdeterminismus), aber
- B6L04-0127 · LangChain4j-Pendantkotlin
Damit du den Vergleich hast: LangChain4j macht dasselbe über AiServices-Interfaces, die als Return-Type direkt eine Kotlin-Datenklasse haben.
- B6L04-0131 · Datenklasse für strukturiertes Ergebniskotlin
In src/main/kotlin/com/example/b6providers/classifier/EmailCategory.kt:
- B6L04-0142 · Prompt als Resource auslagerntext
In src/main/resources/prompts/email-classifier-system.st:
- B6L04-0153 · Service mit `BeanOutputConverter`kotlin
In src/main/kotlin/com/example/b6providers/classifier/EmailClassifierService.kt:
- B6L04-0164 · REST-Endpointkotlin
In src/main/kotlin/com/example/b6providers/classifier/EmailClassifierController.kt:
- B6L04-0175 · Mit echten Mails testenbash
Such dir fünf E-Mails aus deiner Inbox raus oder erfinde realistische Beispiele. Schick jede an den Endpoint:
- B6L04-0185 · Mit echten Mails testenmarkdown
Mach das für alle fünf Mails. Schreib die Ergebnisse in ~/kotlin-sandbox/buch-6/b6-providers/notes-classifier.md:
- B6L04-0196 · MockK-Test für den Servicekotlin
In src/test/kotlin/com/example/b6providers/classifier/EmailClassifierServiceTest.kt:
- B6L04-0207 · Bonus — Few-Shot-Variante zum Vergleichtext
Kopiere die Prompt-Datei zu src/main/resources/prompts/email-classifier-system-fewshot.st und ergänze Beispiele:
- B6L04-0217 · Bonus — Few-Shot-Variante zum Vergleichmarkdown
Schick dieselben fünf Mails jetzt durch beide Varianten und ergänze in notes-classifier.md:
Lesson 5
12 snippets- B6L05-0012 · `BeanOutputConverter<T>` als Standard-Werkzeugkotlin
Im Code sieht das so aus:
- B6L05-0023 · OpenAIs native Structured Outputskotlin
Spring AI 1.1 nutzt das automatisch, wenn du OpenAiChatOptions.responseFormat(...) setzt:
- B6L05-0034 · Verschachtelte Strukturen, Listen, Enumskotlin
Komplexere Datenklassen funktionieren ohne Sondermaßnahmen, solange Jackson sie serialisieren kann:
- B6L05-0044 · Verschachtelte Strukturen, Listen, Enumskotlin
Eine Map<String, Any> taugt nicht — Schema-Generatoren mögen keine Any, und das LLM hat ohnehin keine Chance zu wissen, was darin landen soll. Wenn du wirklich variable Strukturen brauchst, modelliere sie als Sealed-Clas
- B6L05-0055 · Bean Validation auf der LLM-Antwortkotlin
Spring Boot 4 nutzt Jakarta Bean Validation (jakarta.validation.constraints.). Auf Kotlin-Datenklassen ist die Syntax wegen der Parameter-Annotations etwas hakelig — du brauchst den @field:-Use-Site-Target:
- B6L05-0065 · Bean Validation auf der LLM-Antwortkotlin
Im Service kommt nach dem Parsing der Validator dazu:
- B6L05-0071 · Datenklassen anlegenkotlin
Im Package com.example.ai.invoice (oder wo bei dir die Spring-AI-Services wohnen) leg an:
- B6L05-0082 · `InvoiceExtractor`-Service bauenkotlin
Achte darauf, dass keine Default-Werte auf den Pflichtfeldern liegen. String? = null ist okay für echte Optional-Felder, aber currency: String = "EUR" macht den Schema-Generator hier weich.
- B6L05-0093 · Drei Demo-E-Mails durchschickentext
Beispiel für mail-01-deutsch.txt:
- B6L05-0103 · Drei Demo-E-Mails durchschickenkotlin
Schreib einen Integration-Test, der alle drei Mails durchläuft und das Ergebnis ausgibt:
- B6L05-0114 · Output-Stabilität messenkotlin
Lass denselben Test mit der ersten E-Mail fünfmal hintereinander laufen (in einer Schleife im Test oder via @RepeatedTest(5)):
- B6L05-0125 · Bonus: OpenAI Strict-Mode aktivierenkotlin
Bau eine zweite Variante des Extractors (InvoiceExtractorStrict), die OpenAiChatOptions mit responseFormat(JSONSCHEMA, strict = true) setzt. Spring AI 1.1 hat in BeanOutputConverter eine jsonSchema-Property (oder getJson
Lesson 6
11 snippets- B6L06-0012.1 · `@Tool`-Annotation auf Methoden einer Spring-Beankotlin
Der direkteste Weg. Du baust eine @Component, annotierst Methoden mit @Tool, und gibst die Bean dem ChatClient:
- B6L06-0022.1 · `@Tool`-Annotation auf Methoden einer Spring-Beankotlin
Im Service hängst du das Tool an den ChatClient-Call:
- B6L06-0032.2 · Function-Beans (`Function<I, O>` / `BiFunction<I, O, ?>`)kotlin
Wer keine Annotation-Magie mag, kann Tools als reguläre Spring-Beans definieren:
- B6L06-0052.3 · `FunctionCallback` als Low-Levelkotlin
Wenn du dynamisch Tools registrieren willst — z. B. weil sie aus einem MCP-Server kommen (Lektion 14) — gibt es FunctionCallback:
- B6L06-0063 · Tool-Description ist der wichtigste Code in diesem Setupkotlin
Beispiel für eine schlechte vs. eine brauchbare Description:
- B6L06-0087 · LangChain4j im Vergleichkotlin
LangChain4j macht das anders, aber nicht besser oder schlechter. Du definierst ein Interface:
- B6L06-0091 · WeatherTool mit OpenMeteokotlin
Leg WeatherTool.kt an. Statt Mock-Daten nutze die kostenfreie OpenMeteo-API (kein API-Key nötig). Spring Boots RestClient reicht:
- B6L06-0102 · CalculatorTool mit drei Operationenkotlin
data class CurrentWeather(val temperature2m: Double, val weathercode: Int)
- B6L06-0113 · AssistantService mit beiden Toolskotlin
return a / b
- B6L06-0124 · Multi-Step-Frage stellenkotlin
Schreib einen Integration-Test, der die kombinierte Frage stellt:
- B6L06-0146 · Bonus: Side-Effect-Tool mit Confirm-Patternkotlin
Bau ein drittes Tool, AppointmentTool, mit zwei Methoden:
Lesson 7
13 snippets- B6L07-0012 · Spring AIs Streaming-APIkotlin
ChatClient.prompt()...call() gibt dir die ganze Antwort am Stück. ChatClient.prompt()...stream() gibt dir einen Reactor-Flux, der Tokens (oder Token-Gruppen) liefert, sobald sie vom Provider kommen:
- B6L07-0033 · Coroutines-Bridge: `Flux<T>` als `Flow<T>`kotlin
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor:1.10.1")
- B6L07-0043 · Coroutines-Bridge: `Flux<T>` als `Flow<T>`kotlin
Im Service-Layer kannst du jetzt mit Flow arbeiten:
- B6L07-0064 · Spring MVC SSE-Endpointkotlin
Spring MVC kann SSE seit 4.2 ohne WebFlux. Der Trick: gib Flux<T> oder SseEmitter zurück, setze produces = MediaType.TEXTEVENTSTREAMVALUE. Spring MVC hostet den Stream über Servlet-3.1-Async-Mechanik:
- B6L07-007SSE-Wire-Formattext
Wenn du den Endpoint mit curl -N aufrufst, siehst du den rohen SSE-Stream:
- B6L07-0085 · Spring WebFlux SSE-Endpointkotlin
WebFlux ist reactor-nativ — der Code sieht praktisch identisch aus, nur dass du in einer komplett reaktiven Umgebung sitzt. Wenn dein Projekt sowieso WebFlux nutzt (z. B. weil du viele Streaming-Endpoints hast), brauchst
- B6L07-0096 · Coroutines-Variante in WebFluxkotlin
Spring WebFlux unterstützt Kotlin-Coroutines nativ. Du kannst statt Flux<T> direkt Flow<T> returnen:
- B6L07-0108 · Client-Side: `EventSource` im Browserhtml
Der Browser hat die EventSource-API für SSE seit zehn Jahren:
- B6L07-0111 · Streaming-Endpoint in Spring MVCkotlin
Leg ChatStreamController.kt an:
- B6L07-0132 · Mit curl testentext
Erwartung: Du siehst Stück für Stück Zeilen wie
- B6L07-0143 · HTML-Mini-Client mit EventSourcehtml
Leg src/main/resources/static/chat.html an:
- B6L07-0155 · Bonus: Coroutines-Variante in einem zweiten Endpointkotlin
Wenn dein Projekt WebFlux nutzt (oder du es testweise aktivierst), baue einen zweiten Endpoint, der Flow<String> zurückgibt:
- B6L07-0166 · Bonus: Mit fetch + ReadableStream statt EventSource (für authentifizierte Streams)javascript
Bau die HTML-Datei alternativ um, sodass sie fetch statt EventSource nutzt — damit du HTTP-Header (z. B. Authorization) mitschicken könntest:
Lesson 8
26 snippets- B6L08-0012 · Token-Usage aus `ChatResponse.metadata`kotlin
Spring AIs ChatClient.call() gibt dir den finalen Text mit .content(). Wenn du stattdessen .chatResponse() aufrufst, bekommst du ein ChatResponse-Objekt mit Metadaten:
- B6L08-0022 · Token-Usage aus `ChatResponse.metadata`kotlin
Wichtig: bei Streaming-Responses kommt die Usage-Info nicht in jedem Chunk, sondern oft erst im letzten ChatResponse des Streams. Mit .stream().chatResponse() musst du die Usage am Stream-Ende aufsammeln:
- B6L08-0034 · Cost-Calculator pro Provider und Modellyaml
Die exakten Werte ändern sich quartalsweise — die Konfig gehört in application.yml, nicht in den Code. Eine Beispiel-Konfiguration:
- B6L08-004Preise in EUR pro 1 Mio Tokenskotlin
Und die zugehörige @ConfigurationProperties-Klasse:
- B6L08-005Preise in EUR pro 1 Mio Tokensyaml
Die YAML-Map-Verschachtelung ist hier ein Sonderfall — Map<String, Map<String, ModelPricing>> mit Spring-Konventionen funktioniert in Boot 4 mit Kotlin durch den @ConfigurationPropertiesScan und einem leeren Default-Wert
- B6L08-0065 · Cost-Tracking-Service mit Async-Schreibensql
Erst das Datenbank-Schema (Flyway-Migration):
- B6L08-0075 · Cost-Tracking-Service mit Async-Schreibenkotlin
Dann das Event und die JDBC-Repository-Schicht:
- B6L08-0086 · Spring AI Observation-API mit Micrometeryaml
In application.yml:
- B6L08-0096 · Spring AI Observation-API mit Micrometerkotlin
Custom-Tags pro User/Tenant hängst du über eine ObservationCustomizer-Bean rein:
- B6L08-0107 · Rate-Limiting mit Resilience4jyaml
Resilience4j hat einen Spring-Boot-Starter, der pro Bean-Name einen RateLimiter registriert. In application.yml:
- B6L08-0117 · Rate-Limiting mit Resilience4jkotlin
In der Service-Methode:
- B6L08-0127 · Rate-Limiting mit Resilience4jkotlin
Das User-spezifische Rate-Limiting per @RateLimiter ist hier in der einfachen Variante global — alle User teilen denselben Resilience4j-Bucket. Wer pro User einen eigenen Bucket will, baut das mit RateLimiterRegistry.rat
- B6L08-0138 · Daily Budget Capskotlin
Rate-Limiting bremst pro Minute. Daily-Budget-Caps schauen auf Tagessumme in Euro. Pattern:
- B6L08-0148 · Daily Budget Capskotlin
Im @ControllerAdvice wird daraus HTTP 402 Payment Required:
- B6L08-0159 · Retry-After-RateLimit beim Providerkotlin
Provider werfen HTTP 429, wenn du ihre Rate-Limits reißt. Spring AI macht automatisches Retry mit exponentiellem Backoff über RetryUtils (default: 3 Versuche, 2s Initial-Delay, 1.5x Multiplikator). Konfigurierbar pro Mod
- B6L08-0169 · Retry-After-RateLimit beim Providerkotlin
Und bei der OpenAiChatModel-Bean-Konfiguration einhängen:
- B6L08-0171 · Postgres-Setup und Schemakotlin
build.gradle.kts — falls noch nicht geschehen:
- B6L08-0191 · Postgres-Setup und Schemayaml
application.yml:
- B6L08-0201 · Postgres-Setup und Schemasql
Flyway-Migration src/main/resources/db/migration/V1createaicalls.sql:
- B6L08-0212 · Pricing-Konfigurationyaml
application.yml ergänzen:
- B6L08-0223 · Cost-Tracking-Servicekotlin
Test-Snippet:
- B6L08-0234 · Chat-Endpoint mit User-Rate-Limiterkotlin
.isEqualByComparingTo(BigDecimal("0.015")) // 1500/1M5 + 500/1M15 = 0.0075 + 0.0075 = 0.015
- B6L08-0244 · Chat-Endpoint mit User-Rate-Limiterkotlin
RateLimitedChatService.kt aus der Lektion. Wichtig — User-spezifisches Rate-Limiting über RateLimiterRegistry.rateLimiter("user-$userId"), nicht die globale @RateLimiter-Annotation. Sonst teilen sich alle User denselben
- B6L08-0255 · `/api/me/usage`-Endpointkotlin
throw RateLimitExceededException("User $userId hat das Minuten-Limit erreicht")
- B6L08-0266 · Integration-Test über alleskotlin
totalCalls = (row["todaycalls"] as Long).toInt(),
- B6L08-0277 · Bonus: Grafana-Dashboardyaml
Lokales Docker-Setup mit Prometheus und Grafana:
Lesson 9
9 snippets- B6L09-0024 · Spring AI: `EmbeddingModel` und parallele Provideryaml
application.yml:
- B6L09-0034 · Spring AI: `EmbeddingModel` und parallele Providerkotlin
Wenn beide Starter im Classpath sind, registriert Spring AI zwei EmbeddingModel-Beans. Damit du auflösen kannst, welches du willst, gibst du im Code den Bean-Namen via @Qualifier an:
- B6L09-0045 · Batching: der Hebel mit dem größten Effektkotlin
Praktisches Pattern für eine Index-Operation:
- B6L09-0061 · Projekt vorbereitenkotlin
Wenn du das B6-Sandbox-Projekt aus L03 (~/kotlin-sandbox/buch-6/spring-ai-playground/) noch nicht hast, leg eins an — ein Spring-Boot-4.0-Projekt mit Kotlin, Gradle KTS, spring-boot-starter-web. Ergänze die Embedding-Sta
- B6L09-0071 · Projekt vorbereitenyaml
In application.yml:
- B6L09-0082 · `SimilarityService` bauenkotlin
Leg SimilarityService.kt an:
- B6L09-0093 · Distanzmatrix für 10 Sätze ausgebenkotlin
Bau einen CommandLineRunner oder besser einen kleinen REST-Endpoint, der für eine feste Satz-Liste eine Distanzmatrix berechnet:
- B6L09-0105 · Kosten messenmarkdown
Leg eine Datei notes.md im Projekt-Root an, mit Tabelle:
- B6L09-0116 · Bonus: `text-embedding-3-large` mit Truncationkotlin
Ergänze einen dritten EmbeddingModel-Bean (manuell, da Auto-Config nur ein OpenAI-Embedding-Model registriert) mit Modell text-embedding-3-large und dimensions=1024. Vergleich: liefert die Reihenfolge bessere Treffer als
Lesson 10
15 snippets- B6L10-0024 · Spring AI's `VectorStore`-Interfacekotlin
Spring AI bettet beim add(...) automatisch ein, wenn das Document noch kein Embedding hat. Das hält den Anwendungs-Code schlank:
- B6L10-0034 · Spring AI's `VectorStore`-Interfacekotlin
vectorStore.add(docs)
- B6L10-0045.1 · Schema und Indexsql
Mit Auto-Configure macht das Spring AI für dich, aber ein Blick auf das, was passiert, hilft:
- B6L10-0096.1 · Setupyaml
bit-Vektoren mit Hamming-Distance kommen eher in Hybrid-Pipelines vor (Pre-Filter auf 1000 Kandidaten, dann Rerank mit Original-Vektor). Spring AI 1.1 hat noch keine direkte Hilfe dafür, aber pgvector unterstützt es.
- B6L10-010docker-compose.ymlyaml
application.yml:
- B6L10-0137 · Chroma im Detailkotlin
Im Embedded-Modus läuft Chroma als Sub-Prozess oder als In-Memory. Für Tests via Testcontainers:
- B6L10-0148 · Migrations-Story mit Flywaysql
Wenn pgvector deine Wahl ist, gehört das Schema in Flyway. So sieht ein erstes Migration-File aus:
- B6L10-0172 · pgvector-Branchyaml
docker-compose.yml:
- B6L10-0192 · pgvector-Branchyaml
application.yml:
- B6L10-0203 · Qdrant-Branchyaml
docker-compose.yml:
- B6L10-0223 · Qdrant-Branchyaml
application.yml:
- B6L10-0254 · Chroma-Branchyaml
application.yml:
- B6L10-0265 · Gemeinsamer Anwendungs-Codekotlin
In allen drei Branches dasselbe BenchmarkController.kt:
- B6L10-0276 · Benchmark durchführen — pro Branchbash
"Lohnabrechnungen findest du im Mitarbeiter-Portal unter „Gehalt"."
- B6L10-0287 · `notes.md` füllenmarkdown
Im Projekt-Root (am besten in einem vierten Branch benchmark-summary oder im main):
Lesson 11
9 snippets- B6L11-0044.2 · `TokenTextSplitter` als Defaultkotlin
LLMs haben Context-Windows von 200k Tokens und mehr — theoretisch könntest du ein ganzes Buch reinpacken. In der Praxis sind kleine Chunks (200 bis 500 Tokens) besser, aus drei Gründen:
- B6L11-0087 · Stufe 5 — Augmentor und `RetrievalAugmentationAdvisor`kotlin
Der Augmentor baut den finalen Prompt. Spring AI 1.1 liefert das fertig:
- B6L11-009Citation-Patternkotlin
Wenn du im Prompt-Template das LLM bittest, die Source-IDs zu nennen, kommt das in der Antwort als Text mit. Für sauberes Citation-Rendering im Frontend nimmst du strukturierten Output (L05):
- B6L11-0108 · Vollständige Pipeline an einem Beispielkotlin
Dann hat dein Frontend Source-IDs als saubere Liste, kann sie als anklickbare Links rendern.
- B6L11-0121 · Projekt erweiternyaml
application.yml:
- B6L11-0133 · `IngestionService` bauenkotlin
Achte darauf, dass die Texte sich teilweise überlappen (mehrere Quellen zum gleichen Thema) — sonst kannst du Retrieval-Quality später nicht testen.
- B6L11-0144 · `RagService` mit `RetrievalAugmentationAdvisor`kotlin
data class IngestStats(val filesProcessed: Int, val chunksWritten: Int)
- B6L11-0155 · REST-Controllerkotlin
chatClient.prompt(question).call().content() ?: ""
- B6L11-0166 · Pipeline laufen lassenbash
AskResponse(rag.ask(req.question))
Lesson 12
12 snippets- B6L12-0013.1 · Schemasql
Postgres bringt beides mit: pgvector für die Vektor-Suche, eingebauten Volltext-Index für BM25-artiges Ranking. (Postgres' tsrankcd ist nicht exakt BM25, aber funktional ähnlich genug — wer den echten BM25 will, schraubt
- B6L12-0023.2 · Hybrid-Querysql
Die contenttsv-Spalte ist eine Generated Column, Postgres pflegt sie automatisch. totsvector('german', ...) aktiviert das deutsche Stemming und die deutsche Stoppwort-Liste — wichtig, sonst werden „Anträge" und „Antrag"
- B6L12-0033.2 · Hybrid-Querykotlin
Klingt nach viel SQL — ist aber im Wesentlichen einmal hingelegt und gut. In Spring AI hängst du dir das in einen eigenen Retriever-Bean, der das DocumentRetriever-Interface implementiert:
- B6L12-0044.2 · Cohere Rerank API in Spring AIkotlin
Spring AI 1.1 hat noch keinen offiziellen Cohere-Rerank-Starter (Stand Mai 2026 — kommt in 1.2). Bis dahin nimmst du den cohere-java-client direkt:
- B6L12-0054.2 · Cohere Rerank API in Spring AIkotlin
Eingehängt in den Pipeline-Code:
- B6L12-0061 · Flyway-Migration für den Volltext-Indexsql
Lege im Projekt src/main/resources/db/migration/V3addfulltextindex.sql an:
- B6L12-0092 · `HybridRetriever` implementierenkotlin
Setze ein V1 für die Vector-Extension und V2 für die vectorstore-Tabelle vor V3 — siehe L10. Damit hast du dein Schema im Git.
- B6L12-0103 · `RagService` umstellen auf Hybrid-Retrieverkotlin
Im RagService aus L11 tauschst du den VectorStoreDocumentRetriever gegen den HybridRetriever:
- B6L12-0114 · Vergleich pure Vector vs pure BM25 vs Hybridkotlin
Baue drei Endpoints zum Vergleichen. Du brauchst neben dem HybridRetriever zwei zusätzliche, jeweils nur Vector und nur BM25:
- B6L12-0125 · Fünf Test-Queries, dokumentiertmarkdown
Für jede die drei Endpoints aufrufen und in notes.md notieren:
- B6L12-0156 · Cohere Reranker integrierenkotlin
CohereReranker.kt:
- B6L12-0166 · Cohere Reranker integrierenkotlin
RagService erweitern: Hybrid liefert top-20, Reranker macht daraus top-5:
Lesson 13
7 snippets- B6L13-0025 · Der Spring AI MCP Client Starteryaml
Stdio-Server konfigurieren in application.yml:
- B6L13-0035 · Der Spring AI MCP Client Starterkotlin
Damit startet Spring beim Application-Boot einen Subprozess, der den offiziellen Filesystem-MCP-Server hochfährt. Der bekommt das angegebene Verzeichnis als erlaubten Bereich mit. Ab dann kannst du dir den Client per Con
- B6L13-0046 · Tool-Discovery und Tool-Filterkotlin
Spring AI 1.1 hat dafür mehrere Stellschrauben. Du kannst Tools per Property oder Code filtern:
- B6L13-0051 · Projekt anlegen oder erweiternkotlin
Ins build.gradle.kts ergänzt du Spring AI:
- B6L13-0072 · Filesystem-MCP-Server konfigurierenyaml
Dann in src/main/resources/application.yml:
- B6L13-0083 · ChatClient mit MCP-Tools verdrahtenkotlin
Leg eine Konfiguration an:
- B6L13-0093 · ChatClient mit MCP-Tools verdrahtenkotlin
Und einen kleinen REST-Endpoint:
Lesson 14
14 snippets- B6L14-0011 · Mehr als ein Server — und warum das normal wirdyaml
Eine typische Konfiguration mit drei Servern:
- B6L14-0023 · Resources lesen — wenn Tools nicht reichenkotlin
Für das LLM unterscheidet sich das nicht groß von einem Tool — der Spring-AI-Starter exponiert Resources standardmäßig nicht als Tool. Wenn du sie brauchst, machst du das selbst:
- B6L14-0035 · Remote-Server mit OAuthyaml
Spring AI 1.1 hat dafür einen eingebauten OAuth-Adapter. Konfiguration in application.yml:
- B6L14-0046 · Pro Endpoint einen eigenen ChatClientkotlin
Drei spezialisierte Beans, die dasselbe LLM teilen:
- B6L14-0057 · Graceful Degradation — wenn ein Server nicht antwortetyaml
Konfigurations-Stellschrauben dafür:
- B6L14-0078 · Logging der Tool-Callskotlin
Für ein einfaches Audit-Log ohne komplette Observation-Pipeline reicht ein eigener Advisor:
- B6L14-0099 · Testen ohne echten Serverkotlin
Spring AI 1.1 hat dafür ein Mock-Setup. Mit @AutoConfigureMcpServerMock (im Modul spring-ai-test) bekommst du eine In-Memory-Variante, die du programmatisch füllst:
- B6L14-0109 · Testen ohne echten Serverkotlin
In der Praxis nutze ich für reine Unit-Tests einen MockK-basierten McpSyncClient-Mock und teste so:
- B6L14-0111 · GitHub-MCP-Server dazuhängenyaml
Erweitere application.yml:
- B6L14-0132 · Zwei spezialisierte ChatClient-Beanskotlin
Bau in ChatClientConfig.kt zwei Beans:
- B6L14-0144 · Multi-Step-Workflow ausprobierenkotlin
Erweitere AskController oder bau einen zweiten Endpoint:
- B6L14-0165 · Read-Only-Endpoint quercheckenkotlin
Zweiter Endpoint, der den readOnlyChat-Bean nutzt:
- B6L14-0186 · Härtetest mit unerreichbarem Serveryaml
Erweitere application.yml um einen Server, der absichtlich nicht startet:
- B6L14-0197 · Bonus: Einen Test mit Mock-Serverkotlin
Bau einen @SpringBootTest, der mit @AutoConfigureMcpServerMock arbeitet und einen einfachen Workflow ohne echten Server prüft:
Lesson 15
11 snippets- B6L15-0023 · Plan-and-Execute — erst denken, dann ausführenjson
Beispiel-Plan:
- B6L15-0033 · Plan-and-Execute — erst denken, dann ausführenkotlin
Implementiert mit Spring AI:
- B6L15-0044 · Self-Reflection — die eigene Antwort prüfenkotlin
Optional iterativ mit Limit (max 3 Iterationen ist üblich).
- B6L15-0055 · Tool-Use-as-Decisionkotlin
Beispiel: ein Mail-Triage-Agent muss jede Mail in eine Kategorie sortieren. Statt das LLM frei Text produzieren zu lassen, gibst du ihm ein Tool:
- B6L15-0061 · Plan-Modell und Planner-Beankotlin
Leg ein Datenmodell für den Plan an:
- B6L15-0071 · Plan-Modell und Planner-Beankotlin
Ergänze in ChatClientConfig.kt einen planner-Bean — er bekommt keine Tools, soll nur planen:
- B6L15-0081 · Plan-Modell und Planner-Beankotlin
Und einen executor-Bean — der bekommt alle Filesystem- und Git-Tools:
- B6L15-0092 · Planner-Executor-Servicekotlin
.defaultToolCallbacks(tools)
- B6L15-0103 · Endpoint für den Status-Reportkotlin
return outputs.values.last()
- B6L15-0124 · Self-Reflection-Loop für Code-Generierungkotlin
Bau eine zweite Klasse, die Code-Generation mit Self-Reflection wraps:
- B6L15-0134 · Self-Reflection-Loop für Code-Generierungkotlin
data class GeneratedCode(val code: String, val iterations: Int, val history: List<String>)
Lesson 16
18 snippets- B6L16-0012 · Spring AIs Advisor-Patternkotlin
Ein Advisor ist eine Middleware um den ChatClient-Call. Er sieht den Request, kann ihn vor dem LLM-Call manipulieren (z.B. Memory anreichern, RAG-Kontext einfügen), und er sieht die Response, kann sie nach dem Call manip
- B6L16-0023 · Window-Memory — der einfache Fallkotlin
Die einfachste Implementierung: MessageWindowChatMemory. Sie hält die letzten N Messages pro Conversation-ID:
- B6L16-0044 · Persistente Memory mit JDBCsql
Schema (wird über Flyway oder das spring.ai.chat.memory.repository.jdbc.initialize-schema=true-Flag automatisch angelegt):
- B6L16-0054 · Persistente Memory mit JDBCyaml
Konfiguration:
- B6L16-0064 · Persistente Memory mit JDBCkotlin
password: agents
- B6L16-0075 · Conversation-ID — der Schlüsselkotlin
In der API kommt die ID aus dem Request-Kontext. Für einen REST-Endpoint:
- B6L16-0096 · Token-Budget — und warum Window allein nicht reichtkotlin
Token-basierte Window-Strategie statt Message-basiert:
- B6L16-0107 · Summary-Memorykotlin
Spring AI 1.1 hat dafür SummaryChatMemoryAdvisor als Companion zum MessageChatMemoryAdvisor. Es wird auf Threshold getriggert (z.B. „wenn mehr als 15 Messages, summarize die ältesten 5"):
- B6L16-0118 · Long-Term-Memory mit Vector Storekotlin
Das ist konzeptionell RAG, nur dass die Dokumente keine Wiki-Seiten sind, sondern alte Konversationen.
- B6L16-0129 · Hybrid — was die Realität verlangtkotlin
Spring AI 1.1 lässt das zu, indem du mehrere Advisors stapelst:
- B6L16-0131 · Postgres aus Docker hochfahrenyaml
Im Projekt-Root eine docker-compose.yml (oder bestehende ergänzen):
- B6L16-0152 · Spring AI Memory-Modul und JDBC einbindenkotlin
In build.gradle.kts:
- B6L16-0162 · Spring AI Memory-Modul und JDBC einbindenyaml
In application.yml:
- B6L16-0173 · Memory-Beans verdrahtenkotlin
In ChatClientConfig.kt:
- B6L16-0184 · Chat-Endpoint mit Conversation-IDkotlin
.defaultAdvisors(MessageChatMemoryAdvisor.builder(windowMemory).build())
- B6L16-0195 · Mehrstufigen Dialog laufen lassenbash
App starten. Im Terminal ein paar Calls hintereinander:
- B6L16-0216 · Mehrere parallele Konversationenbash
Test mit zwei Conversation-IDs gleichzeitig:
- B6L16-0228 · Summary-Strategie zusätzlichkotlin
Bau einen Summary-Memory-Bean mit kleinem Trigger:
Lesson 17
12 snippets- B6L17-0012 · Supervisor-Patterntext
Ein zentraler Agent (der Supervisor) bekommt die Aufgabe, zerlegt sie und delegiert an spezialisierte Worker-Agenten. Worker arbeiten parallel oder sequenziell, liefern Resultate zurück, der Supervisor aggregiert und gib
- B6L17-0022 · Supervisor-Patternkotlin
In Spring AI bauen wir das selbst. Jeder Agent ist ein ChatClient-Bean mit eigenem System-Prompt, eigenen Tools, eigenem Modell. Der Supervisor ist eine Klasse, die die Workflows orchestriert:
- B6L17-0033 · Sequential Pipelinekotlin
Das ist im Kern eine Pipeline, in der jeder Agent eine klare Verantwortung hat. Vorteil gegenüber einem einzelnen Agent mit dreiteiliger Aufgabe: jeder Schritt ist isoliert testbar, kann unabhängig getunt werden, und die
- B6L17-0044 · Parallel / Map-Reducekotlin
In Kotlin nutzt du Coroutines für die Parallelisierung:
- B6L17-0055 · Debate / Critiquekotlin
Use-Case-Beispiel: Code-Review.
- B6L17-0061 · Brave-Search-MCP-Server einbindenyaml
Voraussetzungen: Setup aus Übung 16 läuft. Du hast Zugriff auf den Brave-Search-MCP-Server (Brave-Search-API-Key) oder einen vergleichbaren Web-Search-MCP-Server. Token-Tracking aus L08 idealerweise schon im Projekt.
- B6L17-0082 · Drei spezialisierte ChatClient-Beanskotlin
Wenn du keinen Brave-Key hast, geht alternativ:
- B6L17-0093 · Orchestrator-Klassekotlin
Für die Models brauchst du zwei AnthropicChatModel-Beans mit unterschiedlicher Konfiguration (Opus für den Supervisor, Sonnet für die anderen). Wenn dein Setup nur ein Modell konfiguriert hat: nimm dasselbe Modell überal
- B6L17-0104 · Endpoints für beide Variantenkotlin
data class ResearchResult(val topic: String, val report: String, val findings: List<String>)
- B6L17-0115 · Beide Varianten mit derselben Aufgabebash
data class ResearchRequest(val topic: String)
- B6L17-0126 · Bonus: Debate-Pattern für Code-Reviewkotlin
Bau ein zweites Multi-Agent-Setup für eine Code-Review-Aufgabe:
- B6L17-0136 · Bonus: Debate-Pattern für Code-Reviewkotlin
.defaultSystem("Du moderierst zwei Code-Reviews. Wäge ab und liefere eine Gesamtbewertung (1–5 Sterne) mit kurzer Begründung.")
Lesson 18
15 snippets- B6L18-0012 · Das Test-Datasetjson
src/test/resources/evaluation/rag-dataset.json:
- B6L18-0023 · LLM-as-Judge — das zentrale Mustertext
Der Prompt für den Judge ist die halbe Miete. Das hier funktioniert in meiner Praxis:
- B6L18-0045 · Spring AI Evaluator-API in Aktionkotlin
Beispiel mit FactCheckingEvaluator:
- B6L18-0055 · Spring AI Evaluator-API in Aktionkotlin
Im Test ruft du das auf:
- B6L18-0066 · Einen LLM-as-Judge-Test bauenkotlin
src/test/kotlin/com/example/agent/evaluation/RagEvaluationTest.kt:
- B6L18-0077 · Continuous Evaluationkotlin
build.gradle.kts:
- B6L18-0098 · Snapshots mit Toleranz — die billigere Variantekotlin
Für viele Smoke-Tests reicht ein einfacheres Verfahren: Cosine-Similarity zwischen Embedding der neuen Antwort und Embedding der gespeicherten Snapshot-Antwort. Wenn der Wert über 0.85 liegt, sagt das System dasselbe — a
- B6L18-0109 · Heuristik-Checks für Format und Tabu-Verhaltenkotlin
Format-Validation. Wenn dein Agent JSON liefern soll, parst du das JSON. Klappt nicht: Test rot.
- B6L18-0111 · Test-Dataset anlegenjson
Schema pro Eintrag (passe das an deine Domäne an):
- B6L18-0122 · Zwei `ChatClient`-Beans konfigurierenkotlin
backend/src/main/kotlin/com/example/agent/config/ChatClients.kt:
- B6L18-0133 · Den Evaluator-Test schreibenkotlin
backend/src/test/kotlin/com/example/agent/evaluation/RagEvaluationTest.kt:
- B6L18-0144 · Faithfulness-Check mit `FactCheckingEvaluator`kotlin
Für drei deiner Datensätze, die einen klar definierten Context-Treffer haben, lass zusätzlich den Spring-AI-FactCheckingEvaluator laufen:
- B6L18-0155 · Markdown-Report schreibenmarkdown
Nach jedem Lauf landet ein Report unter build/reports/evaluation/2026-05-XX.md. Format:
- B6L18-016Detailkotlin
Implementierung:
- B6L18-0176 · Gradle-Task `evaluate` einrichtenkotlin
backend/build.gradle.kts:
Lesson 19
22 snippets- B6L19-001Pattern-Matching gegen bekannte Phrasenkotlin
Ein simpler String.contains-Check mit lowercase-Normalisierung fängt einen großen Teil der naiven Versuche ab. Das ist nicht intelligent, aber es kostet nichts und macht den Angreifer-Aufwand größer.
- B6L19-002Klassifikator-LLM als Pre-Checkkotlin
Spring AI 1.1 hat dafür kein eigenes Built-in, aber du baust dir das in 30 Zeilen:
- B6L19-003PII-Detection mit Regexkotlin
Bevor du den Output an den User schickst, prüfst du auf personenbezogene Daten. Ein paar Regex-Muster decken einen großen Anteil ab:
- B6L19-004Spring AI Advisor — Ort der Verdrahtungkotlin
Spring AI 1.1 hat das Advisor-Konzept eingeführt — eine Art Middleware, die vor und nach einem ChatClient-Call läuft. Das ist der idiomatische Ort für Guards.
- B6L19-005Spring AI Advisor — Ort der Verdrahtungkotlin
Verdrahtung im ChatClient:
- B6L19-006Tool-Whitelist pro User / Tenantkotlin
Nicht jeder User darf jedes Tool ausführen. Ein Helpdesk-Mitarbeiter kann lookupcustomer, aber nicht deletecustomer. Ein Admin kann beides. In Spring AI verdrahtest du das so:
- B6L19-007Tool-Whitelist pro User / Tenantkotlin
Das setzt voraus, dass du deine Tools mit Tag-Metadaten markierst:
- B6L19-008Confirmation-Flow für destruktive Aktionenkotlin
Implementierung mit einem @Tool-Wrapper, der Confirmation erzwingt:
- B6L19-009Audit-Log für jeden Tool-Callkotlin
Jeder Tool-Call wird in eine Audit-Tabelle geschrieben — wer, wann, mit welchen Parametern, mit welchem Ergebnis. Das ist nicht nur Compliance, sondern auch Debugging-Werkzeug.
- B6L19-010Audit-Log für jeden Tool-Callsql
Das AOP-Konstrukt fängt jeden Method-Call, der mit @Tool annotiert ist, ab und schreibt einen Eintrag. Postgres-Tabelle:
- B6L19-0115 · System-Prompt-Lockdowntext
Im System-Prompt baust du selbst Anti-Leak-Anweisungen ein. Das ist die billigste Stufe und sollte unabhängig von allem anderen drin sein:
- B6L19-0126 · LangChain4j-Äquivalent und NeMo Guardrailskotlin
Spring AI ist nicht die einzige Library mit diesem Konzept. LangChain4j hat InputGuardrail und OutputGuardrail Interfaces, die genauso funktionieren:
- B6L19-0131 · `InputGuard` mit Pattern-Matcherkotlin
backend/src/main/kotlin/com/example/agent/guard/InputGuard.kt:
- B6L19-0141 · `InputGuard` mit Pattern-Matcherkotlin
Im Controller (oder in deinem Agent-Service):
- B6L19-0152 · `PiiDetector` und Output-Advisorkotlin
PiiDetector aus der Lektion 1:1 übernehmen. Den Advisor verdrahten:
- B6L19-0163 · `send_mail`-Tool mit Confirmation-Flowsql
src/main/resources/db/migration/V10pendingactions.sql:
- B6L19-0173 · `send_mail`-Tool mit Confirmation-Flowkotlin
Repository und Service-Klassen:
- B6L19-0183 · `send_mail`-Tool mit Confirmation-Flowkotlin
Tools für den Agenten:
- B6L19-0193 · `send_mail`-Tool mit Confirmation-Flowkotlin
System-Prompt entsprechend ergänzen:
- B6L19-0204 · System-Prompt-Lockdownkotlin
Erweitere deinen System-Prompt um die Anti-Leak-Regeln:
- B6L19-0215 · Test-Suite mit fünf Angriffs-Eingabenkotlin
backend/src/test/kotlin/com/example/agent/guard/GuardrailIntegrationTest.kt:
- B6L19-0236 · Bonus — LLM-Klassifikator als Input-Pre-Checkkotlin
Bau einen zweiten Guard, der einen Haiku-Call macht, um den Input zu klassifizieren:
Lesson 20
27 snippets- B6L20-001Architektur in einem Bildtext
Das ist die Vorlage, auf die ich in eigenen Projekten zurückgreife, sobald ein Kunde sagt: „Ich brauche einen Agent, der mit unserer Microsoft-365-Umgebung arbeitet." Wenn du es einmal aufgesetzt hast, sparst du dir bei
- B6L20-002Projekt-Setupkotlin
build.gradle.kts:
- B6L20-003Projekt-Setupyaml
application.yml:
- B6L20-004Projekt-Setupyaml
Wer den Mock-MCP-Server nutzt, ersetzt den servers-Block durch:
- B6L20-005Der Agent — Plan-and-Executekotlin
backend/src/main/kotlin/com/example/agents/triage/MailboxTriageAgent.kt:
- B6L20-006Der Agent — Plan-and-Executekotlin
backend/src/main/kotlin/com/example/agents/triage/SystemPrompt.kt:
- B6L20-007Das Result-Modellkotlin
backend/src/main/kotlin/com/example/agents/triage/TriageResult.kt:
- B6L20-008Audit-Logging und Cost-Trackingsql
src/main/resources/db/migration/V1init.sql:
- B6L20-009Audit-Logging und Cost-Trackingkotlin
Der CostTracker macht ein simples Lookup gegen die Properties:
- B6L20-010MCP-Client-Konfigurationkotlin
Spring AI 1.1 hat einen MCP-Client-Starter, der die Verbindung über stdio oder HTTP-SSE aufbaut. Die Tools werden zur Laufzeit als ToolCallback-Objekte registriert. Im ChatClient brauchst du nur:
- B6L20-011MCP-Client-Konfigurationtext
Wer den Mock-Server nutzt, sieht in den Logs beim Start:
- B6L20-012Der Endpointkotlin
backend/src/main/kotlin/com/example/agents/triage/TriageController.kt:
- B6L20-014Der Endpointjson
Beispiel-Response:
- B6L20-015Memory pro Userkotlin
Memory-Verdrahtung:
- B6L20-016Memory pro Userkotlin
Im Agent-Service nutzt du conversationId als User-Kennung:
- B6L20-017Evaluation für Triage-Qualitätkotlin
backend/src/test/kotlin/com/example/agents/triage/TriageEvaluationTest.kt:
- B6L20-018Build-Pipelinebash
Alles läuft über Gradle:
- B6L20-019Evaluationyaml
docker-compose.yml minimal:
- B6L20-020UI — was minimal reichthtml
Für lokales Spielen reicht curl plus jq. Wer einen visuellen Tab will, baut sich in 30 Minuten eine kleine HTML-Page mit Vanilla-JS:
- B6L20-0211 · Projekt anlegenkotlin
Spring AI fügt der Initializr noch nicht automatisch hinzu (Stand Mai 2026 — die Auto-Inclusion ist für Spring AI 1.2 geplant). Bearbeite build.gradle.kts:
- B6L20-0222 · Mock-MCP-Server aufsetzenkotlin
mock-o365-mcp/build.gradle.kts:
- B6L20-0232 · Mock-MCP-Server aufsetzenkotlin
mock-o365-mcp/src/main/kotlin/com/example/mock/o365/Main.kt:
- B6L20-0263 · `application.yml` im Triage-Projektyaml
backend/src/main/resources/application.yml:
- B6L20-0274 · Flyway-Migrationenyaml
docker-compose.yml:
- B6L20-0295 · Code-Struktur anlegentext
docker compose up -d
- B6L20-0316 · Application startentext
In den Logs sollte stehen:
- B6L20-0359 · Bonus 1 — Slack-Webhook für tägliche Zusammenfassungkotlin
@Scheduled-Task, der einmal pro Tag um 9:00 läuft, Triage anstößt und das summary-Feld an einen Slack-Webhook pusht: