Search code examples
javavalidationspring-boothibernate-annotations

Spring Boot - Custom validation annotation on form not working


I'd like to have an annotation that validates that a MultipartFile is an image. I've created an @interface and a ConstraintValidator, and added the annotation to my field.

Other validation annotations, like @NotEmpty and @Size(min = 0, max = 2) are working fine.

Here is the code in summary. This question has the same problem, but the answer doesn't work for me.

Form.java:

@Validated
public class Form {

    @MultipartImage
    private MultipartFile image;

    ...
}

@Interface MultipartImage

import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.LOCAL_VARIABLE;
import static java.lang.annotation.ElementType.METHOD;

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

import javax.validation.Constraint;
import javax.validation.Payload;

import validation.MultipartFileImageConstraintValidator;

@Documented
@Constraint(validatedBy = { MultipartFileImageConstraintValidator.class })
@Target({ LOCAL_VARIABLE, FIELD, METHOD })
@Retention(RetentionPolicy.RUNTIME)
public @interface MultipartImage {

    String message() default "{MultipartImage.message}";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};

}

The validator, MultipartFileConstraintValidator.java

import java.io.IOException;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;

import org.springframework.web.multipart.MultipartFile;

public class MultipartFileConstraintValidator implements ConstraintValidator<MultipartImage, MultipartFile> {


@Override
public void initialize(final MultipartImage constraintAnnotation) {
}

@Override
public boolean isValid(final MultipartFile file, final ConstraintValidatorContext context) {
    return false;
}

Here's the form submit method in the controller

@RequestMapping(value = "/formsubmit", method = RequestMethod.POST)
public ModelAndView handleForm(@Validated final Form form,
        final BindingResult bindingResult) {

    if (bindingResult.hasErrors()) {
        ...
        // returns the model
    }
}

Validator set up in the @Configuration file, see https://stackoverflow.com/a/21965098/4161471

@Configuration
@ConfigurationProperties("static")
@AutoConfigureAfter(DispatcherServletAutoConfiguration.class)
public class StaticResourceConfig extends WebMvcConfigurerAdapter {

...

@Bean(name = "validator")
public LocalValidatorFactoryBean validator() {
    LocalValidatorFactoryBean bean = new LocalValidatorFactoryBean();
    bean.setValidationMessageSource(messageSource());
    return bean;
}

@Bean
public MethodValidationPostProcessor methodValidationPostProcessor() {
    final MethodValidationPostProcessor methodValidationPostProcessor = new MethodValidationPostProcessor();
    methodValidationPostProcessor.setValidator(validator());

    return methodValidationPostProcessor;
}

@Override
public Validator getValidator() {
    return validator();
}

@Bean
public ReloadableResourceBundleMessageSource messageSource() {
    ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource();
    // Load files containing message keys.
    // Order matters. The first files override later files.
    messageSource.setBasenames(//
            // load messages and ValidationMessages from a folder relative to the jar
            "file:locale/messages", //
            "file:locale/ValidationMessages", //
            // load from within the jar
            "classpath:locale/messages", //
            "classpath:locale/ValidationMessages" //
    );
    messageSource.getBasenameSet();
    messageSource.setCacheSeconds(10); // reload messages every 10 seconds
    return messageSource;
}

}


Solution

  • There was information missing from my original code, specifically regarding the controller, where an additional validator is defined and bound. It uses the wrong method to include the validator FormValidator, and overrides the annotation validations.

    binder.setValidator(formValidator) overrides any other validator. Instead binder.addValidators(formValidator) should be used!

    @Controller
    public class FormController {
    
         @Autowired
         final private FormValidator formValidator;
    
         @InitBinder("form")
         protected void initBinder(WebDataBinder binder) {
            // correct
            binder.addValidators(formValidator);
            // wrong
            //binder.setValidator(formValidator);
        }
    
        ...
    
        @RequestMapping(value = "/formsubmit", method = RequestMethod.POST)
        public ModelAndView handleForm(@Validated final Form form, final BindingResult bindingResult) {
            if (bindingResult.hasErrors()) {
                ...
                // returns the model
            }
        ...
        }
    }
    

    I have also removed the Bean MethodValidationPostProcessor in the @Configuration class.