Dynamische Validierung mit Spring Boot Validation

Keine Kommentare

Serverseitige Validierung ist nicht nur ein Mittel, um eventuelle Angriffe auf ein System abzufangen, sie hilft auch, die Datenqualität sicherzustellen. Im Java-Umfeld wurde uns Entwicklern mit JSR 303 Bean Validation und den javax.validation Packages ein standardisiertes Mittel für diese Aufgabe an die Hand gegeben. Felder, die bestimmten Kriterien genügen müssen, werden mit den entsprechenden Annotationen versehen, wie z.B. @NotNull, und diese werden dann vom Framework ausgewertet. Natürlich gibt es auch die Möglichkeit, eigene Annotationen und Validatoren zu schreiben, um spezifischere Dinge zu prüfen.

Das Spring Framework verfügt über eine gute Integration der Bean Validation. So ist es z.B. möglich, in einem RestController den eingehenden Request mit @Valid zu annotieren, um sicherzustellen, dass das geschickte Objekt valide ist. Ein einfaches Beispiel wäre folgender Controller:

@RestController
public class DataController {
    @RequestMapping(value = "/input", method = RequestMethod.POST)
    public ResponseEntity<?> acceptInput(@Valid @RequestBody Data data ) {
        dataRepository.save(data);
        return new ResponseEntity<>(HttpStatus.OK);
    }
}

Das hier sehr generische „Data“-Objekt wurde, wenn diese Methode aufgerufen wird, bereits komplett vom Framework validiert. Sollte ein Feld nicht valide sein, würde der Aufrufer direkt einen 4xx Statuscode erhalten.

Einen Nachteil bringen die Validierungen leider mit sich: Die Annotationen sind komplett statisch. Sie können keine Informationen aus z.B. dem Request lesen.
Nichtsdestotrotz gibt es Mittel und Wege, diese Einschränkung zu überwinden und die eigene Anwendung mit dynamischeren Validierungen anzureichern.
Konkret geht es darum, einen oder mehrere Werte aus dem HttpRequest zu extrahieren und die Validierung je nach Wert zu variieren.

Dynamischere Validierung

Auf einer bekannten Social-Media-Plattform ging vor einer Weile ein Scherz rum, welcher sich auf die Zeichenbegrenzung bezog. Die Zusammenfassung ist sehr schön in diesem Bild dargestellt.

Unser Beispiel soll genau darauf basieren. Geht in unserer Beispielanwendung ein Request ein, welcher im Header de-DE als Sprache gesetzt hat, soll der Text im JSON maximal 280 Zeichen lang sein. Für jede andere Sprache sollen weiterhin 140 Zeichen das Limit sein.
Um das Zusammenspiel mit der statischen Validierung zu zeigen, erhält unser Datenobjekt auch ein Zahlenfeld, welches validiert wird. Konkret sieht das Objekt wie folgt aus:

public class Data {
    @NotNull
    private final String someStringValue;
    @Min(1)
    private final int someIntValue;
 
    @JsonCreator
    public Data(@JsonProperty("someStringValue") String someStringValue, @JsonProperty("someIntValue") int someIntValue) {
        this.someStringValue = someStringValue;
        this.someIntValue = someIntValue;
    }
 
    public String getSomeStringValue() {
        return someStringValue;
    }
 
    public int getSomeIntValue() {
        return someIntValue;
    }
}

Die JSON-Annotationen stammen allesamt von Jackson und sind praktischerweise im Spring Boot Starter Web schon integriert. Das Feld someStringValue, welches bereits eine Annotation trägt, soll von uns noch auf die Einhaltung des Zeichenlimits geprüft werden.

Für die Validierung benötigen wir eine eigene Klasse, welche die Validierungslogik enthält:

@Component
public class StringValueValidator {
 
    public void validate(String language, Data data, Errors errors) {
        if (!"de-DE".equals(language)) {
            if (data.getSomeStringValue().length() > 140) {
                errors.reject("someStringValue");
            }
        }
    }
}

An dieser Stelle soll noch einmal betont werden, dass der Validator keinerlei Interface von javax.validation, wie z.B. javax.xml.validation.Validator, erweitert oder implementiert. Da unsere Validierung von Werten aus dem Request abhängt, soll diese erst später ausgeführt werden als die restliche Validierung. Dennoch wollen wir nicht auf die existierenden Prüfungen (@NotNull und @Min) verzichten. Abgesehen von der @Component-Annotation handelt es beim StringValueValidator somit um ein POJO.

Errors stammen von Spring und sind voll qualifiziert org.springframework.validation.Errors. Wie zu sehen ist, schreiben wir in diese bei negativer Prüfung, welches Feld zu Problemen geführt hat. Es ist auch möglich, sprechendere Fehlermeldung dort hinein zu schreiben.

Im Controller genügt nun nicht einfach mehr die @Valid-Annotation. Die Methode benötigt nun zusätzlich alle bisherigen Fehler als Parameter. Indem wir Errors  in die Parameterliste mit aufnehmen, erkennt Spring bereits, dass es den Request nicht sofort ablehnen soll und gibt uns alle bisherigen Validierungsfehler. Hier ist zu beachten, dass wir nun selbst dafür verantwortlich sind, dem User einen 4xx Statuscode zu liefern. Spring nimmt uns das nun nicht mehr ab, es sei denn wir werfen eine Exception.

Neben den Fehlern lassen wir Spring gleich die Sprache aus dem Header extrahieren. Wir könnten auch auf den HttpRequest zugreifen, aber so sparen wir uns etwas Arbeit. Die Sprache, die Daten und die bisherigen Fehler geben wir dann an unseren StringValueValidator. Die Request-Methode sieht dann folgendermaßen aus:

    @RequestMapping(value = "/validation", method = RequestMethod.POST)
    public ResponseEntity<?> acceptData(@Valid @RequestBody Data data, Errors errors, 
        @RequestHeader(HttpHeaders.ACCEPT_LANGUAGE) String language) {
        stringValueValidator.validate(language, data, errors);
        if (errors.hasErrors()) {
            return new ResponseEntity<>(createErrorString(errors), HttpStatus.BAD_REQUEST);
        }
        return new ResponseEntity<>(HttpStatus.OK);
    }

Schon haben wir eine dynamische Validierung, welche sich dem entsprechenden Request anpasst. Natürlich ist die Sprache hier nur ein Beispiel für einen Wert der aus dem Request kommen kann. Die Validierung kann genauso gut auf dem URL oder anderen Werten aus dem Request basieren.

Interessant an dieser Stelle ist, dass man denken würde, der Validator könnte doch eine RequestScoped Bean sein, in welche man die Werte injeziert. Leider war es mir nicht möglich, diesen Ansatz zum Laufen zu bekommen. Es blieb immer der alte Request „hängen“, so dass ein Test mit zwei Requests immer fehlschlug.

Das komplette Beispiel inklusive Unit Tests steht auf GitHub bereit: https://github.com/rbraeunlich/spring-boot-additional-validation

Fazit

Wie gezeigt wurde, lässt sich mit einfachen Mitteln die Validierung von Feldern um dynamische Aspekte erweitern. Nicht nur das, wir konnten sogar unsere erweiterte Validierung mit der bisherigen kombinieren, ohne dass wir Einschränkungen hinnehmen mussten. Besonders komplexere Validierungen, welche sich nicht über reine Annotationen abbilden lassen, können so einfach im RestController vorgenommen werden.

Ronny Bräunlich

Ronny arbeitet seit Mai 2017 bei der codecentric AG. Er ist überzeugter Anhänger von TDD und arbeitet meist im JVM-Ökosystem.

Share on FacebookGoogle+Share on LinkedInTweet about this on TwitterShare on RedditDigg thisShare on StumbleUpon

Kommentieren

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.