Vincoli del metodo con Bean Validation 2.0

1. Panoramica

In questo articolo, discuteremo come definire e convalidare i vincoli del metodo utilizzando Bean Validation 2.0 (JSR-380).

Nell'articolo precedente, abbiamo discusso JSR-380 con le sue annotazioni integrate e come implementare la convalida delle proprietà.

Qui, ci concentreremo sui diversi tipi di vincoli di metodo come:

  • vincoli a parametro singolo
  • parametro incrociato
  • vincoli di restituzione

Inoltre, daremo uno sguardo a come convalidare i vincoli manualmente e automaticamente utilizzando Spring Validator.

Per i seguenti esempi, abbiamo bisogno esattamente delle stesse dipendenze di Java Bean Validation Basics.

2. Dichiarazione dei vincoli del metodo

Per iniziare, discuteremo prima di come dichiarare i vincoli sui parametri del metodo e restituire i valori dei metodi .

Come accennato in precedenza, possiamo utilizzare annotazioni da javax.validation.constraints , ma possiamo anche specificare vincoli personalizzati (ad esempio per vincoli personalizzati o vincoli incrociati).

2.1. Vincoli a parametro singolo

La definizione di vincoli su singoli parametri è semplice. Dobbiamo semplicemente aggiungere annotazioni a ciascun parametro come richiesto :

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

Allo stesso modo, possiamo usare lo stesso approccio per i costruttori:

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. Utilizzo di vincoli tra parametri

In alcuni casi, potrebbe essere necessario convalidare più valori contemporaneamente, ad esempio, due importi numerici sono uno più grande dell'altro.

Per questi scenari, possiamo definire vincoli tra parametri personalizzati, che potrebbero dipendere da due o più parametri.

I vincoli tra parametri possono essere considerati come la convalida del metodo equivalente ai vincoli a livello di classe . Potremmo usarli entrambi per implementare la convalida basata su diverse proprietà.

Pensiamo a un semplice esempio: una variazione del metodo createReservation () della sezione precedente accetta due parametri di tipo LocalDate: una data di inizio e una data di fine.

Di conseguenza, vogliamo assicurarci che l' inizio sia nel futuro e che la fine sia dopo l' inizio . A differenza dell'esempio precedente, non possiamo definirlo utilizzando vincoli a parametro singolo.

Invece, abbiamo bisogno di un vincolo tra parametri.

In contrasto con i vincoli a parametro singolo, i vincoli tra parametri vengono dichiarati nel metodo o nel costruttore :

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

2.3. Creazione di vincoli tra parametri

Per implementare il vincolo @ConsistentDateParameters , sono necessari due passaggi.

Innanzitutto, dobbiamo definire l'annotazione del vincolo :

@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 {}; }

Qui, queste tre proprietà sono obbligatorie per le annotazioni dei vincoli:

  • messaggio: restituisce la chiave predefinita per la creazione di messaggi di errore, questo ci consente di utilizzare l'interpolazione dei messaggi
  • gruppi : ci consente di specificare gruppi di convalida per i nostri vincoli
  • payload : può essere utilizzato dai client dell'API Bean Validation per assegnare oggetti payload personalizzati a un vincolo

Per i dettagli su come definire un vincolo personalizzato, dai un'occhiata alla documentazione ufficiale.

Successivamente, possiamo definire la classe del validatore:

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

Come possiamo vedere, il metodo isValid () contiene l'effettiva logica di convalida. Innanzitutto, ci assicuriamo di ottenere due parametri di tipo LocalDate. Successivamente, controlliamo se entrambi sono nel futuro e la fine è dopo l' inizio .

Inoltre, è importante notare che l' annotazione @SupportedValidationTarget (ValidationTarget . PARAMETERS) sulla classe ConsistentDateParameterValidator è obbligatoria. Il motivo è perché @ConsistentDateParameter è impostato a livello di metodo, ma i vincoli devono essere applicati ai parametri del metodo (e non al valore restituito dal metodo, come vedremo nella sezione successiva).

Nota: la specifica Bean Validation consiglia di considerare validi i valori nulli . Se null non è un valore valido, è necessario utilizzare invece l' annotazione @NotNull.

2.4. Vincoli del valore restituito

A volte avremo bisogno di convalidare un oggetto come viene restituito da un metodo. Per questo, possiamo usare i vincoli del valore di ritorno.

L'esempio seguente usa vincoli incorporati:

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

Per getAllCustomers () , si applicano i seguenti vincoli:

  • Innanzitutto, l'elenco restituito non deve essere nullo e deve contenere almeno una voce
  • Inoltre, l'elenco non deve contenere voci nulle

2.5. Vincoli personalizzati valore restituito

In alcuni casi, potremmo anche aver bisogno di convalidare oggetti complessi:

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

In questo esempio, un oggetto Reservation restituito deve soddisfare i vincoli definiti da @ValidReservation , che definiremo in seguito.

Ancora una volta, dobbiamo prima definire l'annotazione del vincolo :

@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.

Se sei interessato a come utilizzare l' interfaccia ExecutableValidator , puoi dare un'occhiata alla documentazione ufficiale.

4. Conclusione

In questo tutorial, abbiamo dato una rapida occhiata a come utilizzare i vincoli del metodo con Hibernate Validator, inoltre abbiamo discusso alcune nuove funzionalità di JSR-380.

Innanzitutto, abbiamo discusso su come dichiarare diversi tipi di vincoli:

  • Vincoli a parametro singolo
  • Parametro incrociato
  • Restituire vincoli di valore

Abbiamo anche esaminato come convalidare i vincoli manualmente e automaticamente utilizzando Spring Validator.

Come sempre, il codice sorgente completo degli esempi è disponibile su GitHub.