RequestResponseBodyMethodProcessor
这个类应该是陌生的,在上面推荐的处理MVC返回值的文章中有提到过它:它能够处理@ResponseBody注解返回值(请参考它的supportsReturnType()方法~)
它还有另一个能力是:它能够处理请求参数(当然也是标注了@RequestBody它的~)
所以它既是个处理返回值的HandlerMethodReturnValueHandler,有是一个处理入参的HandlerMethodArgumentResolver。所以它命名为Processor而不是Resolver/Handler嘛,这就是命名的艺术~
// @since 3.1 public class RequestResponseBodyMethodProcessor extends AbstractMessageConverterMethodProcessor { @Override public boolean supportsParameter(MethodParameter parameter) { return parameter.hasParameterAnnotation(RequestBody.class); } // 类上或者方法上标注了@ResponseBody注解都行 @Override public boolean supportsReturnType(MethodParameter returnType) { return (AnnotatedElementUtils.hasAnnotation(returnType.getContainingClass(), ResponseBody.class) || returnType.hasMethodAnnotation(ResponseBody.class)); } // 这是处理入参封装校验的入口,也是本文关注的焦点 @Override public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception { // 它是支持`Optional`容器的 parameter = parameter.nestedIfOptional(); // 使用消息转换器HttpInputMessage把request请求转换出来,拿到值~~~ // 此处注意:比如本例入参是Person类,所以经过这里处理会生成一个空的Person对象出来(反射) Object arg = readWithMessageConverters(webRequest, parameter, parameter.getNestedGenericParameterType()); // 获取到入参的名称,其实不叫形参名字,应该叫objectName给校验时用的 // 请注意:这里的名称是类名首字母小写,并不是你方法里写的名字。比如本利若形参名写为personAAA,但是name的值还是person // 但是注意:`parameter.getParameterName()`的值可是personAAA String name = Conventions.getVariableNameForParameter(parameter); // 只有存在binderFactory才会去完成自动的绑定、校验~ // 此处web环境为:ServletRequestDataBinderFactory if (binderFactory != null) { WebDataBinder binder = binderFactory.createBinder(webRequest, arg, name); // 显然传了参数才需要去绑定校验嘛 if (arg != null) { // 这里完成数据绑定+数据校验~~~~~(绑定的错误和校验的错误都会放进Errors里) // Applicable:适合 validateIfApplicable(binder, parameter); // 若有错误消息hasErrors(),并且仅跟着的一个参数不是Errors类型,Spring MVC会主动给你抛出MethodArgumentNotValidException异常 // 否则,调用者自行处理 if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) { throw new MethodArgumentNotValidException(parameter, binder.getBindingResult()); } } // 把错误消息放进去 证明已经校验出错误了~~~ // 后续逻辑会判断MODEL_KEY_PREFIX这个key的~~~~ if (mavContainer != null) { mavContainer.addAttribute(BindingResult.MODEL_KEY_PREFIX + name, binder.getBindingResult()); } } return adaptArgumentIfNecessary(arg, parameter); } // 校验,如果合适的话。使用WebDataBinder,失败信息最终也都是放在它身上~ 本方法是本文关注的焦点 // 入参:MethodParameter parameter protected void validateIfApplicable(WebDataBinder binder, MethodParameter parameter) { // 拿到标注在此参数上的所有注解们(比如此处有@Valid和@RequestBody两个注解) Annotation[] annotations = parameter.getParameterAnnotations(); for (Annotation ann : annotations) { // 先看看有木有@Validated Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class); // 这个里的判断是关键:可以看到标注了@Validated注解 或者注解名是以Valid打头的 都会有效哦 //注意:这里可没说必须是@Valid注解。实际上你自定义注解,名称只要一Valid开头都成~~~~~ if (validatedAnn != null || ann.annotationType().getSimpleName().startsWith("Valid")) { // 拿到分组group后,调用binder的validate()进行校验~~~~ // 可以看到:拿到一个合适的注解后,立马就break了~~~ // 所以若你两个主机都标注@Validated和@Valid,效果是一样滴~ Object hints = (validatedAnn != null ? validatedAnn.value() : AnnotationUtils.getValue(ann)); Object[] validationHints = (hints instanceof Object[] ? (Object[]) hints : new Object[] {hints}); binder.validate(validationHints); break; } } } ... }
此处有很有意思的两点我总结一下:
- DataBinder使传入的objectName并不是形参名(形参名Spring是有这能力可以获取到的),而是类名首字母小写
- 关于此处给入参Person类赋值这块,和DataBinder的bind方法没有半毛钱关系~(都是MessageConverters去把值从Request里解析出来封装成对象的)
数据绑定的bind方法内建的只有一个地方用到了:ServletModelAttributeMethodProcessor。并且使用的还是子类ServletRequestDataBinder的bind(ServletRequest request)方法,父类的bind(PropertyValues pvs)内建都没有被调用过~~~
本文我们只着眼于关注@Valid的数据校验这块,有几个相关的使用小细节,总结如下:
- 形参(@RequestBody标注的入参)的name(形参名)和你写什么无关,若是实体类名字就是类名首字母小写。(若是数组、集合等,都会有自己特定的名称)
- @Validated和@Valid都能使校验生效,但却不仅仅是它哥俩才能行:任何名称是"Valid"打头的注解都能使得数据校验生效1. 自定义注解名称以Valid开头,并且给个value属性同样能够指定Group分组2. 个人直接建议使用@Validated即可,而去使用@Valid了,更不用自己给自己找麻烦去自定义注解啥的了~
- 只有当Errors(BindingResult)入参是是仅跟着@Valid注解的实体,Spring MVC才会把错误消息放权交给调用者处理,否则(没有或者不是紧挨着)它会抛出MethodArgumentNotValidException异常~
这是使用@RequestBody结合@Valid完成数据校验的基本原理。其实当Spring MVC在
处理@RequestPart注解入参数据时,也会执行绑定、校验的相关逻辑。对应处理器是RequestPartMethodArgumentResolver,原理大体上和这相似,它主要处理Multipart相关,本文忽略~
此处提示一个点,此文发出去后有一个好奇的小宝宝问我入参能使用多个对象并且都用@RequestBody标注吗?
关于这个问题姑且先不考虑合理与否,我们这样做试试:
@PostMapping("/hello") public Object helloPost(@Valid @RequestBody Person personAAA, BindingResult result, @Valid @RequestBody Person personBBB) { ... }
请求却报错了:
Resolved [org.springframework.http.converter.HttpMessageNotReadableException: I/O err
错误消息很好理解:请求域的Body体是只能被读取一次的(流只能被读取一次嘛)。
若你是好奇的,你可能还会问:URL参数呢?请求链接?后面的参数呢,如何封装???因为本部分内容不是本文的关注点,若有兴趣请出门左拐~
说明:关于使用Map、List、数组等接受@RequestBody参数的情况类似,区别在于绑定器上,对Map、List的校验前面文章有过讲解,此处就不展开了。
希望读者能掌握这部分内容,因为它和面向使用者比较重要的@InitBinder强关联~~~
实际使用中一般使用@Validated分组校验(若需要),然后结合全局异常的处理方式来友好的对调用者展示错误消息~
全局异常处理示例
当校验失败时,Spring
会抛出MethodArgumentNotValidException
异常,该异常会持有校验结果对象BindingResult
,从而获得校验失败信息。本处只给示例,仅供参考:
@RestControllerAdvice public class MethodArgumentNotValidExceptionHandler { @ExceptionHandler(MethodArgumentNotValidException.class) public Result handleMethodArgumentNotValid(MethodArgumentNotValidException ex) { BindingResult bindingResult = ex.getBindingResult(); StringBuilder stringBuilder = new StringBuilder(); for (FieldError error : bindingResult.getFieldErrors()) { String field = error.getField(); Object value = error.getRejectedValue(); String msg = error.getDefaultMessage(); String message = String.format("错误字段:%s,错误值:%s,原因:%s;", field, value, msg); stringBuilder.append(message).append("\r\n"); } return Result.error(MsgDefinition.ILLEGAL_ARGUMENTS.codeOf(), stringBuilder.toString()); } }