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) {
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 {
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.