Wednesday 20 October 2010

Validating service method parameters using JSR 303, Spring AOP/AspectJ and Spring

When building an application with Spring and JSR303 you can easily validate form submissions using the built in JSR303 validators. This helps ensure that parameters passed to service methods are valid in a web application. However, there are certain cases where you might wish to invoke your service methods directly rather than from within an MVC controller, for example if you were using DWR or a REST web service. Here is an example of a service facade that might exist in a Spring application.
public interface UserService {

 public abstract void updateUser(UserForm userForm);

}

@Service
public class UserServiceImpl {

 @Override
 @Secured({"ROLE_EMPLOYEE"})
 @Transactional(readOnly=false, isolation=Isolation.READ_COMMITTED,propagation=Propagation.SUPPORTS)
 public abstract void updateUser(UserForm userForm) {
   User user = extractUserFromUserForm(userForm);
   userDao.updateUser(user);
 }

}
Here is the UserForm parameter that is passed to the updateUser method above. It is a simple Form or Command bean using JSR 303 annotations to validate input.
@FieldMatch.List({
 @FieldMatch(first = "password", second = "confirmPassword"),
})
public class UserForm {
 @NotNull
 private String username;

 @NotNull
 @Pattern(regexp = "(?=.*\\d)(?=.*[a-zA-Z]).{6,12}")
 private String password;

 @NotNull
 @Pattern(regexp = "(?=.*\\d)(?=.*[a-zA-Z]).{6,12}")
 private String confirmPassword;

}
If the only client of the facade is a Spring MVC controller, the JSR303 validator can easily be wired in to validate the UserForm before it gets anywhere near the facade. However, if you are invoking the service in another way, there is a risk that the UserForm wouldn't be valid when it is passsed to the updateUser() method. How could we prevent that? The obvious answer would be to invoke the JSR303 validator in code in the facade implementation. Something like this would do the trick:
@Service
public class UserServiceImpl {

 @Autowired
 private Validator validator;

 @Override
 @Secured({"ROLE_EMPLOYEE"})
 @Transactional(readOnly=false, isolation=Isolation.READ_COMMITTED,propagation=Propagation.SUPPORTS)
 public abstract void updateUser(UserForm userForm) {

   // Validate input
   Set> violations = validator.validate(userForm);
   if (!violations.isEmpty())
     throw new ConstraintViolationException(violations);

   // Perform update
   User user = extractUserFromUserForm(userForm);
   userDao.updateUser(user);
 }

}
Reasonably simple - but if you do that for every service method, the repeated boilerplate code will add up to a fair bit of typing and clutter. Here is my proposed alternative. Use a new annotation to decorate the parameter in the service method, and use AOP to intercept method invocations and perform parameter validation. It is effectively the same solution but with less typing. Here is what the end solution would look like.
@Service
public class UserServiceImpl {

 @Autowired
 private Validator validator;

 @Override
 @Secured({"ROLE_EMPLOYEE"})
 @Transactional(readOnly=false, isolation=Isolation.READ_COMMITTED,propagation=Propagation.SUPPORTS)
 public abstract void updateUser(@Valid UserForm userForm) {

   // Perform update
   User user = extractUserFromUserForm(userForm);
   userDao.updateUser(user);
 }

}
Note the @Valid annnotation on the UserForm parameter. This isn't the standard JSR 303, but our own custom annotation that is used to mark parameters that need validation via AOP. On reflection, I probably should have called it something else to avoid confusion.
package myapp.validators.aspects;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.PARAMETER})
public @interface Valid {
 Class[] groups() default {};
}

The aspect that performs the validation is here.
package myapp.validators.aspects;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.HashSet;
import java.util.Set;

import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import javax.validation.Validator;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;

@Aspect
public class ValidateAspect {
 
 @Autowired
 private Validator validator;

 public void setVaidator(Validator validator) {
   this.validator = validator;
 }
 
  
 @Before("execution(* *(@myapp.validators.aspects.Valid (*)))")
 public void valid(JoinPoint jp) throws NoSuchMethodException {

   // ConstraintViolations to return
   Set<ConstraintViolation<?>> violations = new HashSet<ConstraintViolation<?>>();

   // Get the target method
   Method interfaceMethod = ((MethodSignature)jp.getSignature()).getMethod();
   Method implementationMethod = jp.getTarget().getClass().getMethod(interfaceMethod.getName(), interfaceMethod.getParameterTypes());
  
   // Get the annotated parameters and validate those with the @Valid annotation
   Annotation[][] annotationParameters = implementationMethod.getParameterAnnotations();
   for (int i = 0; i < annotationParameters.length; i++) {
     Annotation[] annotations = annotationParameters[i];
     for (Annotation annotation : annotations) {
       if (annotation.annotationType().equals(Valid.class)) {
         Valid valid = (Valid)annotation;
         Object arg = jp.getArgs()[i];
         violations.addAll(validator.validate(arg, valid.groups()));
       }
     }
   }
        
   // Throw an exception if ConstraintViolations are found
   if (!violations.isEmpty()) {
     throw new ConstraintViolationException(violations);
   }
  }
}

The final bit of glue in Spring to make it all work is to declare the custom annotation aspect in the application context.


 

Hope this is useful to someone. I found the following blogs and forum threads helpful in coming up with this post and related code. References http://forum.springsource.org/showthread.php?t=77390 http://static.springsource.org/spring/docs/3.0.x/spring-framework-reference/html/aop.html http://blog.newsplore.com/2010/02/23/spring-mvc-3-0-rest-rebuttal