Using annotations provided by Jakarta Bean Validation framework (the successor to JSR 349/380) is the the standard approach for bean validation in modern Java applications.. However, annotation-based validation isn't always sufficient for complex business logic.
For example, if an Order object contains a customerId that must be validated against a database, a pure annotation-based approach can become cumbersome. In these scenarios, a programmatic approach is often cleaner.
Spring 7 provides the flexibility to seamlessly combine Jakarta Validation annotations with Spring’s native org.springframework.validation.Validator interface.
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 look up in database.
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 7.0.3 (Spring Context)
Version Compatibility: 3.2.3.RELEASE - 7.0.3
- hibernate-validator 9.0.1.Final (Hibernate's Jakarta Validation reference implementation)
- expressly 6.0.0 (Jakarta Expression Language Implementation)
- JDK 25
- Maven 3.9.11
|