Method Constraints mit Bean Validation 2.0

Methodenbeschränkungen mit Bean Validation 2.0

1. Überblick

In diesem Artikel wird erläutert, wie Methodeneinschränkungen mit Bean Validation 2.0 (JSR-380) definiert und validiert werden.

Inthe previous article haben wir JSR-380 mit seinen integrierten Anmerkungen und der Implementierung der Eigenschaftsüberprüfung erläutert.

Hier konzentrieren wir uns auf die verschiedenen Arten von Methodeneinschränkungen wie:

  • Einzelparameter-Einschränkungen

  • Cross-Parameter

  • Rückgabeeinschränkungen

Außerdem erfahren Sie, wie Sie die Einschränkungen manuell und automatisch mit Spring Validator überprüfen.

Für die folgenden Beispiele benötigen wir genau die gleichen Abhängigkeiten wie inJava Bean Validation Basics.

2. Erklärung der Methodenbeschränkungen

Zu Beginnwe’ll first discuss how to declare constraints on method parameters and return values of methods.

Wie bereits erwähnt, können wir Anmerkungen vonjavax.validation.constraints verwenden, aber wir können auch benutzerdefinierte Einschränkungen angeben (z. g. für benutzerdefinierte Bedingungen oder parameterübergreifende Bedingungen).

2.1. Einzelparameter-Einschränkungen

Das Definieren von Einschränkungen für einzelne Parameter ist unkompliziert. We simply have to add annotations to each parameter as required:

public void createReservation(@NotNull @Future LocalDate begin,
  @Min(1) int duration, @NotNull Customer customer) {

    // ...
}

Ebenso können wir den gleichen Ansatz für Konstruktoren verwenden:

public class Customer {

    public Customer(@Size(min = 5, max = 200) @NotNull String firstName,
      @Size(min = 5, max = 200) @NotNull String lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }

    // properties, getters, and setters
}

2.2. Parameterübergreifende Einschränkungen verwenden

In einigen Fällen müssen möglicherweise mehrere Werte gleichzeitig validiert werden, z. B. zwei numerische Beträge, von denen einer größer als der andere ist.

Für diese Szenarien können benutzerdefinierte parameterübergreifende Einschränkungen definiert werden, die von zwei oder mehr Parametern abhängen können.

Cross-parameter constraints can be considered as the method validation equivalent to class-level constraints. Wir könnten beide verwenden, um die Validierung basierend auf mehreren Eigenschaften zu implementieren.

Lassen Sie uns ein einfaches Beispiel betrachten: Eine Variation dercreateReservation()-Methode aus dem vorherigen Abschnitt verwendet zwei Parameter vom TypLocalDate:, ein Startdatum und ein Enddatum.

Folglich möchten wir sicherstellen, dassbegin in der Zukunft liegt undend nachbegin liegt. Anders als im vorherigen Beispiel können wir dies nicht mit Einschränkungen für einzelne Parameter definieren.

Stattdessen benötigen wir eine parameterübergreifende Einschränkung.

Im Gegensatz zu Einzelparameter-Einschränkungen giltcross-parameter constraints are declared on the method or constructor:

@ConsistentDateParameters
public void createReservation(LocalDate begin,
  LocalDate end, Customer customer) {

    // ...
}

2.3. Parameterübergreifende Einschränkungen erstellen

Um die Einschränkung von@ConsistentDateParameterszu implementieren, benötigen wir zwei Schritte.

Zuerst müssen wirdefine the constraint annotation:

@Constraint(validatedBy = ConsistentDateParameterValidator.class)
@Target({ METHOD, CONSTRUCTOR })
@Retention(RUNTIME)
@Documented
public @interface ConsistentDateParameters {

    String message() default
      "End date must be after begin date and both must be in the future";

    Class[] groups() default {};

    Class[] payload() default {};
}

Hier sind diese drei Eigenschaften für Einschränkungsannotationen obligatorisch:

  • message – gibt den Standardschlüssel zum Erstellen von Fehlermeldungen zurück. Dadurch können wir die Nachrichteninterpolation verwenden

  • groups - ermöglicht es uns, Validierungsgruppen für unsere Einschränkungen anzugeben

  • payload - kann von Clients der Bean Validation API verwendet werden, um einer Einschränkung benutzerdefinierte Nutzdatenobjekte zuzuweisen

Weitere Informationen zum Definieren einer benutzerdefinierten Einschränkung finden Sie unterthe official documentation.

Danach können wir die Validator-Klasse definieren:

@SupportedValidationTarget(ValidationTarget.PARAMETERS)
public class ConsistentDateParameterValidator
  implements ConstraintValidator {

    @Override
    public boolean isValid(
      Object[] value,
      ConstraintValidatorContext context) {

        if (value[0] == null || value[1] == null) {
            return true;
        }

        if (!(value[0] instanceof LocalDate)
          || !(value[1] instanceof LocalDate)) {
            throw new IllegalArgumentException(
              "Illegal method signature, expected two parameters of type LocalDate.");
        }

        return ((LocalDate) value[0]).isAfter(LocalDate.now())
          && ((LocalDate) value[0]).isBefore((LocalDate) value[1]);
    }
}

Wie wir sehen können, enthält die MethodeisValid()die eigentliche Validierungslogik. Zuerst stellen wir sicher, dass wir zwei Parameter vom TypLocalDate. erhalten. Danach prüfen wir, ob beide in der Zukunft liegen undend nachbegin liegt.

Es ist auch wichtig zu beachten, dass die Annotation@SupportedValidationTarget(ValidationTarget.PARAMETERS)für die KlasseConsistentDateParameterValidatorerforderlich ist. Der Grund dafür ist, dass@ConsistentDateParameter auf Methodenebene festgelegt ist, die Einschränkungen jedoch auf die Methodenparameter angewendet werden sollen (und nicht auf den Rückgabewert der Methode, wie im nächsten Abschnitt erläutert wird).

Hinweis: In der Bean-Validierungsspezifikation wird empfohlen,null-Werte als gültig zu betrachten. Wennnull kein gültiger Wert ist, sollte stattdessen die Annotation@NotNull verwendet werden.

2.4. Rückgabewertbeschränkungen

Manchmal müssen wir ein Objekt validieren, wenn es von einer Methode zurückgegeben wird. Hierfür können wir Rückgabewertbeschränkungen verwenden.

Im folgenden Beispiel werden integrierte Einschränkungen verwendet:

public class ReservationManagement {

    @NotNull
    @Size(min = 1)
    public List<@NotNull Customer> getAllCustomers() {
        return null;
    }
}

FürgetAllCustomers() gelten die folgenden Einschränkungen:

  • Erstens darf die zurückgegebene Liste nichtnull sein und muss mindestens einen Eintrag haben

  • Darüber hinaus darf die Liste keinenull-Einträge enthalten

2.5. Rückgabewert Benutzerdefinierte Einschränkungen

In einigen Fällen müssen wir möglicherweise auch komplexe Objekte validieren:

public class ReservationManagement {

    @ValidReservation
    public Reservation getReservationsById(int id) {
        return null;
    }
}

In diesem Beispiel muss ein zurückgegebenesReservation-Objekt die durch@ValidReservation definierten Einschränkungen erfüllen, die wir als Nächstes definieren.

Wiederumwe first have to define the constraint annotation:

@Constraint(validatedBy = ValidReservationValidator.class)
@Target({ METHOD, CONSTRUCTOR })
@Retention(RUNTIME)
@Documented
public @interface ValidReservation {
    String message() default "End date must be after begin date "
      + "and both must be in the future, room number must be bigger than 0";

    Class[] groups() default {};

    Class[] payload() default {};
}

Danach definieren wir die Validator-Klasse:

public class ValidReservationValidator
  implements ConstraintValidator {

    @Override
    public boolean isValid(
      Reservation reservation, ConstraintValidatorContext context) {

        if (reservation == null) {
            return true;
        }

        if (!(reservation instanceof Reservation)) {
            throw new IllegalArgumentException("Illegal method signature, "
            + "expected parameter of type Reservation.");
        }

        if (reservation.getBegin() == null
          || reservation.getEnd() == null
          || reservation.getCustomer() == null) {
            return false;
        }

        return (reservation.getBegin().isAfter(LocalDate.now())
          && reservation.getBegin().isBefore(reservation.getEnd())
          && reservation.getRoom() > 0);
    }
}

2.6. Rückgabewert in Konstruktoren

Wie wir zuvorMETHOD undCONSTRUCTOR alstarget innerhalb unsererValidReservation-Schnittstelle definiert haben, giltwe can also annotate the constructor of Reservation to validate constructed instances:

public class Reservation {

    @ValidReservation
    public Reservation(
      LocalDate begin,
      LocalDate end,
      Customer customer,
      int room) {
        this.begin = begin;
        this.end = end;
        this.customer = customer;
        this.room = room;
    }

    // properties, getters, and setters
}

2.7. Kaskadierte Validierung

Schließlich können wir mit der Bean-Validierungs-API nicht nur einzelne Objekte, sondern auch Objektdiagramme mithilfe der sogenannten kaskadierten Validierung validieren.

Hence, we can use @Valid for a cascaded validation, if we want to validate complex objects. Dies funktioniert sowohl für Methodenparameter als auch für Rückgabewerte.

Nehmen wir an, wir haben eineCustomer-Klasse mit einigen Eigenschaftsbeschränkungen:

public class Customer {

    @Size(min = 5, max = 200)
    private String firstName;

    @Size(min = 5, max = 200)
    private String lastName;

    // constructor, getters and setters
}

Die KlasseReservationhat möglicherweise die EigenschaftCustomerowie weitere Eigenschaften mit Einschränkungen:

public class Reservation {

    @Valid
    private Customer customer;

    @Positive
    private int room;

    // further properties, constructor, getters and setters
}

Wenn wir jetztReservation als Methodenparameter referenzieren, giltwe can force the recursive validation of all properties:

public void createNewCustomer(@Valid Reservation reservation) {
    // ...
}

Wie wir sehen können, verwenden wir@Valid an zwei Stellen:

  • Beimreservation-Parameter: Löst die Validierung desReservation-Objekts aus, wenncreateNewCustomer() aufgerufen wird

  • Da wir hier ein verschachteltes Objektdiagramm haben, müssen wir demcustomer-Attribut auch ein@Valid hinzufügen. Dadurch wird die Validierung dieser verschachtelten Eigenschaft ausgelöst

Dies funktioniert auch für Methoden, die ein Objekt vom TypReservationzurückgeben:

@Valid
public Reservation getReservationById(int id) {
    return null;
}

3. Überprüfen von Methodenbeschränkungen

Nach der Deklaration der Einschränkungen im vorherigen Abschnitt können wir nun damit fortfahren, diese Einschränkungen tatsächlich zu validieren. Dafür haben wir mehrere Ansätze.

3.1. Automatische Validierung mit Spring

Spring Validation bietet eine Integration mit Hibernate Validator.

Hinweis: Spring Validation basiert auf AOP und verwendet Spring AOP als Standardimplementierung. Daher funktioniert die Validierung nur für Methoden, nicht jedoch für Konstruktoren.

Wenn wir nun möchten, dass Spring unsere Einschränkungen automatisch überprüft, müssen wir zwei Dinge tun:

Firstly, we have to annotate the beans, which shall be validated, with @Validated:

@Validated
public class ReservationManagement {

    public void createReservation(@NotNull @Future LocalDate begin,
      @Min(1) int duration, @NotNull Customer customer){

        // ...
    }

    @NotNull
    @Size(min = 1)
    public List<@NotNull Customer> getAllCustomers(){
        return null;
    }
}

Zweitens müssen wir eineMethodValidationPostProcessor-Bohne angeben:

@Configuration
@ComponentScan({ "org.example.javaxval.methodvalidation.model" })
public class MethodValidationConfig {

    @Bean
    public MethodValidationPostProcessor methodValidationPostProcessor() {
        return new MethodValidationPostProcessor();
    }
}

Der Container wirft jetzt einjavax.validation.ConstraintViolationException, wenn eine Einschränkung verletzt wird.

Wenn wir Spring Boot verwenden, registriert der Container eineMethodValidationPostProcessor-Bean für uns, solange sichhibernate-validator im Klassenpfad befindet.

3.2. Automatische Validierung mit CDI (JSR-365)

Ab Version 1.1 funktioniert Bean Validation mit CDI (Contexts and Dependency Injection for Java EE).

Wenn unsere Anwendung in einem Java EE-Container ausgeführt wird, überprüft der Container die Methodeneinschränkungen zum Zeitpunkt des Aufrufs automatisch.

3.3. Programmatische Validierung

Fürmanual method validation in a standalone Java application können wir die Schnittstellejavax.validation.executable.ExecutableValidator verwenden.

Wir können eine Instanz mit dem folgenden Code abrufen:

ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
ExecutableValidator executableValidator = factory.getValidator().forExecutables();

ExecutableValidator bietet vier Methoden:

  • validateParameters() undvalidateReturnValue() zur Methodenvalidierung

  • validateConstructorParameters() undvalidateConstructorReturnValue() für die Konstruktorvalidierung

Die Validierung der Parameter unserer ersten MethodecreateReservation() würde folgendermaßen aussehen:

ReservationManagement object = new ReservationManagement();
Method method = ReservationManagement.class
  .getMethod("createReservation", LocalDate.class, int.class, Customer.class);
Object[] parameterValues = { LocalDate.now(), 0, null };
Set> violations
  = executableValidator.validateParameters(object, method, parameterValues);

Hinweis: In der offiziellen Dokumentation wird davon abgeraten, diese Schnittstelle direkt aus dem Anwendungscode aufzurufen, sie jedoch über eine Methodenüberwachungstechnologie wie AOP oder Proxys zu verwenden.

Wenn Sie sich für die Verwendung derExecutableValidator-Schnittstelle interessieren, können Sie sich dieofficial documentation ansehen.

4. Fazit

In diesem Tutorial haben wir uns kurz mit der Verwendung von Methodeneinschränkungen in Hibernate Validator befasst und einige neue Funktionen von JSR-380 besprochen.

Zuerst haben wir besprochen, wie verschiedene Arten von Einschränkungen deklariert werden:

  • Einschränkungen für einzelne Parameter

  • Cross-Parameter

  • Rückgabewert-Einschränkungen

Wir haben uns auch angesehen, wie die Abhängigkeiten manuell und automatisch mit Spring Validator validiert werden.

Wie immer ist der vollständige Quellcode der Beispieleover on GitHub verfügbar.