38

I'm adding a user validator using the initBinder method:

@InitBinder
    protected void initBinder(WebDataBinder binder) {
        binder.setValidator(new UserValidator());
    }

Here is the UserValidator

public class UserValidator implements Validator {

    public boolean supports(Class clazz) {
        return User.class.equals(clazz);
    }

    public void validate(Object target, Errors errors) {
        User u = (User) target;

        // more code here
    }
}

The validate method is getting properly called during the controller method call.

@RequestMapping(value = "/makePayment", method = RequestMethod.POST)
public String saveUserInformation(@Valid User user, BindingResult result, Model model){

    // saving User here

    // Preparing CustomerPayment object for the payment page.
    CustomerPayment customerPayment = new CustomerPayment();
    customerPayment.setPackageTb(packageTb);
    model.addAttribute(customerPayment);
    logger.debug("Redirecting to Payment page.");

    return "registration/payment";
}

But while returning to the payment screen I'm getting this error:

java.lang.IllegalStateException: Invalid target for Validator [com.validator.UserValidator@710db357]: com.domain.CustomerPayment[ customerPaymentId=null ] org.springframework.validation.DataBinder.setValidator(DataBinder.java:476) com.web.UserRegistrationController.initBinder(UserRegistrationController.java:43) sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39) sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25) java.lang.reflect.Method.invoke(Method.java:597) org.springframework.web.bind.annotation.support.HandlerMethodInvoker.initBinder(HandlerMethodInvoker.java:393) org.springframework.web.bind.annotation.support.HandlerMethodInvoker.updateModelAttributes(HandlerMethodInvoker.java:222) org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter.invokeHandlerMethod(AnnotationMethodHandlerAdapter.java:429) org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter.handle(AnnotationMethodHandlerAdapter.java:414)

This might be because I'm returning a CustomerPayment and there is not validator defined for that.

I'm also not able to add multiple validators in initBinder method.

How can I fix this?

mustaccio
  • 18,234
  • 16
  • 48
  • 57
Ravi
  • 7,939
  • 14
  • 40
  • 43

9 Answers9

108

You need to set the value of the @InitBinder annotation to the name of the command you want it to validate. This tells Spring what to apply the binder to; without it, Spring will try to apply it to everything. This is why you're seeing that exception: Spring is trying to apply the binder - with your UserValidator - to a parameter of type CustomerPayment.

In your specific case, it looks like you need something like:

@InitBinder("user")
protected void initBinder(WebDataBinder binder) {
    binder.setValidator(new UserValidator());
}

To your second question, as Rigg802 explained, Spring does not support attaching multiple validators to a single command. You can, however, define multiple @InitBinder methods for different commands. So, for example, you could put the following in a single controller and validate your user and payment parameters:

@InitBinder("user")
protected void initUserBinder(WebDataBinder binder) {
    binder.setValidator(new UserValidator());
}

@InitBinder("customerPayment")
protected void initPaymentBinder(WebDataBinder binder) {
    binder.setValidator(new CustomerPaymentValidator());
}
encelado
  • 3
  • 2
Annabelle
  • 10,596
  • 5
  • 27
  • 26
8

It's a bit tricky to do, 1 controller has only 1 validator on 1 command object. you need to create a "Composite Validator" that will get all the validators and run them seperately.

Here is a tutorial that explains how to do it: using multiple validators

Rigg802
  • 2,666
  • 1
  • 16
  • 14
8

You can add multiple validators by iterating over all org.springframework.validation.Validator in an ApplicationContext and set up suitable ones in @InitBinder for each request.

@InitBinder
public void setUpValidators(WebDataBinder webDataBinder) {
    for (Validator validator : validators) {
        if (validator.supports(webDataBinder.getTarget().getClass())
                && !validator.getClass().getName().contains("org.springframework"))
            webDataBinder.addValidators(validator);
    }
}

See my project for examples and simple benchmarks. https://github.com/LyashenkoGS/spring-mvc-and-jms-validation-POC/tree/benchamark

6

I do not see a reason why Spring does not filter out all validators which are not applicable to the current entity by default which forces to use things like CompoundValidator described by @Rigg802.

InitBinder allows you to specify name only which give you some control but not full control over how and when to apply your custom validator. Which from my perspective is not enough.

Another thing you can do is to perform check yourself and add validator to binder only if it is actually necessary, since binder itself has binding context information.

For example if you want to add a new validator which will work with your User object in addition to built-in validators you can write something like this:

@InitBinder
protected void initBinder(WebDataBinder binder) {
  Optional.ofNullable(binder.getTarget())
      .filter((notNullBinder) -> User.class.equals(notNullBinder.getClass()))
      .ifPresent(o -> binder.addValidators(new UserValidator()));

}

4

There is a simple hack, always return true in supports method, and delegate the class checking to validate. Then basically you can add multiple validator in the initBinder without issue.

@Component
public class MerchantRegisterValidator implements Validator {

    @Autowired
    private MerchantUserService merchantUserService;

    @Autowired
    private MerchantCompanyService merchantCompanyService;

    @Override
    public boolean supports(Class<?> clazz) {
        return true; // always true
    }

    @Override
    public void validate(Object target, Errors errors) {

        if (!RegisterForm.getClass().equals(target.getClass()))
            return; // do checking here.

        RegisterForm registerForm = (RegisterForm) target;

        MerchantUser merchantUser = merchantUserService.getUserByEmail(registerForm.getUserEmail());

        if (merchantUser != null) {
            errors.reject("xxx");
        }

        MerchantCompany merchantCompany = merchantCompanyService.getByRegno(registerForm.getRegno());

        if (merchantCompany != null) {
            errors.reject("xxx");
        }

    }

}
JoshDM
  • 4,939
  • 7
  • 43
  • 72
Sam YC
  • 10,725
  • 19
  • 102
  • 158
2

Multiple validator on one command is supported with Spring MVC 4.x now. You could use this snippet code:

@InitBinder
protected void initBinder(WebDataBinder binder) {
    binder.addValidators(new UserValidator(), new CustomerPaymentValidator());
}
hunglevn
  • 65
  • 1
2

The safest way is to add a generic validator handling that Controller:

    @InitBinder
    public void initBinder(WebDataBinder binder) {
        binder.setValidator(new GenericControllerOneValidator());
    }

Then, in the generic validator you can support multiple request body models and based of the instance of the object, you can invoke the appropriate validator:

 public class GenericValidator implements Validator {
        @Override
        public boolean supports(Class<?> aClass) {
            return ModelRequestOne.class.equals(aClass) 
                  || ModelRequestTwo.class.equals(aClass);
        }
    
            @Override
            public void validate(Object body, Errors errors) {
                if (body instanceof ModelRequestOne) {
                    ValidationUtils.invokeValidator(new ModelRequestOneValidator(), body, errors);
                }
                if (body instanceof ModelRequestTwo) {
                    ValidationUtils.invokeValidator(new ModelRequestTwoValidator(), body, errors);
                }
                
            }
        }

Then you add your custom validations inside for each model validator implementatios. ModeRequestOneValidator and ModeRequestTwoValidator still need to implement the Validator interface of org.springframework.validation Also, do not forget to use @Valid ModeRequestOne and @Valid ModeRequestTwo inside the controllers method call.

0

One addition to Annabelle's answer:

If controller has this method parameter and you want to validate that one specifically

 @RequestMapping(value = "/users", method = RequestMethod.POST)
 public String findUsers(UserRequest request){..}

Then the binding should be lower case of the class name (but just the first letter, and not everything else)

@InitBinder("userRequest")
protected void initUserBinder(WebDataBinder binder) {
    binder.setValidator(new YourValidator());
}
andreyro
  • 935
  • 12
  • 18
-3

Declare request as

(... , Model model,HttpServletRequest request)

and change

model.addAttribute(customerPayment);

to

request.setAttribute("customerPayment",customerPayment);
Jk1
  • 11,233
  • 9
  • 54
  • 64