Methodeneinschränkungen mit Bean Validation 2.0

1. Übersicht

In diesem Artikel wird erläutert, wie Sie Methodeneinschränkungen mithilfe von Bean Validation 2.0 (JSR-380) definieren und validieren.

Im vorherigen Artikel haben wir JSR-380 mit seinen integrierten Anmerkungen und die Implementierung der Eigenschaftsüberprüfung erläutert.

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

  • Einzelparameter-Einschränkungen
  • Parameterübergreifend
  • Rückgabebeschrä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 in den Grundlagen der Java Bean-Validierung.

2. Erklärung der Methodenbeschränkungen

Um damit zu beginnen, werden wir zunächst besprechen , wie Beschränkungen für Methodenparameter und Rückgabewerte von Methoden zu erklären .

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

2.1. Einzelparameter-Einschränkungen

Das Definieren von Einschränkungen für einzelne Parameter ist unkompliziert. Wir müssen einfach jedem Parameter nach Bedarf Anmerkungen hinzufügen :

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 wir möglicherweise mehrere Werte gleichzeitig validieren, z. B. zwei numerische Beträge, von denen einer größer als der andere ist.

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

Parameterübergreifende Einschränkungen können als Methodenvalidierung betrachtet werden, die Einschränkungen auf Klassenebene entspricht . Wir könnten beide verwenden, um eine Validierung basierend auf mehreren Eigenschaften zu implementieren.

Lassen Sie uns ein einfaches Beispiel betrachten: Eine Variation der Methode createReservation () aus dem vorherigen Abschnitt verwendet zwei Parameter vom Typ LocalDate: ein Startdatum und ein Enddatum.

Folglich möchten wir sicherstellen, dass der Beginn in der Zukunft liegt und das Ende nach dem Beginn liegt . Anders als im vorherigen Beispiel können wir dies nicht mit Einschränkungen einzelner Parameter definieren.

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

Im Gegensatz zu Einzelparameter- Einschränkungen werden für die Methode oder den Konstruktor parameterübergreifende Einschränkungen deklariert :

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

2.3. Parameterübergreifende Einschränkungen erstellen

Um die Einschränkung @ConsistentDateParameters zu implementieren , benötigen wir zwei Schritte.

Zuerst müssen wir die Einschränkungsanmerkung definieren :

@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änkungsanmerkungen obligatorisch:

  • message - Gibt den Standardschlüssel zum Erstellen von Fehlermeldungen zurück. Dadurch können wir die Nachrichteninterpolation verwenden
  • Gruppen - Ermöglicht die Angabe von Validierungsgruppen für unsere Einschränkungen
  • Payload - Kann von Clients der Bean Validation API verwendet werden, um einer Einschränkung benutzerdefinierte Payload-Objekte zuzuweisen

For details how to define a custom constraint, have a look at the official documentation.

After that, we can define the validator class:

@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]); } }

As we can see, the isValid() method contains the actual validation logic. First, we make sure that we get two parameters of type LocalDate. After that, we check whether both are in the future and end is after begin.

Also, it's important to notice that the @SupportedValidationTarget(ValidationTarget.PARAMETERS) annotation on the ConsistentDateParameterValidator class is required. The reason for this is because @ConsistentDateParameter is set on method-level, but the constraints shall be applied to the method parameters (and not to the return value of the method, as we'll discuss in the next section).

Note: the Bean Validation specification recommends to consider null-values as valid. If null isn't a valid value, the @NotNull-annotation should be used instead.

2.4. Return Value Constraints

Sometimes we'll need to validate an object as it is returned by a method. For this, we can use return value constraints.

The following example uses built-in constraints:

public class ReservationManagement { @NotNull @Size(min = 1) public List getAllCustomers() { return null; } }

For getAllCustomers(), the following constraints apply:

  • First, the returned list must not be null and must have at least one entry
  • Furthermore, the list must not contain null entries

2.5. Return Value Custom Constraints

In some cases, we might also need to validate complex objects:

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

In this example, a returned Reservation object must satisfy the constraints defined by @ValidReservation, which we'll define next.

Again, we 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 {}; }

After that, we define the validator class:

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. Return Value in Constructors

As we defined METHOD and CONSTRUCTOR as target within our ValidReservation interface before, we 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. Cascaded Validation

Finally, the Bean Validation API allows us to not only validate single objects but also object graphs, using the so-called cascaded validation.

Hence, we can use @Valid for a cascaded validation, if we want to validate complex objects. This works for method parameters as well as for return values.

Let's assume that we have a Customer class with some property constraints:

public class Customer { @Size(min = 5, max = 200) private String firstName; @Size(min = 5, max = 200) private String lastName; // constructor, getters and setters }

A Reservation class might have a Customer property, as well as further properties with constraints:

public class Reservation { @Valid private Customer customer; @Positive private int room; // further properties, constructor, getters and setters }

If we now reference Reservation as a method parameter, we can force the recursive validation of all properties:

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

As we can see, we use @Valid at two places:

  • On the reservation-parameter: it triggers the validation of the Reservation-object, when createNewCustomer() is called
  • As we have a nested object graph here, we also have to add a @Valid on the customer-attribute: thereby, it triggers the validation of this nested property

This also works for methods returning an object of type Reservation:

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

3. Validating Method Constraints

After the declaration of constraints in the previous section, we can now proceed to actually validate these constraints. For that, we have multiple approaches.

3.1. Automatic Validation With Spring

Spring Validation provides an integration with Hibernate Validator.

Note: Spring Validation is based on AOP and uses Spring AOP as the default implementation. Therefore, validation only works for methods, but not for constructors.

If we now want Spring to validate our constraints automatically, we have to do two things:

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 getAllCustomers(){ return null; } }

Secondly, we have to provide a MethodValidationPostProcessor bean:

@Configuration @ComponentScan({ "org.baeldung.javaxval.methodvalidation.model" }) public class MethodValidationConfig { @Bean public MethodValidationPostProcessor methodValidationPostProcessor() { return new MethodValidationPostProcessor(); } }

The container now will throw a javax.validation.ConstraintViolationException, if a constraint is violated.

If we are using Spring Boot, the container will register a MethodValidationPostProcessor bean for us as long as hibernate-validator is in the classpath.

3.2. Automatic Validation With CDI (JSR-365)

As of version 1.1, Bean Validation works with CDI (Contexts and Dependency Injection for Jakarta EE).

If our application runs in a Jakarta EE container, the container will validate method constraints automatically at the time of invocation.

3.3. Programmatic Validation

For manual method validation in a standalone Java application, we can use the javax.validation.executable.ExecutableValidator interface.

We can retrieve an instance using the following code:

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

ExecutableValidator offers four methods:

  • validateParameters() and validateReturnValue() for method validation
  • validateConstructorParameters() and validateConstructorReturnValue() for constructor validation

Validating the parameters of our first method createReservation() would look like this:

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);
    

Note: The official documentation discourages to call this interface directly from the application code, but to use it via a method interception technology, like AOP or proxies.

Wenn Sie an der Verwendung der ExecutableValidator- Schnittstelle interessiert sind , können Sie sich die offizielle Dokumentation ansehen.

4. Fazit

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

Zuerst haben wir diskutiert, wie verschiedene Arten von Einschränkungen deklariert werden können:

  • Einzelparameter-Einschränkungen
  • Parameterübergreifend
  • Rückgabewertbeschränkungen

Wir haben uns auch angesehen, wie die Einschränkungen manuell und automatisch mit Spring Validator überprüft werden.

Wie immer ist der vollständige Quellcode der Beispiele auf GitHub verfügbar.