简体   繁体   中英

Convert JSR-303 validation errors to Spring's BindingResult

I have the following code in a Spring controller:

@Autowired
private javax.validation.Validator validator;

@RequestMapping(value = "/submit", method = RequestMethod.POST)
public String submitForm(CustomForm form) {
    Set<ConstraintViolation<CustomForm>> errors = validator.validate(form);
    ...
}

Is it possible to map errors to Spring's BindingResult object without manually going through all the errors and adding them to the BindingResult ? Something like this:

// NOTE: this is imaginary code
BindingResult bindingResult = BindingResult.fromConstraintViolations(errors);

I know it is possible to annotate the CustomForm parameter with @Valid and let Spring inject BindingResult as another method's parameter, but it's not an option in my case.

// I know this is possible, but doesn't work for me
public String submitForm(@Valid CustomForm form, BindingResult bindingResult) {
    ...
}

A simpler approach could be to use Spring's abstraction org.springframework.validation.Validator instead, you can get hold of a validator by having this bean in the context:

<bean id="jsr303Validator" class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean" />

@Autowired @Qualifier("jsr303Validator") Validator validator;

With this abstraction in place, you can use the validator this way, passing in your bindingResult:

validator.validate(obj, bindingResult);

Spring uses a SpringValidatorAdapter to convert javax.validation.ConstraintViolation objects to ObjectError or FieldError objects, as found in the binding result. The BindStatus then uses a message source (like the web application context itself) to translate the errors. In short, you could do:

SpringValidatorAdapter springValidator = new SpringValidatorAdapter(validator);
BindingResult bindingResult= new BeanPropertyBindingResult(myBeanToValidate, "myBeanName");
springValidator.validate(myBeanToValidate, bindingResult);

This is easier when writing a unit test, because you don't even need to create a Spring context.

@RequestMapping(value = "/submit", method = RequestMethod.POST)
public String submitForm(CustomForm form) {
    Set<ConstraintViolation<CustomForm>> errors = validator.validate(form);

    BindingResult bindingResult = toBindingResult(errors, form, "form");
    ...
}

private BindingResult toBindingResult(ConstraintViolationException e, Object object, String objectName) {
    BindingResult bindingResult = new BeanPropertyBindingResult(object, objectName);
    new AddConstraintViolationsToErrors().addConstraintViolations(e.getConstraintViolations(), bindingResult);
    return bindingResult;
}

private static class AddConstraintViolationsToErrors extends SpringValidatorAdapter {
    public AddConstraintViolationsToErrors() {
        super(Validation.buildDefaultValidatorFactory().getValidator()); // Validator is not actually used
    }

    @SuppressWarnings({"rawtypes", "unchecked"})
    public void addConstraintViolations(Set<? super ConstraintViolation<?>> violations, Errors errors) {
        // Using raw type since processConstraintViolations specifically expects ConstraintViolation<Object>
        super.processConstraintViolations((Set) violations, errors);
    }
}

Unlike the other answers to this question, this solution handles the case where there already exists a Set<ConstraintViolation<?>> which needs to be converted to to a BindingResult .

Explanation

Spring provides the SpringValidatorAdapter class to perform bean validations, storing the results in an Errors instance (note that BindingResult extends Errors ). The normal manual use of this class would be to use it to perform the validations via the validate method:

Validator beanValidator = Validation.buildDefaultValidatorFactory().getValidator();
SpringValidatorAdapter validatorAdapter = new SpringValidatorAdapter(beanValidator);

BindException bindException = new BindException(form, "form");
validatorAdapter.validate(form, bindException);

However, this doesn't help in the case where there already exists a Set<ConstraintViolation<?>> which needs to be converted to a BindingResult .

It is still possible to achieve this goal, though it does require jumping through a couple extra hoops. SpringValidatorAdapter contains a processConstraintViolations method which converts the ConstraintViolation objects into the appropriate Spring ObjectError subtypes, and stores them on an Errors object. However, this method is protected, limiting its accesibility to subclasses.

This limitation can be worked around by creating a custom subclass of SpringValidatorAdapter which delegates to or exposes the protected method. It is not a typical usage, but it works.

public class AddConstraintViolationsToErrors extends SpringValidatorAdapter {
    public AddConstraintViolationsToErrors() {
        super(Validation.buildDefaultValidatorFactory().getValidator()); // Validator is not actually used
    }

    @SuppressWarnings({"rawtypes", "unchecked"})
    public void addConstraintViolations(Set<? super ConstraintViolation<?>> violations, Errors errors) {
        // Using raw type since processConstraintViolations specifically expects ConstraintViolation<Object>
        super.processConstraintViolations((Set) violations, errors);
    }
}

This custom class can be used to populate a newly created BindingResult , achieving the goal of creating a BindingResult from a Set<ConstraintViolation<?>> .

private BindingResult toBindException(ConstraintViolationException e, Object object, String objectName) {
    BindingResult bindingResult = new BeanPropertyBindingResult(object, objectName);
    new AddConstraintViolationsToErrors().addConstraintViolations(e.getConstraintViolations(), bindingResult);
    return bindingResult;
}

Expanding on Kristiaan's answer, for testing purposes it is not necessary to create a spring context to validate using Spring's bindingResult. The following is an example:

public class ValidatorTest {

    javax.validation.Validator javaxValidator = Validation.buildDefaultValidatorFactory().getValidator();
    org.springframework.validation.Validator springValidator = new SpringValidatorAdapter(javaxValidator);

    @Test
    public void anExampleTest() {

    JSR303AnnotatedClassToTest   ctt  = new JSR303AnnotatedClassToTest( ..init vars..)

    ... test setup...

    WebDataBinder dataBinder = new WebDataBinder(ctt);
    dataBinder.setValidator(springValidator);
    dataBinder.validate();
    BindingResult bindingResult = dataBinder.getBindingResult(); 

    ... test analysis ...

    }
}

This approach doesn't require creating a binding result ahead of time, the dataBinder builds the right one for you.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM