Using JSR 349/380 annotations is the recommended way to perform bean validations. Although it's not always possible to use it for complex validation.
Let's say in our previous examples, Order object has an additional field, customerId, which has to be validated against the ids loaded from a database. In those kind of scenarios pure JSR 349 annotation based approach is not very suitable.
Spring provides enough flexibility to mix JSR 349/380 annotation with Spring's org.springframework.validation.Validator approaches.
Example
The object to be validated
package com.logicbig.example;
import jakarta.validation.constraints.DecimalMin;
import jakarta.validation.constraints.Future;
import jakarta.validation.constraints.NotNull;
import java.math.BigDecimal;
import java.util.Date;
public class Order {
@NotNull(message = "{date.empty}")
@Future(message = "{date.future}")
private Date date;
@NotNull(message = "{price.empty}")
@DecimalMin(value = "0", inclusive = false, message = "{price.invalid}")
private BigDecimal price;
String customerId;
.............
}
Implementing Spring Validator interface
package com.logicbig.example;
import org.springframework.validation.Errors;
import org.springframework.validation.ValidationUtils;
import org.springframework.validation.Validator;
public class OrderValidator implements Validator {
@Override
public boolean supports(Class<?> clazz) {
return Order.class == clazz;
}
@Override
public void validate(Object target, Errors errors) {
ValidationUtils.rejectIfEmpty(errors, "customerId", "customerId.empty");
Order order = (Order) target;
if (order.getCustomerId() != null) {
Customer customer = getCustomerById(order.getCustomerId());
if (customer == null) {
errors.reject("customer.id.invalid",
new Object[]{order.getCustomerId()},
"Customer id is not valid");
}
}
}
private Customer getCustomerById(String customerId) {
//just for test returning null.
// in real example the customer could be loop up in database etc.
return null;
}
}
A generic approach to loop up validator and apply validation
package com.logicbig.example;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.DefaultMessageSourceResolvable;
import org.springframework.context.support.ResourceBundleMessageSource;
import org.springframework.validation.BindingResult;
import org.springframework.validation.DataBinder;
import org.springframework.validation.ObjectError;
import org.springframework.validation.Validator;
import jakarta.validation.ConstraintViolation;
import java.awt.print.Book;
import java.util.Comparator;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
public class GenericValidator {
@Autowired
ApplicationContext context;
public boolean validateObject(Object objectToValidate) {
Map<String, Validator> validatorMap = context.getBeansOfType(Validator.class);
if (validatorMap == null) {
return true;
}
DataBinder binder = new DataBinder(objectToValidate);
//in this example two validators are register OrderValidator
// and LocalValidatorFactoryBean which will do JSR 349 validations.
for (Validator validator : validatorMap.values()) {
if (validator.supports(objectToValidate.getClass())) {
binder.addValidators(validator);
}
}
binder.validate();
BindingResult bindingResult = binder.getBindingResult();
if (bindingResult.hasErrors()) {
ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
messageSource.setBasename("ValidationMessages");
System.err.println(messageSource.getMessage("object.invalid",
new Object[]{objectToValidate.getClass().getSimpleName()}, Locale.US));
bindingResult.getAllErrors()
.stream()
.map(e -> messageSource.getMessage(e, Locale.US))
.sorted(Comparator.naturalOrder())
.forEach(System.err::println);
return false;
}
return true;
}
}
Validating Order object
package com.logicbig.example;
import org.springframework.beans.factory.annotation.Autowired;
public class ClientBean {
@Autowired
private GenericValidator genericValidator;
public void processOrder(Order order) {
if (genericValidator.validateObject(order)) {
System.out.println("processing " + order);
}
}
}
Spring Configuration
package com.logicbig.example;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.validation.Validator;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
@Configuration
public class Config {
@Bean
public ClientBean clientBean() {
return new ClientBean();
}
@Bean
public Validator validatorFactory() {
return new LocalValidatorFactoryBean();
}
@Bean
public OrderValidator orderValidator() {
return new OrderValidator();
}
@Bean
public GenericValidator genericValidator() {
return new GenericValidator();
}
}
Main class
package com.logicbig.example;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class ValidationMixedExample {
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new
AnnotationConfigApplicationContext(Config.class);
ClientBean clientBean = context.getBean(ClientBean.class);
Order order = new Order();
// order.setPrice(BigDecimal.TEN);
// order.setDate(new Date(System.currentTimeMillis() + 100000));
order.setCustomerId("111");
clientBean.processOrder(order);
}
}
OutputOrder has validation errors: Date cannot be empty No customer found with id 111 Price cannot be empty
Example ProjectDependencies and Technologies Used: - spring-context 6.1.2 (Spring Context)
Version Compatibility: 3.2.3.RELEASE - 6.1.2 Version compatibilities of spring-context with this example: Versions in green have been tested.
- hibernate-validator 8.0.1.Final (Hibernate's Jakarta Bean Validation reference implementation)
- expressly 5.0.0 (Jakarta Expression Language Implementation)
- JDK 17
- Maven 3.8.1
|