1. Komplexe Typen in der Konfiguration
Zu diesem Zeitpunkt kann CatalogProperties bereits als normaler Bean im Kontext leben. Aber wenn der Binder nur Strings, Zahlen und Flags binden kann, wird catalog-service trotzdem nicht auf die typisierte Konfiguration umsteigen: Im Katalog gibt es eine Liste von Kursen, einen verschachtelten Preis, Enums, Daten und Diensttypen wie Intervalle und Größen.
Daher betrachten wir jetzt nicht eine Sammlung von verstreuten Tricks, sondern die Bindungsformen, ohne die das vollständige CatalogProperties einfach nicht zusammengebaut werden kann.
Wenn man gerade anfängt, scheint es, dass die Konfiguration sehr einfach sein sollte: ein paar Zeichenfolgen, ein paar Zahlen und maximal ein boolesches Flag „ein/aus“. Aber das echte Leben bringt schnell „lass uns eine Liste von Kursen aus der Konfiguration haben“, „lass uns einen Preis als Objekt haben“, „lass uns ein Aktualisierungsintervall in Sekunden haben“ und „lass uns ein Größenlimit in Megabyte haben“. Und hier hat der Anfänger normalerweise den Wunsch… alles von Hand zu parsen.
Um sich ein wenig zu beruhigen: Spring Boot hat @ConfigurationProperties speziell so gemacht, dass Sie die Konfiguration mit Typen beschreiben können, nicht mit Zeichenketten-Puzzles. Dies ist keine „Premium-Funktion für Senioren“, sondern eine Möglichkeit, alltäglichen Schmerz zu vermeiden, wie „jemand hat 10MB geschrieben, aber unser Code erwartete 10Mb und alles ist abgestürzt“ (oder noch schlimmer, es ist nicht abgestürzt, sondern hat leise falsch funktioniert).
Es ist nützlich, ein einfaches Bild davon im Kopf zu behalten, was beim Start passiert:
flowchart TD A["application.yaml + Profil-Dateien + Imports"] --> B["Environment
(endgültige Eigenschaften)"] B --> C["@ConfigurationProperties Binder
(Typkonvertierung)"] C --> D["CatalogProperties / andere Properties-Klassen"] D --> E["Ihre Beans
(Service, Runner, Controller)"]
Das bedeutet, die „Komplexität“ liegt nicht darin, dass Sie YAML raffinierter machen. Die Komplexität liegt genau darin, dass Sie nicht die YAML-Struktur in den Kopf des Entwicklers übertragen wollen. Sie wollen sie in Java-Klassen übertragen, wo die IDE mit Autovervollständigung hilft und der Compiler mit Typen.
2. Verschachtelte Objekte: wenn ein Wert eine Struktur ist
Verschachtelte Objekte in der Konfiguration entstehen nicht aus Liebe zur YAML-Ästhetik (obwohl YAML sich auch bemüht), sondern weil einige Dinge logisch einen zusammenhängenden Satz von Feldern darstellen. Ein klassisches Beispiel ist der Preis: Fast nie ist der Preis „einfach eine Zahl“, weil sich daneben immer eine Währung versteckt. Und wenn Sie alles als einen String speichern, machen Sie sich selbst zusätzliche Arbeit mit Parsen und Prüfungen.
Stellen wir uns einen Ausschnitt des Kurskonfigurations in catalog-data.yaml vor (oder direkt in application.yaml, falls Sie die Daten noch nicht ausgelagert haben – kein Problem, die Struktur ist dieselbe):
app:
catalog:
courses:
- slug: "spring-boot"
title: "Spring Boot"
price:
amount: 19900
currency: "GBP"
Anstatt price: "19900 GBP" zu speichern und das durch split(" ") zu parsen (und dann unter doppelten Leerzeichen, „£“ und der Fantasie von jemandem zu leiden), beschreiben wir ein verschachteltes Objekt.
Minimaler Properties-Klasse für den Preis:
package com.example.catalogservice.config;
public class MoneyProperties {
// Betrag in kleinsten Geldeinheiten (z.B. Cent),
// um sich nicht mit double/float in der Konfiguration herumschlagen zu müssen.
private long amount;
// Währung als String (z.B. "GBP").
// Bei Bedarf kann dies auch ein Enum sein, aber ein String ist ein normaler Start.
private String currency;
// getters and setters
}
Und das Listenelement der Kurse, das das verschachtelte Objekt verwendet:
package com.example.catalogservice.config;
public class CourseItemProperties {
// Eindeutige Kurskennung in URL/Katalog
private String slug;
// Menschenlesbarer Kurstitel
private String title;
// Verschachteltes Objekt: der Binder füllt es aus price.amount und price.currency
private MoneyProperties price;
// getters and setters
}
Hier ist es wichtig, die Mechanik zu verstehen: Der Binder sieht, dass das Feld price ein separates Objekt ist, und versucht, es aus dem Unterpfad price.amount und price.currency zu füllen. Sie müssen keine separaten @Value("${...}") für jedes verschachtelte Feld schreiben. Sie haben die Form der Daten beschrieben – und Boot bindet sie selbst.
Um zu fühlen, dass das wirklich funktioniert, können Sie (auf der Ebene des Lesens in einem Runner) etwas Einfaches ausgeben. Ja, System.out.println werden wir später durch Logs ersetzen (das wird im Modul über Logging behandelt), aber jetzt ist uns die Mechanik wichtig:
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;
@Component
public class PriceDebugRunner implements ApplicationRunner {
// Die Properties-Klasse muss bereits als Bean registriert sein (durch scan/enable)
private final CatalogProperties properties;
public PriceDebugRunner(CatalogProperties properties) {
// Wir injizieren die Konfiguration als typisiertes Objekt, nicht als String-Sammlung
this.properties = properties;
}
@Override
public void run(ApplicationArguments args) {
// Der Einfachheit halber nehmen wir den ersten Kurs (im echten Code besser auf leere Liste prüfen)
var first = properties.getCourses().get(0);
// Prüfen, ob die verschachtelte Struktur korrekt gebunden wurde
System.out.println(first.getPrice().getCurrency()); // GBP
}
}
Der Sinn der Verschachtelung liegt auch darin, dass sie die Konfiguration lesbar macht: price.currency liest sich mit den Augen als „Währung des Preises“, nicht als mysteriöses Suffix in einem String.
3. Listen (List): Konfiguration als Datenquelle
Wenn in der Konfiguration eine wiederkehrende Entität auftaucht (Kurse, Regeln, Limits, Presets), kommt der Gedanke „lass uns das in einen String packen und parsen“ irgendwie sehr schnell. Wahrscheinlich ist das eine Erinnerung an die Arbeit mit .properties und dem ewigen Format a=b,c=d,e=f. Aber mit YAML und @ConfigurationProperties kann man wesentlich glücklicher leben.
Für die Kursliste sieht YAML natürlich aus: courses: und dann Elemente mit Gedankenstrichen.
app:
catalog:
courses:
- slug: "spring-boot"
title: "Spring Boot"
track: "SPRING"
- slug: "spring-core"
title: "Spring Core"
track: "SPRING"
In CatalogProperties wird dies durch das Feld List<CourseItemProperties> abgebildet.
import java.util.ArrayList;
import java.util.List;
import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties("app.catalog")
public class CatalogProperties {
// Kursliste aus app.catalog.courses (YAML mit Gedankenstrichen)
// Initialisierung schützt vor NPE, falls courses in der Konfiguration nicht angegeben ist.
private List<CourseItemProperties> courses = new ArrayList<>();
public List<CourseItemProperties> getCourses() {
return courses;
}
public void setCourses(List<CourseItemProperties> courses) {
// Der Binder weist hier eine neue Liste zu, die aus der Konfiguration erstellt wurde
this.courses = courses;
}
}
Hier gibt es einen kleinen, aber wichtigen Punkt: Wir initialisieren die Liste mit einer leeren ArrayList. Dies ist keine „Feature des Tages über Defaults“, sondern einfach ein alltäglicher Schutz vor NullPointerException, damit Code, der getCourses().size() aufruft, nicht abstürzt, wenn die Konfiguration aus irgendeinem Grund leer ist.
Danach können Sie mit courses wie mit einer normalen Sammlung von Java-Objekten arbeiten. Zum Beispiel die Anzahl der Kurse ausgeben:
import org.springframework.stereotype.Component;
@Component
public class CoursesCountPrinter {
// Auch hier: keine Strings und kein @Value, sondern eine fertige Struktur
private final CatalogProperties properties;
public CoursesCountPrinter(CatalogProperties properties) {
this.properties = properties;
}
public int coursesCount() {
// Wenn courses nicht angegeben ist, wird 0 zurückgegeben (dank der Listeninitialisierung)
return properties.getCourses().size();
}
}
Beachten Sie, dass wir auf der Seite des Anwendungscodes überhaupt nicht darüber nachdenken, woher die Daten kommen: aus application.yaml, aus catalog-data.yaml über spring.config.import, aus Umgebungsvariablen oder von einem externen Ort. Das ist die Aufgabe der Konfigurationsschicht, nicht des Geschäftscodes.
Wenn wir ein wenig verallgemeinern, dann ist List in der Konfiguration ein Signal: „Die Konfiguration enthält einen Satz gleichartiger Objekte“. Und das ist genau der Fall, in dem die typisierte Konfiguration ihren Hauptwert zeigt: Sie „parsen keine Daten“, Sie „erhalten eine Struktur“.
4. enum in der Konfiguration: weniger Strings – weniger zufälliger Schmerz
In der Konfiguration ist es sehr leicht, alles als Strings zu speichern. Am Anfang scheint es flexibel: heute schreiben wir "SPRING", morgen "spring", übermorgen "Spring"… und dann stellt sich plötzlich heraus, dass drei verschiedene Werte dasselbe bedeuten, aber die Vergleiche mit equalsIgnoreCase haben sich über das ganze Projekt verteilt. Und ja, irgendwo taucht mit Sicherheit der String "Sprnig" auf (das ist kein Tippfehler, sondern die zukünftige Legende Ihres Projekts).
Wenn ein Wert aus einer begrenzten Liste stammt, ist das fast immer ein Kandidat für ein enum. In unserem catalog-service passt das perfekt auf CourseTrack und CourseLevel.
Beispiel eines enum in der Domäne (das haben wir bereits laut Projektspezifikation):
package com.example.catalogservice.catalog.domain;
public enum CourseTrack {
// Explizite Liste der zulässigen Werte.
// Wenn in der Konfiguration "SPRNG" vorkommt, stürzt die Anwendung beim Start ab – und das ist gut.
SPRING, JAVA_BACKEND, DATA, INFRA, ADVANCED
}
Und jetzt können Sie in CourseItemProperties dieses Enum direkt als Feldtyp verwenden:
import com.example.catalogservice.catalog.domain.CourseTrack;
public class CourseItemProperties {
private String slug;
// Boot versucht, den String aus YAML in eine enum-Konstante zu konvertieren
private CourseTrack track;
// getters and setters
}
In YAML sieht das so aus:
track: "SPRING"
Boot versucht beim Binden, den String in das Enum zu konvertieren. Und hier gibt es eine gute Nachricht: Beim Properties-Binding macht Boot die Konvertierung normalerweise recht „menschlich“ (einschließlich einer milderen Handhabung von Groß-/Kleinschreibung und Schreibweise). Aber methodisch empfehle ich im Lernprojekt, nicht zu sehr darauf zu vertrauen, dass „er es schon merkt“, sondern die Werte so zu schreiben, dass sie mit den enum-Konstanten übereinstimmen: Das ist einfacher zu lesen, einfacher im Projekt zu suchen und einfacher zu erklären.
Das Wichtigste: Wenn in der Konfiguration ein Wert vorkommt, den es im Enum nicht gibt (z. B. track: "SPRNG"), wird die Anwendung wahrscheinlich nicht starten, weil der Binder keine Konvertierung durchführen kann. Und das ist gutes Verhalten: Es ist besser, das Problem beim Start zu sehen, als zwei Stunden nach einem Fehler zu suchen, weil „die Kursliste leer ist“.
5. Maps (Map): Wörterbücher und Abbildungen ohne manuelle Formate
Map in der Konfiguration wird benötigt, wenn Sie keine „Liste gleichartiger Objekte“, sondern ein Wörterbuch haben: Schlüssel → Wert. Das ist praktisch für benannte Abbildungen, Umbenennungen, einfache „Nachschlagetabellen“ und Flags, die vom Namen abhängen.
Zum Beispiel möchten Sie lesbare Abbildungen von Tracks in der UI/Logs/Landingpage haben, aber sie nicht fest im Code verankern. Dann können Sie eine Map trackDisplayNames erstellen.
YAML:
app:
catalog:
track-display-names:
SPRING: "Spring"
JAVA_BACKEND: "Java Backend"
DATA: "Data"
Java-Feld:
import java.util.HashMap;
import java.util.Map;
import com.example.catalogservice.catalog.domain.CourseTrack;
public class CatalogProperties {
// Schlüssel sind Enums, daher müssen die YAML-Schlüssel auch den enum-Konstanten entsprechen
// (oder zumindest erfolgreich in sie konvertiert werden).
private Map<CourseTrack, String> trackDisplayNames = new HashMap<>();
// getters and setters
}
Hier gibt es einen interessanten Punkt: Der Schlüssel der Map ist CourseTrack, also ein Enum. Boot wird versuchen, die YAML-Schlüssel in das Enum zu konvertieren. Das ergibt einen sehr angenehmen Effekt: Sie erhalten typisierte Schlüssel, keine stringbasierte Willkür.
Verwendung (z. B. in einem Code zur Generierung „menschlicher“ Zeichenfolgen):
public String displayNameFor(CourseTrack track) {
// Wenn die Map keine Abbildung enthält – verwende den Enum-Namen als Fallback
return properties.getTrackDisplayNames().getOrDefault(track, track.name());
}
Die zweite häufige Variante von Map ist eine einfache Map<String, String>, wenn die Schlüssel wirklich beliebig sind. Zum Beispiel, wenn wir Labels für verschiedene „Badges“ auf der Landingpage speichern wollten:
app:
catalog:
badges:
featured: "Empfohlen"
new: "Neu"
Java:
import java.util.HashMap;
import java.util.Map;
public class CatalogProperties {
// Beliebige String-Schlüssel (z. B. "featured", "new")
private Map<String, String> badges = new HashMap<>();
// getters and setters
}
Ein wichtiger Gedanke: Map ist fast immer besser als ein „String im Format key=value,key=value“. Denn ein String ist eine Einladung zu Fehlern, zu Escaping, zu Leerzeichen und zu dem split(","), das genau bis zum ersten Komma im Wert funktioniert.
6. Duration und DataSize in der Konfiguration
Duration und DataSize sind zwei Typen, die besonders gut die Philosophie von Boot zeigen: Die Konfiguration sollte menschenlesbar sein, und der Code sollte mit Typen arbeiten, nicht mit einem „rohen String“.
Duration: „30s“, „5m“, „2h“ statt long Millisekunden
Stellen wir uns vor, wir haben ein periodisches Verhalten (auch wenn wir noch keinen echten Scheduler machen – uns ist jetzt das Binden wichtig). Wir wollen schreiben:
app:
catalog:
refresh-period: 30s
Und in Java bereits eine Duration erhalten, um typsicher damit zu arbeiten.
import java.time.Duration;
import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties("app.catalog")
public class CatalogProperties {
// Der Wert wird in YAML als 30s/5m/2h angegeben und in java.time.Duration konvertiert
// Wenn nicht angegeben, ist der Wert null (daher entweder einen Default setzen oder prüfen).
private Duration refreshPeriod;
public Duration getRefreshPeriod() {
return refreshPeriod;
}
public void setRefreshPeriod(Duration refreshPeriod) {
this.refreshPeriod = refreshPeriod;
}
}
Und dann im Code:
Duration d = properties.getRefreshPeriod();
System.out.println(d.toSeconds()); // 30
Wichtig: Für Duration werden in Boot menschenlesbare Suffixe wie ms, s, m, h, d unterstützt. Auch das ISO-8601-Format wie PT30S wird oft unterstützt. Aber im Lernprojekt ist es besser, einen einfachen und lesbaren Stil zu verwenden: 30s, 5m, 2h. Das ist bequem für Sie und für diejenigen, die die Konfiguration nach Ihnen lesen werden (also für Sie selbst, aber in einem Monat, wenn Sie vergessen haben, was die Zahl 30000 bedeutete).
DataSize: „64KB“, „10MB“ statt unverständlicher Zahlen
DataSize ist ein Typ aus Spring (org.springframework.util.unit.DataSize), der praktisch für alle Limits „bezogen auf die Datengröße“ ist. Ja, unser catalog-service lädt keine Dateien hoch und macht kein Multipart, aber Größenlimits können überall auftauchen: Zum Beispiel möchten wir eine „Vorschau“ oder die Größe eines Datenstücks begrenzen, das wir für angemessen halten.
YAML:
app:
catalog:
preview-payload-limit: 64KB
Java:
import org.springframework.util.unit.DataSize;
public class CatalogProperties {
// Der Wert wird als 64KB/10MB angegeben und in DataSize konvertiert
private DataSize previewPayloadLimit;
public DataSize getPreviewPayloadLimit() {
return previewPayloadLimit;
}
public void setPreviewPayloadLimit(DataSize previewPayloadLimit) {
this.previewPayloadLimit = previewPayloadLimit;
}
}
Verwendung:
DataSize limit = properties.getPreviewPayloadLimit();
System.out.println(limit.toBytes()); // 65536
Die unterstützten Suffixe sehen normalerweise aus wie B, KB, MB, GB (und bei Bedarf weiter). Es ist wichtig, die Einheiten explizit anzugeben. Wenn Sie nur eine Zahl lassen, zwingen Sie alle zu raten: Sind das Bytes? Kilobytes? Megabytes? Oder „das ist die Anzahl der Zeichen, aber wir haben es aus irgendeinem Grund size genannt“?
Und hier entsteht ein kleiner methodischer Genuss: Anstatt den String "64KB" von Hand zu parsen, arbeiten Sie sofort mit DataSize. Die IDE lässt Sie nicht versehentlich Kilobytes mit Sekunden addieren, und der Code beginnt wie ein technischer Text auszusehen, nicht wie ein „magischer Taschenrechner“.
7. Relaxed Binding: Namenszuordnung
Genau in diesem Moment taucht normalerweise die Frage auf: „Moment, in YAML haben wir kebab-case, aber in Java sind die Felder camelCase. Wie wird das überhaupt gebunden?“ Und hier macht Spring Boot Ihnen ein Geschenk: Es hat das Konzept des relaxed binding – eine sanfte Namenszuordnung, die versteht, dass max-featured-count, max_featured_count und maxFeaturedCount im Wesentlichen derselbe Name in verschiedenen Stilen sind.
Zum Beispiel dieses YAML:
app:
catalog:
max-featured-count: 4
maintenance-mode: false
startup-report-enabled: true
Wird problemlos in diese Felder gebunden:
public class CatalogProperties {
private int maxFeaturedCount;
private boolean maintenanceMode;
private boolean startupReportEnabled;
// getters and setters
}
Das scheint eine Kleinigkeit zu sein, aber in der Praxis ist es ein riesiger Vorteil: YAML bleibt lesbar (kebab-case ist normalerweise augenfreundlicher), und Java bleibt das vertraute Java (camelCase).
Das Einzige, was ich als Disziplin empfehlen würde: Wählen Sie einen Stil in YAML (normalerweise kebab-case) und halten Sie sich daran. Boot wird Ihnen eine Mischung von Stilen verzeihen … aber Ihr Team (und Ihr zukünftiges Ich) nicht immer.
8. Typische Fehler beim Binden komplexer Strukturen
Fehler Nr. 1: Eine „Struktur“ als einen String speichern und manuell parsen.
Sehr oft versuchen Anfänger, den Preis als price: "19900 GBP" oder die Kursliste als courses: "spring-boot,spring-core,..." zu speichern und dann heldenhaft mit split zu zerlegen. Das wird schnell zu einem fragilen Format, das bei Leerzeichen, Kommas in Werten und beliebigen „zufälligen Verbesserungen“ der Konfiguration bricht. Ein verschachteltes Objekt und List lösen dieses Problem systematisch: Die Konfiguration bleibt eine Struktur, und der Code erhält Typen.
Fehler Nr. 2: List oder Map ohne generische Parameter verwenden.
In Java kann man (leider) private List courses; schreiben, und der Compiler zwingt einen nicht immer sofort zur Vernunft. Aber für das Binding ist das eine schlechte Idee: Sie verlieren den Elementtyp, und im Code beginnt eine „Cast-Party“. Geben Sie in Properties-Klassen immer explizit List<CourseItemProperties> und Map<CourseTrack, String> an, damit die Typen für Sie arbeiten.
Fehler Nr. 3: Liste und Map auf YAML-Ebene verwechseln.
Listen in YAML sind Gedankenstriche -, Maps sind key: value. Wenn Sie einmal den Einzug falsch machen, verwandelt YAML Ihre „Liste von Objekten“ in ein „einzelnes Objekt mit dem Schlüssel - slug“ (das klingt wie ein Witz, aber es ist ein echtes Problem). Bei solchen Fehlern kann die Anwendung entweder nicht starten oder mit unerwartet leeren Daten starten. Wenn plötzlich courses.size() gleich 0 ist – verdächtigen Sie als Erstes die YAML-Einzüge.
Fehler Nr. 4: Enum-Werte „irgendwie“ schreiben.
Ja, Boot ist oft recht nachsichtig, aber das ist kein Grund, die Konfiguration in einen Wettbewerb für künstlerische Darbietungen zu verwandeln. Wenn Sie CourseTrack.SPRING haben, schreiben Sie in der Konfiguration SPRING. Dann wird die Suche im Projekt einfach, und das Risiko eines „fast gleichen Werts“ sinkt.
Fehler Nr. 5: Keine Einheiten für Duration und DataSize angeben (oder sie inkonsistent angeben).
Die Zahl 30000 ohne Einheiten in der Konfiguration ist ein Rätsel. Ist es Zeit? Größe? Oder „eine Anzahl von etwas“? Schreiben Sie 30s, 5m, 64KB, 10MB. Erstens ist es lesbar. Zweitens reduziert es die Wahrscheinlichkeit eines Fehlers um Größenordnungen. Drittens bewahrt es Sie vor der Situation „wir dachten, es sind Sekunden, aber es waren Millisekunden“.
GO TO FULL VERSION