Spring validation check validation of invalid argument

I have a controller with a web method that looks like this:

public Response registerDevice( @Valid final Device device, @RequestBody final Tokens tokens ) {...} 

And a validator that looks like this:

 public class DeviceValidator implements Validator { @Override public boolean supports(Class<?> clazz) { return Device.class.isAssignableFrom(clazz); } @Override public void validate(Object target, Errors errors) { // Do magic } } } 

I am trying to get Spring to validate the Device argument that is generated by the interceptor. But every time I try, it checks for token arguments instead.

I tried using @InitBinder to specify validator, @Validated instead of @Valid and register MethodValidationPostProcessor classes. So far no luck.

Either the validator is not called at all, or the token argument is checked when I was the Device validated argument.

I am using Spring 4.1.6 and Hibernate validator 5.1.3.

Can anyone tell me what I'm doing wrong? I’ve been searching the Internet all my life, trying to figure it out. I can’t believe that the Spring validation scope is still as confusing as it was 5 years ago: - (

+1
spring bean validation
source share
1 answer

Ok Did it decide after two days of riots with all sorts of variations. If there is one thing Spring validation lets you do - it comes up with an incredible array of things that don't work! But back to my decision.

Basically, I needed a way to manually create argument mapping arguments, validate them, and then ensure that regardless of whether it was success or failure, the caller always received a custom JSON response. Doing this turned out to be a lot more complicated than I thought, because despite the number of blog posts and stackoverflow responses, I did not find a complete solution. So I tried to describe every piece of the puzzle needed to achieve what I wanted.

Note: in the following code examples, I have summarized the names of things to help clarify what is common and what is not.

Configuration

Although the few blog posts I read spoke of various classes, such as MethodValidationPostProcessor , in the end I found that I did not need the setting except for the @EnableWebMvc annotation. Default solvers, etc. Turned out to be what I need.

Query display

My final query match mappings looked like this:

 @RequestMapping(...) public MyMsgObject handleRequest ( @Valid final MyHeaderObj myHeaderObj, @RequestBody final MyRequestPayload myRequestPayload ) {...} 

Here you will notice that, unlike the entire blog post and the sample I found, I have two objects passed to the method. The first is the object that I want to dynamically generate from the headers. The second is a deserialized object from the JSON payload. Other objects can be easily included, such as path arguments, etc. Try something like this without the code below and you will get many unusual and wonderful errors.

The hard part that caused me all the pain was that I wanted to check the instance of myHeaderObj and NOT check the instance of myRequestPayload . This caused a big headache to resolve.

Also pay attention to the result object MyMsgObject . Here I want to return an object that will be serialized in JSON. Including when exceptions occur, since this class contains error fields that must be filled in addition to the HttpStatus code.

Admin Tip

Next, I created the ControllerAdvice class, which contained a binding for checking and a common error trap.

 @ControllerAdvice public class MyControllerAdvice { @Autowired private MyCustomValidator customValidator; @InitBinder protected void initBinder(WebDataBinder binder) { if (binder.getTarget() == null) { // Plain arguments have a null target. return; } if (MyHeaderObj.class.isAssignableFrom(binder.getTarget().getClass())) { binder.addValidators(this.customValidator); } } @ExceptionHandler(Exception.class) @ResponseStatus(value=HttpStatus.INTERNAL_SERVER_ERROR) @ResponseBody public MyMsgObject handleException(Exception e) { MyMsgObject myMsgObject = new MyMsgObject(); myMsgObject.setStatus(MyStatus.Failure); myMsgObject.setMessage(e.getMessage()); return myMsgObject; } } 

A lot is going on here. The first registers a validator. Note that we must check the type of the argument. This is because @InitBinder is called for each argument in @RequestMapping , and we only need a validator in the argument myHeaderObj . If we do not, exceptions will be thrown if Spring tries to apply the validator to arguments for which it is not valid.

Secondly, an exception handler. We must use @ResponseBody to ensure that Spring treats the returned object as something to be serialized. Otherwise, we just get a standard HTML exception report.

validator

Here we use a fairly standard validator implementation.

 @Component public class MyCustomValidator implements Validator { @Override public boolean supports(Class<?> clazz) { return MyHeaderObj.class.isAssignableFrom(clazz); } @Override public void validate(Object target, Errors errors) { ... errors.rejectValue("fieldName", "ErrorCode", "Invalid ..."); } } 

One thing that I still don't understand is the supports(Class<?> clazz) . I would think that Spring uses this method to check arguments to decide if this validator should be applied. But this is not so. Therefore, all the code in @InitBinder must decide when to use this validator.

Argument handler

This is the biggest piece of code. Here we need to generate the myHeaderObj object, which will be passed to @RequestMapping . Spring will automatically detect this class.

 public class MyHeaderObjArgumentHandler implements HandlerMethodArgumentResolver { @Override public boolean supportsParameter(MethodParameter parameter) { return MyHeaderObj.class.isAssignableFrom(parameter.getParameterType()); } @Override public Object resolveArgument( MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { // Code to generate the instance of MyHeaderObj! MyHeaderObj myHeaderObj = ...; // Call validators if the argument has validation annotations. WebDataBinder binder = binderFactory.createBinder(webRequest, myHeaderObj, parameter.getParameterName()); this.validateIfApplicable(binder, parameter); if (binder.getBindingResult().hasErrors()) { throw new MyCustomException(myHeaderObj); } return myHeaderObj; } protected void validateIfApplicable(WebDataBinder binder, MethodParameter methodParam) { Annotation[] annotations = methodParam.getParameterAnnotations(); for (Annotation ann : annotations) { Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class); if (validatedAnn != null || ann.annotationType().getSimpleName().startsWith("Valid")) { Object hints = (validatedAnn != null ? validatedAnn.value() : AnnotationUtils.getValue(ann)); Object[] validationHints = (hints instanceof Object[] ? (Object[]) hints : new Object[] { hints }); binder.validate(validationHints); break; } } } } 

The main task of this class is to use any means necessary to build an argument ( myHeaderObj ). After building it, it goes to the Spring validators to validate this instance. If there is a problem (detected by checking for returned errors), it throws an exception that @ExceptionHandler can detect and handle.

Note the validateIfApplicable(WebDataBinder binder, MethodParameter methodParam) method validateIfApplicable(WebDataBinder binder, MethodParameter methodParam) . This is the code I found in several Spring classes. The challenge is to determine if any argument has an @Validated or @Valid , and if so, call the related validators. By default, Spring does not do this for custom argument handlers like this, so we can add this functionality. Seriously Spring ???? No AbstractSomething ????

Last Part, Explicit Exception

Finally, I also needed to catch the more obvious exceptions. For example, MyCustomException described above. So here I created a second @ControllerAdvise .

 @ControllerAdvice @Order(Ordered.HIGHEST_PRECEDENCE) // Make sure we get the highest priority. public class MyCustomExceptionHandler { @ExceptionHandler @ResponseStatus(value = HttpStatus.BAD_REQUEST) @ResponseBody public Response handleException(MyCustomException e) { MyMsgObject myMsgObject = new MyMsgObject(); myMsgObject.setStatus(MyStatus.Failure); myMsgObject.setMessage(e.getMessage()); return myMsgObject; } } 

Although it looks like a general exception handler. There is one other. We need to specify the annotation @Order(Ordered.HIGHEST_PRECEDENCE) . Without this, Spring will only execute the first exception handler that matches the thrown exception. Regardless of whether there is a better matching handler or not. Therefore, we use this annotation to ensure that this exception handler is given priority over the general one.

Summary

This solution works well for me. I'm not sure that I have a better solution, and there may be Spring classes that I have not found that can help you. I hope this helps anyone who has the same or similar problems.

+4
source share

All Articles