Scott Frederick's humble blog

on sofware development and technology

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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
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 &amp;&amp; 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:

TypeMatchingValidator.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public class TypeMatchingValidator implements Validator, InitializingBean, ApplicationContextAware {
  private ApplicationContext context;
  private Collection<Validator> 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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
  <!-- 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"/>