Validierung der funktionalen Endpunkte in Spring 5

Validierung für funktionale Endpunkte im Frühjahr 5

1. Überblick

Es ist oft nützlich, eine Eingabevalidierung für unsere APIs zu implementieren, um unerwartete Fehler später bei der Verarbeitung der Daten zu vermeiden.

Leider gibt es in Spring 5 keine Möglichkeit, Validierungen automatisch auf funktionalen Endpunkten auszuführen, wie dies bei annotierten Endpunkten der Fall ist. Wir müssen sie manuell verwalten.

Wir können jedoch einige nützliche Tools von Spring verwenden, um auf einfache und saubere Weise die Gültigkeit unserer Ressourcen zu überprüfen.

2. Spring-Validierungen verwenden

Beginnen wir mit der Konfiguration unseres Projekts mit einem funktionierenden funktionalen Endpunkt, bevor wir uns mit den tatsächlichen Validierungen befassen.

Stellen Sie sich vor, wir haben die folgendenRouterFunction:

@Bean
public RouterFunction functionalRoute(
  FunctionalHandler handler) {
    return RouterFunctions.route(
      RequestPredicates.POST("/functional-endpoint"),
      handler::handleRequest);
}

Dieser Router verwendet die von der folgenden Controller-Klasse bereitgestellte Handler-Funktion:

@Component
public class FunctionalHandler {

    public Mono handleRequest(ServerRequest request) {
        Mono responseBody = request
          .bodyToMono(CustomRequestEntity.class)
          .map(cre -> String.format(
            "Hi, %s [%s]!", cre.getName(), cre.getCode()));

        return ServerResponse.ok()
          .contentType(MediaType.APPLICATION_JSON)
          .body(responseBody, String.class);
    }
}

Wie wir sehen können, müssen wir in diesem funktionalen Endpunkt nur die Informationen formatieren und abrufen, die wir im Anforderungshauptteil erhalten haben, der alsCustomRequestEntity-Objekt strukturiert ist:

public class CustomRequestEntity {

    private String name;
    private String code;

    // ... Constructors, Getters and Setters ...

}

Dies funktioniert einwandfrei, aber stellen wir uns vor, wir müssen jetzt überprüfen, ob unsere Eingabe einigen vorgegebenen Einschränkungen entspricht, z. B. dass keines der Felder null sein kann und der Code mehr als 6 Stellen haben sollte.

Wir müssen einen Weg finden, um diese Behauptungen effizient und wenn möglich getrennt von unserer Geschäftslogik umzusetzen.

2.1. Validator implementieren

As it’s explained in this Spring Reference Documentation, we can use the Spring’s Validator interface to evaluate our resource’s values:

public class CustomRequestEntityValidator
  implements Validator {

    @Override
    public boolean supports(Class clazz) {
        return CustomRequestEntity.class.isAssignableFrom(clazz);
    }

    @Override
    public void validate(Object target, Errors errors) {
        ValidationUtils.rejectIfEmptyOrWhitespace(
          errors, "name", "field.required");
        ValidationUtils.rejectIfEmptyOrWhitespace(
          errors, "code", "field.required");
        CustomRequestEntity request = (CustomRequestEntity) target;
        if (request.getCode() != null && request.getCode().trim().length() < 6) {
            errors.rejectValue(
              "code",
              "field.min.length",
              new Object[] { Integer.valueOf(6) },
              "The code must be at least [6] characters in length.");
        }
    }
}

Wir werden nicht auf Details zuhow derValidator-Arbeit eingehen. Es reicht zu wissen, dass bei der Validierung eines Objekts alle Fehler erfasst werden -an empty error collection means that the object adheres to all our constraints.

Nachdem wir nun unsereValidator eingerichtet haben, müssen wir sie explizitvalidate  nennen, bevor wir unsere Geschäftslogik tatsächlich ausführen.

2.2. Ausführen der Validierungen

Zunächst können wir uns vorstellen, dass die Verwendung vonHandlerFilterFunction in unserer Situation geeignet wäre.

Wir müssen jedoch berücksichtigen, dass wir in diesen Filtern - genau wie in den Handlern - mitasynchronous constructions wieMono undFlux umgehen.

Dies bedeutet, dass wir Zugriff aufPublisher (Mono oderFlux) haben, jedoch nicht auf die Daten, die es eventuell bereitstellen wird.

Daher ist das Beste, was wir tun können, den Körper zu validieren, wenn wir ihn tatsächlich in der Handlerfunktion verarbeiten.

Lassen Sie uns fortfahren und unsere Handler-Methode ändern, einschließlich der Validierungslogik:

public Mono handleRequest(ServerRequest request) {
    Validator validator = new CustomRequestEntityValidator();
    Mono responseBody = request
      .bodyToMono(CustomRequestEntity.class)
      .map(body -> {
        Errors errors = new BeanPropertyBindingResult(
          body,
          CustomRequestEntity.class.getName());
        validator.validate(body, errors);

        if (errors == null || errors.getAllErrors().isEmpty()) {
            return String.format("Hi, %s [%s]!", body.getName(), body.getCode());
        } else {
            throw new ResponseStatusException(
              HttpStatus.BAD_REQUEST,
              errors.getAllErrors().toString());
        }
    });
    return ServerResponse.ok()
      .contentType(MediaType.APPLICATION_JSON)
      .body(responseBody, String.class);
}

Kurz gesagt, unser Service ruft jetzt die Antwort "Bad Request" ab, wenn der Text der Anfrage nicht unseren Einschränkungen entspricht.

Können wir sagen, dass wir unser Ziel erreicht haben? Nun, wir sind fast da. We’re running the validations, but there are many drawbacks in this approach.

Wir mischen die Validierungen mit der Geschäftslogik. Um die Sache noch schlimmer zu machen, müssen wir den obigen Code in jedem Handler wiederholen, in dem wir unsere Eingabevalidierung durchführen möchten.

Versuchen wir dies zu verbessern.

3. Arbeiten an einem DRY-Ansatz

To create a cleaner solution we’ll start by declaring an abstract class containing the basic procedure to process a request.

Alle Handler, die eine Eingabevalidierung benötigen, erweitern diese abstrakte Klasse, um ihr Hauptschema wiederzuverwenden, und folgen daher dem DRY-Prinzip (wiederholen Sie sich nicht).

Wir werden Generika verwenden, um es flexibel genug zu machen, um jeden Körpertyp und seinen jeweiligen Validator zu unterstützen:

public abstract class AbstractValidationHandler {

    private final Class validationClass;

    private final U validator;

    protected AbstractValidationHandler(Class clazz, U validator) {
        this.validationClass = clazz;
        this.validator = validator;
    }

    public final Mono handleRequest(final ServerRequest request) {
        // ...here we will validate and process the request...
    }
}

Codieren wir nun unserehandleRequest-Methode mit der Standardprozedur:

public Mono handleRequest(final ServerRequest request) {
    return request.bodyToMono(this.validationClass)
      .flatMap(body -> {
        Errors errors = new BeanPropertyBindingResult(
          body,
          this.validationClass.getName());
        this.validator.validate(body, errors);

        if (errors == null || errors.getAllErrors().isEmpty()) {
            return processBody(body, request);
        } else {
            return onValidationErrors(errors, body, request);
        }
    });
}

Wie wir sehen können, verwenden wir zwei Methoden, die wir noch nicht erstellt haben.

Definieren wir diejenige, die aufgerufen wird, wenn zuerst Validierungsfehler auftreten:

protected Mono onValidationErrors(
  Errors errors,
  T invalidBody,
  ServerRequest request) {
    throw new ResponseStatusException(
      HttpStatus.BAD_REQUEST,
      errors.getAllErrors().toString());
}

Dies ist jedoch nur eine Standardimplementierung, die von den untergeordneten Klassen leicht überschrieben werden kann.

Finally, we’ll set the processBody method undefined -we’ll leave it up to the child classes to determine how to proceed in that case:

abstract protected Mono processBody(
  T validBody,
  ServerRequest originalRequest);

In dieser Klasse sind einige Aspekte zu analysieren.

Bei der Verwendung von Generika müssen die untergeordneten Implementierungen zunächst explizit den erwarteten Inhaltstyp und den Validator angeben, der zur Bewertung verwendet wird.

Dies macht auch unsere Struktur robust, da es die Signaturen unserer Methoden einschränkt.

Zur Laufzeit weist der Konstruktor das eigentliche Validator-Objekt und die Klasse zu, mit der der Anforderungshauptteil umgewandelt wurde.

Wir können uns die komplette Klassehereansehen.

Lassen Sie uns nun sehen, wie wir von dieser Struktur profitieren können.

3.1. Anpassung unseres Handlers

Das erste, was wir tun müssen, ist natürlich, unseren Handler von dieser abstrakten Klasse zu erweitern.

Auf diese Weise könnenwe’ll be forced to use the parent’s constructor and to define how we’ll handle our request in the processBody method:

@Component
public class FunctionalHandler
  extends AbstractValidationHandler {

    private CustomRequestEntityValidationHandler() {
        super(CustomRequestEntity.class, new CustomRequestEntityValidator());
    }

    @Override
    protected Mono processBody(
      CustomRequestEntity validBody,
      ServerRequest originalRequest) {
        String responseBody = String.format(
          "Hi, %s [%s]!",
          validBody.getName(),
          validBody.getCode());
        return ServerResponse.ok()
          .contentType(MediaType.APPLICATION_JSON)
          .body(Mono.just(responseBody), String.class);
    }
}

Wie wir zu schätzen wissen, ist unser Child-Handler jetzt viel einfacher als der, den wir im vorherigen Abschnitt erhalten haben, da er es vermeidet, die tatsächliche Validierung der Ressourcen zu beeinträchtigen.

4. Unterstützung für Bean Validation API Annotations

Mit diesem Ansatz können wir auch die leistungsstarkenBean Validation’s annotations nutzen, die das Paketjavax.validation bietet.

Definieren wir beispielsweise eine neue Entität mit kommentierten Feldern:

public class AnnotatedRequestEntity {

    @NotNull
    private String user;

    @NotNull
    @Size(min = 4, max = 7)
    private String password;

    // ... Constructors, Getters and Setters ...
}

We can now simply create a new handler injected with the default Spring Validator provided by the LocalValidatorFactoryBean bean:

public class AnnotatedRequestEntityValidationHandler
  extends AbstractValidationHandler {

    private AnnotatedRequestEntityValidationHandler(@Autowired Validator validator) {
        super(AnnotatedRequestEntity.class, validator);
    }

    @Override
    protected Mono processBody(
      AnnotatedRequestEntity validBody,
      ServerRequest originalRequest) {

        // ...

    }
}

Wir müssen bedenken, dass wir, wenn andereValidator-Beans im Kontext vorhanden sind, diese möglicherweise explizit mit der Annotation@Primarydeklarieren müssen:

@Bean
@Primary
public Validator springValidator() {
    return new LocalValidatorFactoryBean();
}

5. Fazit

Zusammenfassend haben wir in diesem Beitrag gelernt, wie Sie Eingabedaten in den funktionalen Endpunkten von Spring 5 validieren.

Wir haben einen netten Ansatz entwickelt, um Validierungen elegant zu handhaben, indem wir vermieden haben, die Logik mit der Geschäftslogik zu vermischen.

Natürlich ist die vorgeschlagene Lösung möglicherweise nicht für jedes Szenario geeignet. Wir müssen unsere Situation analysieren und die Struktur wahrscheinlich an unsere Bedürfnisse anpassen.

Wenn wir das gesamte Arbeitsbeispiel sehen wollen, können wir es inour GitHub repo finden.