文章目录

  • 一、开发使用`@Validated` 出现问题
    • 1.1 代码
    • 1.2 请求
    • 1.3 响应
  • 二、源码:
    • RequestResponseBodyMethodProcessor.resolveArgument()
    • ConstraintTree#validateSingleConstraint
    • InvocableHandlerMethod.getMethodArgumentValues()
    • ErrorsMethodArgumentResolver.resolveArgument

SpringBoot @Validated原理解析

一、开发使用@Validated 出现问题

开发过程中遇到一个问题:通过@RequestBody发送post请求的接口,接收参数为 TOUserAppModifyAddrReq,TOUserAppModifyAddrReq 有一个父类ToModifyAddrReq,调用接口时报错``

1.1 代码

引入依赖:

后边代码分析中会用到其中依赖传递的hibernate-validator-5.3.6.Final.jar,参数校验逻辑在其中。

<!-- spring-boot web依赖 -->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId><exclusions><exclusion><artifactId>jackson-databind</artifactId><groupId>com.fasterxml.jackson.core</groupId></exclusion></exclusions>
</dependency>

TOUserAppModifyAddrReq.java

public class ToUserAppModifyAddrReq extends ToModifyAddrReq{@NotNull(message="订单号不能为空z")private  String escOrderId;@NotNull(message="申请类型不能为空")private  String applyType;@NotNull(message="购买人ID不能为空")private  String buyerOnlyId;public String getBuyerOnlyId() {return buyerOnlyId;}public void setBuyerOnlyId(String buyerOnlyId) {this.buyerOnlyId = buyerOnlyId;}public String getEscOrderId() {return escOrderId;}public void setEscOrderId(String escOrderId) {this.escOrderId = escOrderId;}public String getApplyType() {return applyType;}public void setApplyType(String applyType) {this.applyType = applyType;}
}

ToModifyAddrReq.java

public class ToModifyAddrReq {@NotNull(message = "订单号不能为空f")private  String escOrderId;private  String applyType;private  String sellerOnlyId;public String getSellerOnlyId() {return sellerOnlyId;}public void setSellerOnlyId(String sellerOnlyId) {this.sellerOnlyId = sellerOnlyId;}public String getEscOrderId() {return escOrderId;}public void setEscOrderId(String escOrderId) {this.escOrderId = escOrderId;}public String getApplyType() {return applyType;}public void setApplyType(String applyType) {this.applyType = applyType;}
}

Controller 代码如下:

这里使用@Validated 校验入参,@RequestBody

@ResponseBody
@RequestMapping("/toUserModifyAddressPage")
public ReturnData toUserModifyAddressPage(@Validated @RequestBody RequestData<ToUserAppModifyAddrReq> req, BindingResult bindingResult){……if(bindingResult.hasErrors()){log.warn("toMerchantModifyAddressPage illegal params:" + bindingResult.getAllErrors().get(0).getDefaultMessage());}……
}

1.2 请求

curl --location --request POST 'http://retailorder-ordersignandcancelservice.http.beta.uledns.com/orderSignAndCancelService/order/toUserModifyAddressPage.do' \
--header 'User-Agent: Apipost client Runtime/+https://www.apipost.cn/' \
--header 'Content-Type: application/json' \
--data '{"head":{"requestTime":1635924914426,"requestId":"fa774d76e52649499043204a1ecc0a01","moduleApp":"my-myShoppingOrderWeb"},"dataBody":{"applyType":"USER","escOrderId":"621073000058578405","buyerOnlyId":10000040365}
}'

1.3 响应

{"result":null,"returnCode":"0002","returnMsg":"订单号不能为空f"}

这里看到,校验成功,显示订单号不能为空f, 这里为什么出现这个错误呢? 原因是,@Validated 校验中,会把参数父类中的字段escOrderId 也校验,但是父类中的escOrderId字段值为空, 这里其实是代码有问题,继承写的有问题,不需要重写父类属性,修改下即可。

但具体为什么如此,我下面分析下代码,,,,

二、源码:

​ 接口接收请求是通过 @RequestBody, spring的方法参数解析器(HandlerMethodArgumentResolver),参数校验这块肯定是在对应的方法参数解析器里执行的。如下是@RequestBody注解对应的参数解析器RequestResponseBodyMethodProcessor。

RequestResponseBodyMethodProcessor.resolveArgument()

​ 直接定位到resolveArgument这个方法,很明显,该方法是根据参数类型找到支持的消息转换器(Message Converter),然后从request body中读取信息,最后转换成对应的参数实体。

​ WebDataBinder主要是完成对象属性校验的。如果你熟悉@ModelAttribute注解对应的方法参数解析器(ModelAttributeMethodProcessor),是先通过WebDataBinder进行入参属性绑定,然后再进行校验。

@Overridepublic Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {parameter = parameter.nestedIfOptional();//消息转换Object arg = readWithMessageConverters(webRequest, parameter, parameter.getNestedGenericParameterType());String name = Conventions.getVariableNameForParameter(parameter);if (binderFactory != null) {WebDataBinder binder = binderFactory.createBinder(webRequest, arg, name);if (arg != null) {//遍历参数注解validateIfApplicable(binder, parameter);//如果校验结果有异常,且目标方法中最后有Errors(BindingResult 继承 Errors)类型的参数,则抛出异常if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) {throw new MethodArgumentNotValidException(parameter, binder.getBindingResult());}}if (mavContainer != null) {// BindingResult结果,放入 ModelAndViewContainer 对象中保存起来mavContainer.addAttribute(BindingResult.MODEL_KEY_PREFIX + name, binder.getBindingResult());}}return adaptArgumentIfNecessary(arg, parameter);}

​ 简单说一下validateIfApplicable方法的逻辑,遍历当前参数methodParam所有的注解,如果注解是@Validated或注解的名字以‘Valid’开头,则使用WebDataBinder对象执行校验逻辑。

//简单说一下validateIfApplicable方法的逻辑,遍历当前参数methodParam所有的注解,
// 如果注解是@Validated或注解的名字以‘Valid’开头,则使用WebDataBinder对象执行校验逻辑。
protected void validateIfApplicable(WebDataBinder binder, MethodParameter parameter) {Annotation[] annotations = parameter.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;}}
}

ConstraintTree#validateSingleConstraint

通过 hibernate-validator-5.3.6.Final-sources.jar 对参数进行校验,下面是部分代码

org.hibernate.validator.internal.engine.constraintvalidation.ConstraintTree#validateSingleConstraint ,判断valueContextcurrentValue中是否有值:

子类中对应的字段:

父类中对应的字段:

这里可以看出父类中的字符值为空, 且父类中设置了 @NotNull 注解 ,此处就会返回到 Set<ConstraintViolation<T>> localViolations中,接着设置到 constraintViolations 中,

​ 最后会把BindingResult结果放到ModelAndViewContainer对象中保存起来,记住BindingResult.MODEL_KEY_PREFIX这个key prefix。

BindingResult结果也已经拿到了,该怎么传递给方法呢?

请求,通过DispatcherServlet.doDispatch()

RequestHandlerMappingAdapter

InvocableHandlerMethod.getMethodArgumentValues()为请求获取入参信息,

HandlerMethodArgumentResolverComposite#resolveArgument获取对应方法解析器RequestResponseBodyMethodProcessor,并解析方法参数,

InvocableHandlerMethod.getMethodArgumentValues()

这里遍历方法参数,逐个解析,当解析完@Validated @RequestBody RequestData<ToUserAppModifyAddrReq> req参数,进入参数BindingResult bindingResult的解析,

/*** Get the method argument values for the current request.*/private Object[] getMethodArgumentValues(NativeWebRequest request, ModelAndViewContainer mavContainer,Object... providedArgs) throws Exception {MethodParameter[] parameters = getMethodParameters();Object[] args = new Object[parameters.length];// 不同参数,不同的参数解析器for (int i = 0; i < parameters.length; i++) {MethodParameter parameter = parameters[i];parameter.initParameterNameDiscovery(this.parameterNameDiscoverer);args[i] = resolveProvidedArgument(parameter, providedArgs);if (args[i] != null) {continue;}if (this.argumentResolvers.supportsParameter(parameter)) {try {args[i] = this.argumentResolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory);continue;}catch (Exception ex) {if (logger.isDebugEnabled()) {logger.debug(getArgumentResolutionErrorMessage("Failed to resolve", i), ex);}throw ex;}}if (args[i] == null) {throw new IllegalStateException("Could not resolve method parameter at index " +parameter.getParameterIndex() + " in " + parameter.getMethod().toGenericString() +": " + getArgumentResolutionErrorMessage("No suitable resolver for", i));}}return args;}

通过BindingResult bindingResult参数,获取到ErrorsMethodArgumentResolver 解析器。

private HandlerMethodArgumentResolver getArgumentResolver(MethodParameter parameter) {HandlerMethodArgumentResolver result = this.argumentResolverCache.get(parameter);if (result == null) {for (HandlerMethodArgumentResolver methodArgumentResolver : this.argumentResolvers) {if (logger.isTraceEnabled()) {logger.trace("Testing if argument resolver [" + methodArgumentResolver + "] supports [" +parameter.getGenericParameterType() + "]");}if (methodArgumentResolver.supportsParameter(parameter)) {result = methodArgumentResolver;this.argumentResolverCache.put(parameter, result);break;}}}return result;}

ErrorsMethodArgumentResolver.resolveArgument

​ 看到ErrorsMethodArgumentResolver这个参数解析器的注释和源码,的确是针对BindingResult这种参数类型的。BindingResult.MODEL_KEY_PREFIX这个常量在这里出现了,在ModelAndViewContainer对象中拿到BindingResult对象。注意最后面抛出了一个IllegalStateException异常,也就是在ModelAndViewContainer对象中没有找到BindingResult对象的时候才会抛出这个异常。

@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {ModelMap model = mavContainer.getModel();if (model.size() > 0) {int lastIndex = model.size()-1;String lastKey = new ArrayList<String>(model.keySet()).get(lastIndex);if (lastKey.startsWith(BindingResult.MODEL_KEY_PREFIX)) {return model.get(lastKey);}}throw new IllegalStateException("An Errors/BindingResult argument is expected to be declared immediately after the model attribute, " +"the @RequestBody or the @RequestPart arguments to which they apply: " + parameter.getMethod());
}

SpringBoot @Validated原理解析相关推荐

  1. Springboot启动原理解析

    点击上方"方志朋",选择"置顶公众号" 技术文章第一时间送达! 我们开发任何一个Spring Boot项目,都会用到如下的启动类 @SpringBootAppl ...

  2. SpringBoot自动装配原理解析——面试可以这样会回答

    1. 前言 SpringBoot是目前软件中最主流的框架,无论是工作还是面试基本都有它的身影,SpringBoot主要解决了传统spring的重量级xml配置Bean,实现了自动装配:所以,我们也常在 ...

  3. 轻松理解之SpringBoot实现原理

    一.什么是SpringBoot? SpringBoot是一个快速开发框架,快速的将一些常用的第三方依赖整合(原理:通过Maven子父工程的方式),简化XML配置,全部采用注解形式,内置Http服务器( ...

  4. Spring Boot:(二)启动原理解析

    Spring Boot:(二)启动原理解析 前言 前面几章我们见识了SpringBoot为我们做的自动配置,确实方便快捷,但是对于新手来说,如果不大懂SpringBoot内部启动原理,以后难免会吃亏. ...

  5. Spring Boot(18)---启动原理解析

    Spring Boot(18)---启动原理解析 前言 前面几章我们见识了SpringBoot为我们做的自动配置,确实方便快捷,但是对于新手来说,如果不大懂SpringBoot内部启动原理,以后难免会 ...

  6. SpringBoot核心原理:自动配置、事件驱动、Condition

    点击关注公众号,实用技术文章及时了解 来源:blog.csdn.net/l6108003/article/ details/106966386 前言 SpringBoot是Spring的包装,通过自动 ...

  7. springboot启动原理及其流程

    springboot启动原理精简版 spring,springMVC和spring有什么区别? 一 springboot启动流程及其相关流程概览. 二 springboot的启动类入口 三 sprin ...

  8. java网关限流_基于网关GateWay实现限流-令牌桶 及原理解析

    一.使用流程 1) 引入坐标 org.springframework.boot spring-boot-starter-data-redis-reactive 2.1.3.RELEASE 2) 创建b ...

  9. SpringMVC @RequestBody和@ResponseBody原理解析

    SpringMVC @RequestBody和@ResponseBody原理解析 前言 @RequestBody作用是将http请求解析为对应的对象.例如: http请求的参数(application ...

最新文章

  1. 虎牙直播在微服务改造方面的实践和总结
  2. python将数据存入数据库_Python读取NGINX日志将其存入数据库
  3. vmware安装渗透系统 Linux Kail最新版
  4. PHP设计模式之建造者模式
  5. 万元大奖,FlyAI算法新赛事,心理卡牌目标检测
  6. 点石成金-3-超市大亨
  7. 华为云再“祭”神器!
  8. TCP/IP 基础简介
  9. HBase学习之路 (四)HBase的API操作
  10. 解决ajaxSubmit无法传递自动回填和下拉框的数据
  11. lvds接口屏线安装图解_lvds液晶屏幕接口详解
  12. 夜曲歌词 拼音_夜曲歌词 周杰伦夜曲LRC歌词_九酷音乐
  13. 18年一剑!德州心脏研究所研制出磁悬浮心脏,每秒2000转,为心衰患者续命
  14. unity2D:视觉差Parallex
  15. 2019年日历假期添加
  16. ggplot2绘制数据分布crossbar图教程
  17. 半加器 全加器 Verilog描述
  18. 7-95 深入虎穴 (树的深搜)
  19. Django个人博客搭建4-配置使用 Bootstrap 4 改写模板文件
  20. centos6.8经典实用大全、教程

热门文章

  1. 分享下我这些天的戒烟心得
  2. 联想yoga14s跑matlab,2021开学季高性价比轻薄笔记本推荐_TOM资讯
  3. 欧洲fba海运详解:欧洲fba海运怎么样?有哪些优势?
  4. 一个程序员关注的微信公众账号
  5. 阿里云OSS---阿里云对象存储服务
  6. Linux个性化命令行登陆提示文字
  7. 抖音、腾讯、阿里、美团春招服务端开发岗位硬核面试(完结)
  8. 基于opencv的自动祛斑算法
  9. Android脱离USB执行Shell脚本的方法
  10. 倒计时进度条动态效果