Lab M03P01
Validation implementation in Spring is provided by the Hibernate Validator, which is the reference implementation of the Bean Validation Specification. You can validate beans at:
- Application layer
- Service layer
- Domain layer
- Persistence layer
Declarative constraints
The Java Bean Validation provider offers ready to use constraint definitions and validators implementation. Just check documentation or jakarta.validation.constraints package. Few examples of such are:
- @NotNull - The annotated element must not be nul
- @NotEmpty - The annotated element must not be null nor empty
- @Pattern - The annotated string must match the specified regular expression
- @Size - The annotated element size must be between the specified boundaries
-
First focus of simple example of the bean (java object) validation. There is the ite.librarymaster.ValidationTests class with bookDTOValidationTest() test. Test utilizes the Spring Boot where Validator is autoconfigured and injected into test class for you. Now execute test. It should pass as the tested object is valid. You are going to add another constraint to ite.librarymaster.api.BookDTO class. Notice the catId is not constrained, but it is required to be not empty. So use @NotEmpty built in annotation to fix it.
Now re-run the test again. The test should fail as the catId is obviously empty. If you are interested about violation details, you can check the result of validation:@NotEmpty public String catId;Now fi the test and set the catId field and re-run the test again. Test should pass this time.result.forEach(violation -> { System.out.println(violation.getMessage()); }); -
If there is no standard constrain annotation for your validation use case, you can define custom constrain and implement validator for it. Let's demonstrate this feature of Java Bean Validation by implementing custom constrain for catId format validation. First you need new Java annotation definition for custom constraint. There is empty implementation in ite.librarymaster.validation.CatalogueId. You should introduce few meta-annotations and mandatory attributes of the CatalogueId constraint:
Custom constraint must be annotated by a @Constraint annotation which refers to its list of constraint validation implementations. In our case the validator is implemented by the CatalogueIdValidator class.@Documented @Constraint(validatedBy = CatalogueIdValidator.class) @Target( { ElementType.METHOD, ElementType.FIELD }) @Retention(RetentionPolicy.RUNTIME) public @interface CatalogueId { String message() default "{book.invalid.catalogueId}"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; } -
You need to complete the implementation of the ite.librarymaster.validation.CatalogueIdValidator class. You can come up with your ow implementation , or use this one:
Now you can use and test the new constraint and validator. Use @CatalogueId annotation on catId field of the ite.librarymaster.api.BookDTO class. Yes, single field can be annotated by multiple constraint annotations.public class CatalogueIdValidator implements ConstraintValidator<CatalogueId, String> { @Override public boolean isValid(String value, ConstraintValidatorContext context) { return value != null && value.matches("^LM-[0-9]+") && (value.length() >= 8) && (value.length() <= 13); } }Finally, re-run test again. Don't forget to set the catId field to valid value. Note: If your validator logic uses only validation based on regular expressions, you can save effort and use @Pattern constraint.@NotEmpty @CatalogueId public String catId;
Validation execution
-
Now focus on how Spring handles validation. This example application implements REST API, where requests holding data should be also validated against API contract. REST controller is implemented by te.librarymaster.api.BookRestController and API data model is represented by ite.librarymaster.api.BookDTO. You already have added constraints into BookDTO class. There is also BookRestControllerTest test which utilises MockMVC for HTTP like testing. Execute the testBookNotCreatedWithEmptyAuthor() test. Interesting is that test fails event there is @NotEmpty constraint defined on author field of BookDTO. It seems Spring doe not execute any validation logic by default.
-
In order to activate validation logic you should add @Valid annotation on parameter of processCreation() method of the te.librarymaster.api.BookRestController:
Now re-run the testBookNotCreatedWithEmptyAuthor() and check result and also test logs. There should be validation error.@PostMapping(value = "/books", consumes = {MediaType.APPLICATION_JSON_VALUE, MediaType.APPLICATION_XML_VALUE}) public ResponseEntity processCreation(@RequestBody @Valid BookDTO book) {...} -
If you like to take more control on validation, you can use programmatic validation. Inject Validator into the te.librarymaster.api.BookRestController:
Initialize validation in the processCreation() method of controller:@Autowired private Validator validator;Now re-run the testBookNotCreatedWithEmptyAuthor() and check result and also test logs.Set<ConstraintViolation<BookDTO>> violations = validator.validate(book); if (!violations.isEmpty()) { throw new ConstraintViolationException(violations); } -
There is resource bundle for validation error messages defined. Can you figure out, how this works?
Programmatically invoked validation
Spring provides Validator implementation you can inject and use it for programmatic validation.
@Service
class ProgrammaticallyValidatingService {
private Validator validator;
public ProgrammaticallyValidatingService(Validator validator) {
this.validator = validator;
}
public void validateInputWithInjectedValidator(Input input) {
Set<ConstraintViolation<Input>> violations = validator.validate(input);
if (!violations.isEmpty()) {
throw new ConstraintViolationException(violations);
}
}
}
- ISBN Pattern:
@Pattern(regexp = "^(97(8|9))?\\d{9}(\\d|X)$") - Cat ID Pattern:
"^LM-[0-9]+"