CodeGym /Kurse /Spring Boot /Bean Validation und Fail-Fast-Start

Bean Validation und Fail-Fast-Start

Spring Boot
Level 18, Lektion 2
Verfügbar

1. Type-Safe-Binding und Korrektheit der Konfiguration

Wenn Sie jemals eine Konfiguration wie max-featured-count: -999 gesehen haben, dann wissen Sie, dass „es ist ja eine Zahl!“ ein schwacher Trost ist. Type-Safe-Binding garantiert, dass der Wert in den gewünschten Typ konvertiert werden kann (int, boolean, LocalDate), aber es garantiert keinen Sinn. Und der Sinn ist das, was in der Realität normalerweise Anwendungen zerstört: ein leerer title, eine negative Kursdauer, eine fehlende Währung, null dort, wo der Code später „das gibt es definitiv“ erwartet.

Für den Übungs-catalog-service ist das besonders wichtig, weil wir keine Datenbank haben. Im Grunde ist die Konfiguration unsere „Quelle der Wahrheit“ für den Katalog. Wenn in einer Datenbank Daten normalerweise durch Constraints und Migrationen geschützt werden, dann liegt unsere „Datenbank“ in YAML. Also müssen wir genau hier eine „Schranke“ setzen: Schlechte Daten dürfen den Start nicht passieren.

Fail-Fast – das ist nicht „Härte um der Härte willen“. Es ist eine Zeit- und Nervenersparnis. Besser einen klaren Fehler beim Start zu bekommen, als später irgendwo in der Verarbeitung eines /api/catalog/featured-Requests einen NPE zu fangen und sich zu erinnern, wer und warum max-featured-count: 0 gesetzt hat (und warum das überhaupt nicht verboten wurde).

2. Bean Validation in Spring Boot

Wenn von „Validierung in Spring“ die Rede ist, denkt ein Anfänger oft automatisch an @Valid im Controller und an hübsche Fehlermeldungen wie „Feld ist erforderlich“. Aber Bean Validation ist ein allgemeinerer Mechanismus. Es ist ein Standard (in der Boot-4-Welt bereits im Namensraum jakarta.validation.*), der es erlaubt, Einschränkungen direkt neben den Daten zu beschreiben und dann das Objekt zentral auf die Einhaltung dieser Einschränkungen zu prüfen.

Im Kontext von @ConfigurationProperties funktioniert Bean Validation wie eine „Qualitätskontrolle am Eingang“: Boot bindet YAML an Ihr record-Objekt und durchläuft dann dieses Objekt mit dem Validator. Wenn Einschränkungen verletzt werden, sollte die Anwendung nicht erfolgreich starten. Und das ist logisch: Die Konfiguration ist die „Parameter zur Erstellung der Realität“ für die Anwendung. Wenn die Realität schief gebaut ist, ist es besser, gar nicht erst zu starten.

Es ist wichtig, die Grenze zu ziehen: Heute validieren wir die Anwendungskonfiguration, nicht eingehende HTTP-Anfragen. Die Validierung der Webschicht ist ein separates Thema, das seine eigenen Nuancen hat (und seine eigenen „Wie macht man es, dass der Kunde nicht weint“). Hier ist der Kunde jedoch Sie selbst und Ihr Deployment, und die Aufgabe ist sicherzustellen, dass der Dienst nicht so tut, als wäre er lebendig, wenn er fehlerhafte Einstellungen hat.

spring-boot-starter-validation einbinden

Damit Annotationen wie @NotBlank und @Positive nicht nur dekorative Aufkleber sind („ich hätte gern, dass das korrekt ist“), wird ein Validation-Provider auf dem Classpath benötigt. In Spring Boot ist das üblicherweise Hibernate Validator, das über den Starter spring-boot-starter-validation hereinkommt.

Beachten Sie die Philosophie von Boot: Sie fügen fast nie „ein kleines Jar für eine einzelne Annotation“ hinzu. Sie fügen einen Starter hinzu, der einen abgestimmten Satz von Abhängigkeiten und Defaults mitbringt. Genauso machen wir es auch hier: Wir fügen einen Starter hinzu – und erhalten eine funktionierende Validierung.

In build.gradle.kts sieht das so aus (die Version geben wir nicht an: sie wird durch das Boot BOM verwaltet):

dependencies {
    // Bean-Validation-Provider (normalerweise Hibernate Validator) + Spring Boot Auto-Konfiguration
    implementation("org.springframework.boot:spring-boot-starter-validation")
}

Wenn Sie diesen Schritt vergessen, können Sie einen halben Roman mit @NotNull und @NotBlank schreiben, aber dann wird es traurig: Die Anwendung wird die Konfiguration entweder nicht wie erwartet validieren, oder das Verhalten wird „seltsam“ sein, und Sie werden anfangen, Spring der Zauberei zu verdächtigen. In Wirklichkeit fehlt dann einfach die benötigte Abhängigkeit.

3. Validierung von @ConfigurationProperties

Jetzt stellen wir das „Einsatzkit“ zusammen. Wir brauchen zwei Dinge: Die Prüfung für eine bestimmte Properties-Klasse aktivieren und die Einschränkungen beschreiben. In Spring Boot wird die Aktivierung für @ConfigurationProperties meist über die Annotation @Validated (das ist eine Spring-Annotation aus org.springframework.validation.annotation.Validated) erreicht, und die Einschränkungen sind die standardmäßigen Bean-Validation-Annotationen aus jakarta.validation.constraints.

Nachfolgend betrachten wir nur den Teil von CatalogProperties, an dem man die Mechanik der Validierungsaktivierung am einfachsten sehen kann. Der vollständige Vertrag des Projekts ist umfangreicher, aber das Prinzip ändert sich dadurch nicht.

Minimalbeispiel: Erforderlicher title

Beginnen wir einfach. Der Katalogtitel darf nicht leer sein und nicht nur aus Leerzeichen bestehen. Für Strings ist das fast immer @NotBlank, nicht @NotNull.

package com.example.catalogservice.config;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.validation.annotation.Validated;

import jakarta.validation.constraints.NotBlank;

@Validated // Aktiviert die Validierung beim Binden der Konfiguration
@ConfigurationProperties("app.catalog") // Bindung an app.catalog.* in YAML
public record CatalogProperties(
        @NotBlank // Kein null/""/"   " erlaubt
        String title
) {}

Warum nicht @NotNull? Weil @NotNull " " nicht verbietet. Und wenn Sie jemals in einer Benutzeroberfläche einen Titel aus drei Leerzeichen gesehen haben, dann wissen Sie: Formal ist es ein String, aber inhaltlich ist es Leere und eine philosophische Frage „Was ist ein Titel?“.

Numerische Einschränkungen hinzufügen: @Positive, @Min

Im catalog-service gibt es Limits, zum Beispiel maxFeaturedCount. Diese Zahl muss strikt positiv sein. Es gibt zwei Möglichkeiten: @Min(1) oder @Positive. Beide sind geeignet, aber @Positive liest sich etwas „menschlicher“.

package com.example.catalogservice.config;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.bind.DefaultValue;
import org.springframework.validation.annotation.Validated;

import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Positive;

@Validated // Überprüft die Einschränkungen sofort beim Start der Anwendung
@ConfigurationProperties("app.catalog")
public record CatalogProperties(
        @NotBlank // title ist erforderlich und darf nicht aus Leerzeichen bestehen
        String title,

        // Standardwert, falls der Parameter nicht in YAML gesetzt ist
        @DefaultValue("4")
        @Positive // Explizit gesetzte 0 oder -1 muss die Konfiguration „ablehnen“
        int maxFeaturedCount
) {}

Hier sieht man eine interessante Kombination: Default und Validierung arbeiten zusammen. Wenn der Wert nicht gesetzt ist, wird 4 verwendet, und das besteht @Positive. Wenn der Wert explizit gesetzt ist, z.B. 0, wird der Default nicht verwendet und die Validierung muss die Konfiguration „ablehnen“.

4. Kaskadierende Validierung: @Valid für verschachtelte Objekte

In einer realen Konfiguration gibt es fast immer verschachtelte Strukturen. Bei uns ist das eine Liste von Kursen und innerhalb eines Kurses der price. Der häufigste Fehler von Anfängern: Sie hängen Einschränkungen an die verschachtelten records und wundern sich dann, dass nichts geprüft wird. Der Grund ist, dass Bean Validation standardmäßig nur die „oberste Ebene“ validiert. Damit der Validator „nach innen“ geht, wird @Valid benötigt.

Jetzt betrachten wir nicht das gesamte Wurzelobjekt, sondern den Teil, bei dem die Validierung besonders leicht fehlschlagen kann: die Liste der Kurse und der verschachtelte Preis.

Die gesamte Kursliste und jedes Element validieren

Die Kursliste im Übungsprojekt darf nicht leer sein (sonst wird der Dienst „Katalog“ zu einem philosophischen Konzept „Katalog ohne Elemente“). Dafür gibt es @NotEmpty. Und damit auch die Elemente der Liste geprüft werden, fügen wir @Valid hinzu.

package com.example.catalogservice.config;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.validation.annotation.Validated;

import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotEmpty;

import java.util.List;

@Validated // Aktiviert die Validierung beim Erstellen des Objekts aus YAML
@ConfigurationProperties("app.catalog")
public record CatalogProperties(
        @NotBlank // Menschenlesbarer Katalogtitel
        String title,

        @NotEmpty // Die Liste selbst darf nicht leer sein
        @Valid // Und jetzt gehen wir hinein und prüfen jedes CourseItem
        List<CourseItem> courses
) {}

Hier prüft @NotEmpty die Liste selbst (nicht null und nicht leer), und @Valid sagt: „Und jetzt prüfe jedes CourseItem darin“.

Felder von CourseItem und den verschachtelten Preis validieren

Wir erstellen das Konfigurationsmodell eines einzelnen Kurses. Hier bietet es sich an, die domänenspezifischen Enums CourseTrack und CourseLevel zu verwenden, die es im Projekt bereits gibt. Für sie gibt es bereits Typsicherheit (es kann kein „SPRONG“ statt „SPRING“ geben), aber null ist immer noch möglich, daher bleibt @NotNull nützlich.

package com.example.catalogservice.config;

import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Positive;

import java.time.LocalDate;

import com.example.catalogservice.catalog.domain.CourseLevel;
import com.example.catalogservice.catalog.domain.CourseTrack;

public record CourseItem(
        @NotBlank // Technischer Identifikator (nicht leer und nicht nur Leerzeichen)
        String slug,

        @NotBlank // Kursname für die UI
        String title,

        @Positive // Dauer muss streng > 0 sein
        int durationDays,

        @NotNull // In YAML kann das Feld fehlen => wird zu null
        CourseTrack track,

        @NotNull // Analog: Fehlen des Feldes in YAML führt zu null
        CourseLevel level,

        @NotNull // Startdatum ist erforderlich
        LocalDate launchDate,

        @NotNull // Das Preisobjekt muss vorhanden sein
        @Valid // Und wir müssen in MoneyProperties hineingehen und seine Felder prüfen
        MoneyProperties price
) {}

Und separat das Geld:

package com.example.catalogservice.config;

import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;

public record MoneyProperties(
        @Min(0) // „Kostenlos“ erlauben wir, negative Werte nicht
        long amount,

        @NotBlank // Währung darf kein leerer String / nur Leerzeichen sein
        String currency
) {}

Beachten Sie: @Valid steht auf price in CourseItem. Ohne dieses würde MoneyProperties zwar erstellt, aber seine internen Einschränkungen (currency und amount) könnten nicht kaskadierend geprüft werden – und Sie würden das Problem erst später entdecken, wenn Sie JSON serialisieren oder den Text in einer Startup-Zusammenfassung aufbauen.

Kurze „Karte“ der Einschränkungen für die Konfiguration

Um die Vorlesung nicht in ein Nachschlagewerk zu verwandeln, aber dennoch eine Orientierung zu geben, ist eine einfache Tabelle hilfreich. Dies ist keine Liste „von allem, was es gibt“, sondern eine grundlegende Sammlung, die für catalog-service ausreicht.

Was wird in der Konfiguration validiert Typ Üblich verwendet Was gilt als schlecht
Name (title, slug, currency) String @NotBlank null, "", " "
Numerisches Limit (maxFeaturedCount, durationDays) int / long @Positive oder @Min(…) 0, negative Werte
Erforderliches Enum (track, level) enum @NotNull null (fehlt in YAML)
Erforderliches Datum (launchDate) LocalDate @NotNull null
Kursliste (courses) List<…> @NotEmpty + @Valid null, leere Liste, Elemente mit schlechten Feldern
Verschachteltes Objekt (price) MoneyProperties @NotNull + @Valid fehlt vollständig oder enthält schlechte Felder

5. Fail-Fast-Start: Kaputte Konfiguration

Jetzt das Interessanteste: Was passiert genau, wenn die Konfiguration schlecht ist? Die Antwort: Die Anwendung sollte nicht starten. In der Praxis sieht das so aus: Boot lädt die Property-Quellen, führt das Binding in CatalogProperties durch, startet die Validierung, und wenn es Verstöße gibt, wird der Kontext nicht in einen fertigen Zustand „gerefresh“.

Man kann sich diesen Weg gut als Mini-Pipeline vorstellen:

flowchart TD
    A[application.yaml / catalog-data.yaml] --> B["Binder: @ConfigurationProperties"]
    B --> C[CatalogProperties record erstellt]
    C --> D["Bean Validation: constraints + @Valid"]
    D -->|OK| E[Context Refresh läuft weiter]
    D -->|Fehler| F[Start fehlgeschlagen: Anwendung nicht gestartet]

Um zu spüren, dass das keine Theorie ist, schauen wir uns ein Beispiel-YAML an, das „irgendwie nach Wahrheit aussieht“, aber in Wirklichkeit schlecht ist:

app:
  catalog:
    title: "   "
    max-featured-count: 0
    courses:
      - slug: ""
        title: "Spring Boot"
        duration-days: -1
        track: "SPRING"
        level: "BASIC"
        launch-date: "2026-04-01"
        price:
          amount: -10
          currency: ""

Hier gibt es mehrere Probleme auf einmal: title besteht aus Leerzeichen, max-featured-count ist null, slug ist leer, die Dauer ist negativ, der Preis ist negativ, die Währung ist leer. Wenn Sie keine Konfigurationsvalidierung haben, kann die Anwendung starten und dann an den unerwartetsten Stellen kaputtgehen. Mit Validierung wird sie sofort abstürzen und ziemlich ehrlich sagen, welche Felder gegen die Regeln verstoßen.

Und genau das ist Fail-Fast als Ingenieurswert. Wir versuchen nicht, die Konfiguration zur Laufzeit „zu heilen“ und tun nicht so, als ob „es irgendwie schon gehen wird“. Wir entscheiden uns für Vorhersagbarkeit: Entweder die Konfiguration ist gültig, oder der Dienst existiert nicht.

6. Invarianten und @AssertTrue

Einschränkungen auf Feldebene (@NotBlank, @Positive) lösen die meisten Probleme, aber manchmal bezieht sich eine Regel nicht auf einen einzelnen Wert, sondern auf eine Beziehung. Zum Beispiel: Wenn Sie ein Limit maxFeaturedCount haben, ist es logisch zu fordern, dass es die Anzahl der Kurse in der Konfiguration nicht überschreitet. Das ist keine „Einzelfeldvalidierung“ mehr, sondern eine Prüfung der Konsistenz des gesamten Modells – das nennen wir eine Invariante.

Für solche Fälle kann man ein einfaches, aber sehr verständliches Werkzeug verwenden – @AssertTrue. Es wird auf eine Methode (oder ein boolean-Feld) gesetzt, die true zurückgeben muss, sonst schlägt die Validierung fehl.

Wir nehmen einen lokalen Ausschnitt des Modells: nur die Felder, zwischen denen tatsächlich eine Abhängigkeit besteht.

package com.example.catalogservice.config;

import jakarta.validation.constraints.AssertTrue;

import java.util.List;

public record CatalogProperties(String title, int maxFeaturedCount, List<CourseItem> courses) {

    @AssertTrue(message = "maxFeaturedCount must be <= courses.size()") // Fehlermeldung bei Verletzung der Invariante
    public boolean isFeaturedLimitSane() {
        // Wenn courses überhaupt nicht vorhanden ist, soll das von genaueren @NotEmpty / @NotNull abgefangen werden
        if (courses == null) {
            return true;
        }

        // Wichtig: Invarianten sollten schnelle Prüfungen sein, ohne I/O und schwere Berechnungen
        return maxFeaturedCount <= courses.size();
    }
}

Auf diese Weise maskiert eine Cross-Field-Regel kein genaueres Problem wie eine fehlende Kursliste: Es ist ehrlicher, dieses an @NotEmpty / @NotNull zu delegieren, und die Invariante sollte genau die Konsistenz der bereits vorhandenen Daten prüfen.

Hier gibt es keine Magie: Die Methode gibt einen boolean zurück, Sie beschreiben die „Wahrheitsregel“. Ja, das ähnelt einem if, nur dass es als Teil des Konfigurationsvertrags formuliert ist. Und das ist praktisch: Die Regel lebt neben dem Modell und wird beim Start aktiviert, nicht in über das Projekt verstreuten Services.

Es ist wichtig zu bedenken, dass Invarianten sorgfältig geschrieben werden müssen. Wenn Sie innerhalb einer solchen Methode schwere Arbeit verrichten (Netzwerkaufrufe, Dateilesen, komplexe Berechnungen), verwandeln Sie die Konfigurationsvalidierung in eine Mini-Verarbeitung. Eine Invariante ist eine schnelle Prüfung der Datenkonsistenz.

7. Typische Fehler bei der Validierung von @ConfigurationProperties

Fehler Nr. 1: Einschränkungen setzen, aber spring-boot-starter-validation vergessen.
In diesem Moment beginnen die Annotationen an ein Schild „Nicht berühren – lebensgefährlich“ zu erinnern, das an Kabel geklebt ist, die nicht an Strom angeschlossen sind. Sie haben zwar @NotBlank geschrieben, aber es gibt keinen echten Validator in der Anwendung. Infolgedessen kann die Konfiguration durchkommen, und Sie warten auf Fail-Fast, bekommen aber Fail-Later (die ärgerlichste Art des Fehlschlags).

Fehler Nr. 2: @Validated auf dem root-@ConfigurationProperties vergessen.
Einschränkungen auf den Feldern können vorhanden sein, aber die Prüfung wird nicht speziell für das Binding der Konfiguration aktiviert. Im Kopf klingt das normalerweise so: „Aber ich habe doch @NotNull gesetzt! Warum hat es nicht funktioniert?“ Weil Sie Regeln beschrieben, aber Boot nicht gesagt haben: „Bitte wende sie beim Start auf dieses Objekt an.“

Fehler Nr. 3: In die Rekursion „Ich validiere alles, außer verschachtelten Objekten“ geraten, weil @Valid fehlt.
Das häufigste Szenario: CatalogProperties wird validiert, courses ist nicht leer – und Sie sind zufrieden. Aber innerhalb von CourseItem ist die Währung leer, der Betrag negativ, launchDate fehlt. Wenn es auf der Collection und den verschachtelten Objekten kein @Valid gibt, ist der Validator nicht verpflichtet, nach innen zu gehen. Infolgedessen erhalten Sie eine Illusion von Sicherheit: „die Wurzel wurde geprüft“, aber das Interessanteste blieb unkontrolliert.

Fehler Nr. 4: @NotNull und @NotBlank für Strings verwechseln.
@NotNull bedeutet „das Objekt existiert“. Für einen String reicht das nicht, denn " " ist nicht null, aber für den Benutzer (und für Sie in einer Woche) ist das fast immer Müll. Daher wird für Textparameter der Konfiguration meistens @NotBlank benötigt. Das ist ein kleines Detail, das eine große Klasse „seltsamer“ Probleme beseitigt.

Fehler Nr. 5: Einschränkungen zu streng machen und normale Szenarien blockieren.
Manchmal möchte man in der Hitze des Gefechts „wie in der Produktion“ alles verbieten, was nicht perfekt ist. In einem Übungsdienst führt das oft dazu, dass Sie die Anwendung nicht mit einer minimalen Konfiguration für die lokale Entwicklung starten können. Die Validierung sollte Invarianten schützen, aber nicht zu einer Bürokratie werden. Wenn ein Parameter wirklich optional ist, ist es besser, ihm einen @DefaultValue und eine weiche Einschränkung zu geben, als ihn immer zu verlangen.

1
Aufgabe
Spring Boot, Level 18, Lektion 2
Gesperrt
Fail-fast für ungültige Root-Eigenschaften
Fail-fast für ungültige Root-Eigenschaften
1
Aufgabe
Spring Boot, Level 18, Lektion 2
Gesperrt
Kaskadierende Validierung einer Liste und des verschachtelten Preises
Kaskadierende Validierung einer Liste und des verschachtelten Preises
Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION