Monday, March 5, 2012

HOW-TO: Method-level validation in Spring 3.1 with @Validated annotation

Spring MVC 3.1 Bean Validation support has been extended with @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

2 comments:

  1. You have started this blog exactly an year ago.

    ReplyDelete
  2. Thanks for the article, I think if we use this validation we'll have a robust code.

    ReplyDelete

Fork me on GitHub