Spring Bean Validation
Data validation is not a new topic in web application development.
We take a brief look at data validation in the Java ecosystem in general and the Spring Framework specifically. The Java platform has been the de facto standard for implementing data validation that is Bean Validation specification. Bean Validation specification has several versions:
- 1.0 (JSR-303),
- 1.1 (JSR-349),
- 2.0 (JSR 380) – the latest version
This specification defines a set of components, interfaces, and annotations. This provides a standard way to put constraints to the parameters and return values of methods and parameters of constructors, provide API to validate objects and object graphs.
A Declarative model is used to put constraints in the form of annotations on objects and their fields. There are predefined annotations like @NotNull
, @Digits
, @Pattern
, @Email
, @CreditCard
. There is an ability to create new custom constraints.
Validation can run manually or more naturally, when other specification and frameworks validate data at the right time, for example, user input, insert or update in JPA.
Validation in Java Example
Let’s take a look at how it can be done in practice in this simple Bean Validation example inside regular Java application.
We have an object that we want to validate with all fields annotated with constraints.
public class SimpleDto {
@Min(value = 1, message = "Id can't be less than 1 or bigger than 999999")
@Max(999999)
private int id;
@Size(max = 100)
private String name;
@NotNull
private Boolean active;
@NotNull
private Date createdDatetime;
@Pattern(regexp = "^asc|desc$")
private String order = "asc";
@ValidCategory(categoryType="simpleDto")
private String category;
…
Constructor, getters and setters
Note: @Min
, @Max
, @Size
, @NotNull
, and @Pattern
are standard annotations, and @ValidCategory
is custom.
Now we can use it in a simple Java application and manually validate the object.
public class SimpleApplication {
public static void main(String[] args) {
final SimpleDto simpleDto = new SimpleDto();
simpleDto.setId(-1);
simpleDto.setName("Test Name");
simpleDto.setCategory("simple");
simpleDto.setActive(true);
simpleDto.setOrder("asc");
simpleDto.setCreatedDatetime(new Date());
ValidatorFactory validatorFactory = Validation.buildDefaultValidatorFactory();
Validator validator = validatorFactory.usingContext().getValidator();
Set constrains = validator.validate(simpleDto);
for (ConstraintViolation constrain : constrains) {
System.out.println(
"[" + constrain.getPropertyPath() + "][" + constrain.getMessage() + "]"
);
}
}
}
And result in Console will be:
“[id] [Id can't be less than 1 or bigger than 999999]”
Validation Constraint and Annotation
Create a custom validation constraint and annotation.
@Retention(RUNTIME)
@Target(FIELD)
@Constraint(validatedBy = {ValidCategoryValidator.class})
public @interface ValidCategory {
String categoryType();
String message() default "Category is not valid";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
And constraint validation implementation:
public class ValidCategoryValidator implements ConstraintValidator<ValidCategory, String> {
private static final Map<String, List> availableCategories;
static {
availableCategories = new HashMap<>();
availableCategories.put("simpleDto", Arrays.asList("simple", "advanced"));
}
private String categoryType;
@Override
public void initialize(ValidCategory constraintAnnotation) {
this.setCategoryType(constraintAnnotation.categoryType());
}
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
List categories = ValidCategoryValidator.availableCategories.get(categoryType);
if (categories == null || categories.isEmpty()) {
return false;
}
for (String category : categories) {
if (category.equals(value)) {
return true;
}
}
return false;
}
}
In the example above, available categories come from a simple hash map. In real application use cases, they can be retrieved from the database or any other services.
Please note that constraints and validations could be specified and performed not only on the field level, but also on an entire object.
When we need to validate various field dependencies, for example, start date, it can’t be after the end date.
The most widely used implementations of Bean Validation specifications are Hibernate Validator and Apache BVal.
Validation with Spring
The Spring framework provides several features for validation.
- Support for Bean Validation API versions 1.0, 1.1 (JSR-303, JSR-349) was introduced in Spring Framework starting with version 3.
- Spring has its own Validator interface that is very basic and can be set in specific DataBinder instance. This could be useful for implementing validation logic without annotations.
Bean Validation with Spring
Spring Boot provides validation started which can be included in the project:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
This starter provides a version of Hibernate Validator compatible with the current Spring Boot.
Using Bean Validation, we could validate a request body, query parameters, variables within the path (e.g. / /simpledto/{id}), or any method or constructor parameters.
POST or PUT Requests
In POST or PUT requests, for example, we pass JSON payload, Spring automatically converts it into Java object and now we want to validate resulting object. Let’s use SimpleDto
object from the 1st example:
@RestController
@RequestMapping("/simpledto")
public class SimpleDtoController {
@Autowired
private SimpleDtoService simpleDtoService;
@RequestMapping(path = "", method = RequestMethod.POST, produces =
"application/json")
public SimpleDto createSimpleDto(
@Valid @RequestBody SimpleDto simpleDto) {
SimpleDto result = simpleDtoService.save(simpleDto);
return result;
}
}
We just added @Valid
annotation to the SimpleDto
parameter annotated with @RequestBody
. This will tell Spring to process validation before making an actual method call. In case validation fails, Spring will throw a MethodArgument NotValidException
which, by default, will return a 400 (Bad Request) response.
Validating Path Variables
Validating Path Variables works a little differently. The problem is that now we have to add constraint annotations directly to method parameters instead of inside of objects.
To make this work there are 2 possible options:
Option 1: @Validated Annotation
Add @Validated
annotation to the controller at the class level to evaluate constraint annotations on method parameters.
Option 2: Path Variable
Use an object that represents the path variable as seen in the example below:
@RestController
@RequestMapping("/simpledto")
public class SimpleDtoController {
@Autowired
private SimpleDtoService simpleDtoService;
@RequestMapping(path = "/{simpleDtoId}", method = RequestMethod.GET, produces =
"application/json")
public SimpleDto getSimpleDto(
@Valid SimpleDtoIdParam simpleDtoIdParam) {
SimpleDto result = simpleDtoService.findById(simpleDtoIdParam.getSimpleDtoId());
if (result == null) {
throw new NotFoundException();
}
return result;
}
}
In this case, we have SimpleDtoIdParam
class that contains the simpleDtoId
field which will be validated against a standard or custom Bean constraint annotation. Path Variable name (/{simpleDtoId}) should be the same as the field name (so Spring will be able to find the setter for this field).
private static final long serialVersionUID = -8165488655725668928L;
@Min(value = 1)
@Max(999999)
private int simpleDtoId;
public int getSimpleDtoId() {
return simpleDtoId;
}
public void setSimpleDtoId(int simpleDtoId) {
this.simpleDtoId = simpleDtoId;
}
}
In contrast to Request Body validation, Path Variable validation throws ConstraintViolationException
instead of MethodArgumentNotValidException
. Therefore, we will need to create a custom exception handler.
The Java Spring framework also allows validating parameters on a service level with @Validated
annotation (class level) and @Valid
(parameter level).
Considering Spring JPA is using Hibernate underneath, it supports Bean Validation for entity classes as well. Please note that it’s probably not a good idea in many cases rely on this level of validation since it means that all logic before was dealing with invalid objects.
Spring Validation Interface
Spring defines its own interface for validation Validator (org.springframework.validation.Validator). It can be set for a specific DataBinder instance and implement validation without annotations (non-declarative approach).
To implement this approach we would need to:
- Implement the Validator Interface
- Add Validator
Implement Validator Interface
Implement the Validator interface, for example lets work with our SimpleDto
class:
@Component
public class SpringSimpleDtoValidator implements Validator {
@Override
public boolean supports(Class<?> clazz) {
return SimpleDto.class.isAssignableFrom(clazz);
}
@Override
public void validate(Object target, Errors errors) {
if (errors.getErrorCount() == 0) {
SimpleDto param = (SimpleDto) target;
Date now = new Date();
if (param.getCreatedDatetime() == null) {
errors.reject("100",
"Create Date Time can't be null");
} else if (now.before(param.getCreatedDatetime())) {
errors.reject("101",
"Create Date Time can't be after current date time");
}
}
}
}
Check here if the created Datetime
timestamp is in the future.
Add Validator
Add Validator implementation to DataBinder
:
@RestController
@RequestMapping("/simpledto")
public class SimpleDtoController {
@Autowired
private SimpleDtoService simpleDtoService;
@Autowired
private SpringSimpleDtoValidator springSimpleDtoValidator;
@InitBinder("simpleDto")
public void initMerchantOnlyBinder(WebDataBinder binder) {
binder.addValidators(springSimpleDtoValidator);
}
@RequestMapping(path = "", method = RequestMethod.POST, produces =
"application/json")
public SimpleDto createSimpleDto(
@Valid @RequestBody SimpleDto simpleDto) {
SimpleDto result = simpleDtoService.save(simpleDto);
return result;
}
}
Now we have SimpleDto
validated using constraint annotations and our custom Spring Validation implementation.
Conclusion
This article touched on Java Spring Validation with a particular focus on Java Bean Validation specification. This includes its implementation and how it’s supported in the Spring Framework Validation Interface.
For further reference, here is the GitHub repository with code examples presented in this article.
原创文章,作者:奋斗,如若转载,请注明出处:https://blog.ytso.com/tech/dev/222753.html