SyntaxHighlighter

Monday, May 10, 2010

Spring 3 Validation Aspect

Update: The problem that this article addresses has been fixed in Spring 3.1. If possible, upgrade to Spring 3.1 and skip the work-around below. See SPR-6709 for more details.

This post is a follow-up to a thread on the Spring Framework support forums. The thread discusses an issue with Spring 3 and the JSR-303 style of annotation-based validation.

To summarize the thread: the @Valid annotation can be used to trigger validation of a parameter to a method of an MVC Controller, but the validation is not properly invoked when the @Valid annotation is used along with the @RequestBody annotation. Without the @RequestBody annotation, the Spring DataBinder mechanism - with full @Valid support - is used to unmarshal the input message to the parameter object. With the @RequestBody annotation, the MessageConverter mechanism - without any @Valid support - is used instead of the DataBinder mechanism.

The issue is also documented in the Spring issue tracking system.

While waiting for Spring to address this gap, several people have developed work-arounds using Spring AOP. I also decided to use this approach. One of my goals in a work-around was to come up with a solution that was very easy to back out when Spring fixes the problem with @RequestBody and @Valid. The AOP-based approach meets this goal, since we can simply remove the aspect when the framework has proper support for this combination of annotations.

There are a few ways to implement an aspect for this, including one posted to the Spring forum thread by user @taku. The implementations are similar but differ in details like how controller methods are intercepted and how validation errors are dealt with.

The aspect my project is using will intercept a call to a method of a Spring-managed bean when the method has the @RequestMapping annotation on it. (Other approaches intercept methods that following certain naming convention or methods of controllers in certain packages.) Each parameter of an intercepted method is inspected, and an injected validator is called for every parameter that has both the @RequestBody and @Valid annotation on it.

If any method parameter fails validation, an HttpMessageConversionException will be thrown. This exception can then be caught and handled by the framework or by @ExceptionHandler methods in a controller. BindException would have been a more natural exception to throw, but it is a checked exception so it cannot be easily thrown from the aspect.

Here is the code for the aspect:

RequestBodyValidatorAspect.java:
@Aspect
public class RequestBodyValidatorAspect {
  private Validator validator;

  @Pointcut("@annotation(org.springframework.web.bind.annotation.RequestMapping)")
  private void controllerInvocation() {
  }

  @Around("controllerInvocation()")
  public Object aroundController(ProceedingJoinPoint joinPoint) throws Throwable {

    MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
    Method method = methodSignature.getMethod();
    Annotation[][] argAnnotations = method.getParameterAnnotations();
    String[] argNames = methodSignature.getParameterNames();
    Object[] args = joinPoint.getArgs();

    for (int i = 0; i < args.length; i++) {
      if (hasRequestBodyAndValidAnnotations(argAnnotations[i])) {
        validateArg(args[i], argNames[i]);
      }
    }

    return joinPoint.proceed(args);
  }

  private boolean hasRequestBodyAndValidAnnotations(Annotation[] annotations) {
    if (annotations.length < 2)
      return false;

    boolean hasValid = false;
    boolean hasRequestBody = false;

    for (Annotation annotation : annotations) {
      if (Valid.class.isInstance(annotation))
        hasValid = true;
      else if (RequestBody.class.isInstance(annotation))
        hasRequestBody = true;

      if (hasValid && hasRequestBody)
        return true;
    }
    return false;
  }

  @SuppressWarnings({"ThrowableInstanceNeverThrown"})
  private void validateArg(Object arg, String argName) {
    BindingResult result = getBindingResult(arg, argName);
    validator.validate(arg, result);
    if (result.hasErrors()) {
      throw new HttpMessageConversionException("Validation of controller input parameter failed",
              new BindException(result));
    }
  }

  private BindingResult getBindingResult(Object target, String targetName) {
    return new BeanPropertyBindingResult(target, targetName);
  }

  @Required
  public void setValidator(Validator validator) {
    this.validator = validator;
  }
}

One limitation with this work-around is that it can only apply a single validator to all controllers. If you are using JSR-303 style annotation-based validation exclusively then this is not a problem - you just inject a LocalValidatorFactoryBean into the aspect. If you need to use a mix of annotation-based validation and class-based validation, this becomes a problem. 

To get around this limitation and make this AOP-based approach more flexible, I also implemented a meta-validator that finds all Validator classes in the application context and calls the appropriate validator for the type of object being validated. This meta-validator can then be injected into the aspect (and into the DataBinder). All other validators are just declared as beans in the app context.

Here is the code for the meta-validator:

public class TypeMatchingValidator implements Validator, InitializingBean, ApplicationContextAware {
  private ApplicationContext context;
  private Collection validators;

  public void afterPropertiesSet() throws Exception {
    findAllValidatorBeans();
  }

  public boolean supports(Class clazz) {
    for (Validator validator : validators) {
      if (validator.supports(clazz)) {
        return true;
      }
    }

    return false;
  }

  public void validate(Object target, Errors errors) {
    for (Validator validator : validators) {
      if (validator.supports(target.getClass())) {
        validator.validate(target, errors);
      }
    }
  }

  private void findAllValidatorBeans() {
    Map<String, Validator> validatorBeans =
            BeanFactoryUtils.beansOfTypeIncludingAncestors(context, Validator.class, true, false);
    validators = validatorBeans.values();
    validators.remove(this);
  }

  public void setApplicationContext(ApplicationContext context) throws BeansException {
    this.context = context;
  }
}


Here is an example of a Spring XML configuration file using the validator aspect and the meta-validator together:

<!-- enable Spring AOP support -->  
  <aop:aspectj-autoproxy proxy-target-class="true"/>

  <!-- declare the validator aspect and inject the validator into it -->
  <bean id="validatorAspect" class="com.something.RequestBodyValidatorAspect">
    <property name="validator" ref="validator"/>
  </bean>

  <!-- inject the validator into the DataBinder framework -->
  <bean class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter">
    <property name="webBindingInitializer">
      <bean class="org.springframework.web.bind.support.ConfigurableWebBindingInitializer" p:validator-ref="validator"/>
    </property>
  </bean>

  <!-- declare the meta-validator bean -->
  <bean id="validator" class="com.something.TypeMatchingValidator"/>

  <!-- declare all Validator beans, these will be discovered by TypeMatchingValidator -->
  <bean class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean"/>
  <bean class="com.something.PersonValidator"/>
  <bean class="com.something.AccountValidator"/>

7 comments:

  1. Thank you for quick and smart introducion.

    ReplyDelete
  2. Can you please share the source code? puneetpandey37@gmail.com

    ReplyDelete
  3. @Puneet All of the code for the aspect is shown on this page. Is there other code you are asking about?

    ReplyDelete
  4. @Scott: Hey, thanks for quick reply. I have developed Spring MVC annotation based Rest Web Services using Spring DM. I use JAXB to convert RequestBody and ResponseBody to XML. My requirement is to validate the Request XML. I tried using JSR 303 but it doesn't seem to work. I am not too sure about the Spring configurations to do this. I deploy my bundle in Spring DM Server (Virgo), which fails to find validator (even though I have installed Hibernate Validator v4.2.0.Final bundle in Spring DM server). Please suggest.

    ReplyDelete
  5. @Puneet I suggest you post this question to the Spring forums at http://forum.springsource.org/forum.php. If you include a description of what you are trying to do, along with some sample Spring config to wire up the validation, you might get some help there. This blog comment area isn't good for including code snippets, and doesn't get as many eyes on it as the Spring forums do. If you want to post the link to the forum posting here, I will take a look at it.

    ReplyDelete
  6. @Scott Today I Deployed my Services in Spring DM server 3.6.1 and found that the issue is not fixed by SpringSource even in version 3.1.0 Release. I am going to raise this issue to the SpringSource people.Will post the link here and keep you updated. Thanks a lot for your help!

    ReplyDelete
  7. I mean Spring DM server 3.6.1 contains Spring 3.1.0 dependencies that are not fixed for JSR 303 validations.

    ReplyDelete