@Validated annotation. The annotation is a Spring's specific variant of JSR-303's javax.validation.Valid, supporting the specification of validation groups. @Validated may be used either with Spring's @Controller method arguments or with with method-level validation.In order to indicate that a specific class is supposed to be validated at the method level it needs to be annotated with
@Validated annotation at type level:
@Service
@Validated
public class UserCreateService {
}
Methods applicable for validation must have JSR-303 constraint annotations on their parameters and/or on their return value:
@Service
@Validated
public class UserCreateService {
@Autowired
private UserRepository repo;
@NotNull
public User createUser(@NotBlank @Email String email,
@NotBlank String username, @NotBlank String password) {
User user = new User();
user.setEmail(email);
user.setName(username);
user.setPassword(password);
return repo.save(user);
}
}
Beans annotated with @Validated annotation will be detected by MethodValidationPostProcessor and validation functionality is delegetated to Hibernate's MethodValidator that requires Hibernate Validator 4.2 or higher. When the validation fails MethodConstraintViolationException, with a set of contraint violations, is thrown.
The easiest way to understand and verify method-level validation is to create a unit test. The context of the test is configured by very simple
@Config class. Expected exceptions are verified with JUnit ExpectedException rule and three custom Hamcrest matchers:
- has n contraint violations (
HasContraintViolations) - has contraint violation for parameter with index i and message containing m (
ParameterViolation) - has contraint violation for return value and message containing m (
ReturnValueViolation)
@ContextConfiguration(classes = {UserCreateServiceTest.Config.class})
@RunWith(SpringJUnit4ClassRunner.class)
public class UserCreateServiceTest {
@Autowired
private UserCreateService service;
@Autowired
private UserRepository userRepositoryMock;
@Rule
public ExpectedException error = ExpectedException.none();
@Test
public void createUser_3ArgumentsNull_Has3ContraintViolations() {
error.expect(new HasContraintViolations(3));
error.expect(new ParameterViolation(0, "may not be empty"));
error.expect(new ParameterViolation(1, "may not be empty"));
error.expect(new ParameterViolation(2, "may not be empty"));
service.createUser(null, null, null);
}
@Test
public void createUser_2ArgumentsNull_Has2ContraintViolations() {
error.expect(new HasContraintViolations(2));
error.expect(new ParameterViolation(0, "may not be empty"));
error.expect(new ParameterViolation(1, "may not be empty"));
service.createUser(null, null, "x");
}
@Test
public void createUser_EmailIsBlank_Has1ContraintViolations() {
error.expect(new HasContraintViolations(1));
error.expect(new ParameterViolation(0, "may not be empty"));
service.createUser("", "x", "x");
}
@Test
public void createUser_EmailIsInvalid_Has1ContraintViolations() {
error.expect(new HasContraintViolations(1));
error.expect(new ParameterViolation(0, "not a well-formed email address"));
service.createUser("x", "x", "x");
}
@Test
public void createUser_ReturnValueIsNull_Has1ContraintViolations() {
when(userRepositoryMock.save(any(User.class))).thenReturn(null);
error.expect(new HasContraintViolations(1));
error.expect(new ReturnValueViolation("may not be null"));
service.createUser("user@domain.com", "x", "x");
verify(userRepositoryMock).save(null);
}
@Test
public void createUser() {
when(userRepositoryMock.save(any(User.class))).thenReturn(new User());
service.createUser("user@domain.com", "x", "x");
verify(userRepositoryMock).save(any(User.class));
}
}
Here below, you can see how the method-level validation is configured using MethodValidationPostProcessor:
@Configuration
public static class Config {
@Bean
public MethodValidationPostProcessor methodValidationPostProcessor() {
return new MethodValidationPostProcessor();
}
@Bean
public UserRepository userRepository() {
return Mockito.mock(UserRepository.class);
}
@Bean
public UserCreateService userCreateService() {
return new UserCreateService();
}
}
And with below custom matchers we are able to quickly check if the validation occurred as we expected:
public class HasContraintViolations extends TypeSafeMatcher<MethodConstraintViolationException> {
private int constraintViolationCount;
public HasContraintViolations(int constraintViolationCount) {
this.constraintViolationCount = constraintViolationCount;
}
@Override
public boolean matchesSafely(MethodConstraintViolationException e) {
return e.getConstraintViolations().size() == constraintViolationCount;
}
public void describeTo(Description description) {
description.appendText(MessageFormat.format("has {0} contraint violations", constraintViolationCount));
}
}
public class ParameterViolation extends TypeSafeMatcher<MethodConstraintViolationException> {
private int index;
private String substring;
public ParameterViolation(int index, String substring) {
this.index = index;
this.substring = substring;
}
@Override
@SuppressWarnings("rawtypes")
public boolean matchesSafely(MethodConstraintViolationException e) {
for(MethodConstraintViolation c : e.getConstraintViolations()) {
if(Kind.PARAMETER == c.getKind() && c.getParameterIndex() == index) {
return c.getMessage().contains(substring);
}
}
return false;
}
public void describeTo(Description description) {
description.appendText(MessageFormat.format("has no contraint violation for parameter {0} with message containing '{1}'", index, substring));
}
}
public class ReturnValueViolation extends TypeSafeMatcher<MethodConstraintViolationException> {
private String substring;
public ReturnValueViolation(String substring) {
this.substring = substring;
}
@Override
@SuppressWarnings("rawtypes")
public boolean matchesSafely(MethodConstraintViolationException e) {
for(MethodConstraintViolation c : e.getConstraintViolations()) {
if(Kind.RETURN_VALUE == c.getKind()) {
return c.getMessage().contains(substring);
}
}
return false;
}
public void describeTo(Description description) {
description.appendText(MessageFormat.format("has no contraint violation for return value with message containing '{0}'", substring));
}
}
}
Within couple of minutes we have learned the basic usage of method-level validation in Spring 3.1. But there is one pending question: "Do we want to utilize method-level validation in production code?". What's your opinion?
Resources
Spring MVC Validation Demo Project
Top 5 enhancements of Spring MVC 3.1
You have started this blog exactly an year ago.
ReplyDeleteThanks for the article, I think if we use this validation we'll have a robust code.
ReplyDelete