Ограничения на метода с валидиране на Bean 2.0

1. Общ преглед

В тази статия ще обсъдим как да дефинираме и проверим ограниченията на метода, използвайки Bean Validation 2.0 (JSR-380).

В предишната статия обсъдихме JSR-380 с вградените анотации и как да приложим проверка на свойствата.

Тук ще се съсредоточим върху различните видове ограничения на методите като:

  • еднопараметрични ограничения
  • кръстосан параметър
  • връщане на ограничения

Също така ще разгледаме как да проверяваме ограниченията ръчно и автоматично с помощта на Spring Validator.

За следващите примери ни трябват точно същите зависимости, както в Основите за проверка на Java Bean.

2. Декларация за ограничения на метода

За да започнем, първо ще обсъдим как да декларираме ограничения върху параметрите на метода и връщаните стойности на методите .

Както споменахме по-горе, можем да използваме анотации от javax.validation.constraints , но можем да посочим и персонализирани ограничения (например за персонализирани ограничения или ограничения за кръстосани параметри).

2.1. Ограничения с единичен параметър

Дефинирането на ограничения върху единични параметри е лесно. Ние просто трябва да добавим пояснения към всеки параметър, както се изисква :

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

По същия начин можем да използваме същия подход за конструктори:

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. Използване на ограничения за кръстосани параметри

В някои случаи може да се наложи да проверим няколко стойности наведнъж, например две цифрови суми са една по-голяма от другата.

За тези сценарии можем да дефинираме ограничения по отношение на различни параметри, които могат да зависят от два или повече параметъра.

Ограниченията за кръстосани параметри могат да се разглеждат като валидиране на метода, еквивалентно на ограниченията на ниво клас . Бихме могли да използваме и двете, за да реализираме валидиране въз основа на няколко свойства.

Нека помислим за един прост пример: вариант на метода createReservation () от предишния раздел взема два параметъра от типа LocalDate: начална дата и крайна дата.

Следователно искаме да сме сигурни, че начало е в бъдещето, а краят е след начало . За разлика от предишния пример, ние не можем да дефинираме това с помощта на ограничения за единични параметри.

Вместо това се нуждаем от ограничение за кръстосани параметри.

За разлика от ограниченията с един параметър, ограниченията за кръстосани параметри се декларират в метода или конструктора :

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

2.3. Създаване на ограничения за различни параметри

За да приложим ограничението @ConsistentDateParameters , са ни необходими две стъпки.

Първо, трябва да дефинираме анотацията на ограничението :

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

Тук тези три свойства са задължителни за анотации на ограничения:

  • message - връща ключа по подразбиране за създаване на съобщения за грешки, това ни позволява да използваме интерполация на съобщенията
  • групи - позволява ни да посочим групи за валидиране за нашите ограничения
  • полезен товар - може да се използва от клиенти на Bean Validation API за присвояване на персонализирани обекти на полезен товар на ограничение

За подробности как да дефинирате персонализирано ограничение, погледнете официалната документация.

След това можем да дефинираме класа на валидатора:

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

Както виждаме, методът isValid () съдържа действителната логика за валидиране. Първо се уверяваме, че получаваме два параметъра от типа LocalDate. След това проверяваме дали и двете са в бъдещето и края е след начало .

Също така е важно да забележите, че е необходима анотацията @SupportedValidationTarget (ValidationTarget . PARAMETERS) в класа ConsistentDateParameterValidator . Причината за това е, защото @ConsistentDateParameter е зададен на ниво метод, но ограниченията ще бъдат приложени към параметрите на метода (а не към възвръщаемата стойност на метода, както ще обсъдим в следващия раздел).

Забележка: Спецификацията за проверка на Bean препоръчва да се считат за нулеви стойности като валидни. Ако null не е валидна стойност, вместо това трябва да се използва @ NotNull- анотация.

2.4. Ограничения за връщаната стойност

Понякога ще трябва да проверим обект, тъй като той се връща от метод. За това можем да използваме ограничения на връщаната стойност.

Следващият пример използва вградени ограничения:

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

За getAllCustomers () се прилагат следните ограничения:

  • Първо, върнатият списък не трябва да е нула и трябва да има поне един запис
  • Освен това списъкът не трябва да съдържа нулеви записи

2.5. Персонализирани ограничения на върнатата стойност

В някои случаи може да се наложи да проверим и сложни обекти:

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

В този пример върнат обект на резервация трябва да отговаря на ограниченията, дефинирани от @ValidReservation , които ще дефинираме по-нататък.

Отново, първо трябва да дефинираме анотацията на ограничението :

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

В случай, че се интересувате как да използвате интерфейса ExecutableValidator , можете да разгледате официалната документация.

4. Заключение

В този урок имахме бърз поглед как да използваме ограниченията на метода с Hibernate Validator, също така обсъдихме някои нови функции на JSR-380.

Първо обсъдихме как да декларираме различни видове ограничения:

  • Ограничения за единични параметри
  • Кръстосан параметър
  • Връщане на ограничения на стойността

Също така разгледахме как да проверяваме ограниченията ръчно и автоматично с помощта на Spring Validator.

Както винаги, пълният изходен код на примерите е достъпен в GitHub.