Tuesday, September 25, 2012

Method Parameter Validation in Spring 3 MVC Controllers


JSR-303 specification allows validation of beans only however in this post I am going to tell you how you can use JSR-303 provider such as hibernate validator to validate request parameters, path variables in Spring 3 controller classes. To run this examaple Hibernate Validator 4.2 must be in classpath.

Below is the sample controller class in Spring 3 for fetching the product

@Controller
public class ProductController {

    @Autowired
    private transient ProductService productService;

    @RequestMapping(value = "/product/{prodId}/", method = RequestMethod.GET)
    public ModelAndView getProduct(@PathVariable("prodId") final String productId) {
        ProductDetail product = productService.getProduct(productId);
        mv.addObject("product", product);
        mv.setViewName("product");
        return mv;
    }
}

You can see that anyone can enter garbage value for {prodId} and that value will be passed to the getProduct() method of product service. We will validate the {prodId} before method execution begins.

Lets create our own JSR-303 annotation @ProductId which will validate the format of prodId

@NotBlank
@Size(min = 5, max = 5)
@Digits(integer = 5, fraction = 0)
@Target({ FIELD, METHOD, PARAMETER, ANNOTATION_TYPE, LOCAL_VARIABLE })
// specifies where this validation can be used (Field, Method, Parameter etc)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint(validatedBy = {})
@ReportAsSingleViolation
// specifies if any of the validation fails, it will be reported as single validation
public @interface ProductId {

    /**
     * This is the key to message will that will be looked in validation.properties for validation
     * errors
     * 
     * @return the string
     */
    String message() default "{invalid.product.id}";

    Class[] groups() default {};

    Class[] payload() default {};
}

@ProductId annotation validates a given string for
  • Not empty string
  • Size should be exactly 5 characters 
  • All characters should be digits.

Now we will use this annotation to validate our prodId path variable.
To use this validation we would need to do following:
  1. Prefix the @PathVariable with @ProductId 
  2. Add @Validated annotation to controller class. It tells spring to validate class at method level.
  3. We need a bean of type MethodValidationPostProcessor in the spring context which will look for @Validated annotated classes and will apply method level validations.
See below how the ProductController now looks like:

@Controller
@Validated
// enables methods parameters JSR validation.
public class ProductController {

    @Autowired
    private transient ProductService productService;

    @RequestMapping(value = "/product/{prodId}/", method = RequestMethod.GET)
    public ModelAndView getProduct(@ProductId @PathVariable("prodId") final String productId) {
        ProductDetail product = productService.getProduct(productId);
        mv.addObject("product", product);
        mv.setViewName("product");
        return mv;
    }
}

Below code tells how to instantiate MethodValidationPostProcessor in spring configuration

@Configuration
@ComponentScan(basePackages = { "web.controller" })
// packages to scans for components
@EnableWebMvc
// enable the MVC support
public class SpringWebConfig extends WebMvcConfigurerAdapter {

    /**
     * Method validation post processor. This bean is created to apply the JSR validation in method
     * parameters. Any class which want to perform method param validation must use @Validated
     * annotation at class level.
     * 
     * @return the method validation post processor
     */
    @Bean
    public MethodValidationPostProcessor methodValidationPostProcessor() {
        return new MethodValidationPostProcessor();
    }
}


Now when you will call this controller method using url "/product/{some garbage value}", it will throw MethodConstraintViolationException exception. You can handle this error using @ExceptionHandler annotation and redirect user to error page with a message.

    @ExceptionHandler({Exception.class})
    @ResponseStatus(value = HttpStatus.NOT_FOUND)
    protected ModelAndView handleException(final Exception ex) {
        final ModelAndView mv = new ModelAndView();
        final List messages = new ArrayList();
        if (ex instanceof MethodConstraintViolationException) {
            for (ConstraintViolation failure : ((MethodConstraintViolationException) ex).getConstraintViolations()) {
                messages.add(failure.getMessage());
            }
        } else {
            messages.add(ex.getMessage());
        }
        mv.addObject("exceptionModel", messages);
        mv.setViewName("exception");
        return mv;
    }

You can use the similar way to validate request parameters by prefixing your validation annotation in front of @RequestParameter annotation.

If you want to disable the validation for some particular class remove the @Validated annotation from that class. If you are interested in completely disabling the method validation then remove the MethodValidationPostProcessor bean from spring context. You might want to disable validations for unit tests.

1 comment: