SpringBoot @Validated原理解析
文章目录
- 一、开发使用`@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
,判断valueContext
中 currentValue
中是否有值:
子类中对应的字段:
父类中对应的字段:
这里可以看出父类中的字符值为空, 且父类中设置了 @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原理解析相关推荐
- Springboot启动原理解析
点击上方"方志朋",选择"置顶公众号" 技术文章第一时间送达! 我们开发任何一个Spring Boot项目,都会用到如下的启动类 @SpringBootAppl ...
- SpringBoot自动装配原理解析——面试可以这样会回答
1. 前言 SpringBoot是目前软件中最主流的框架,无论是工作还是面试基本都有它的身影,SpringBoot主要解决了传统spring的重量级xml配置Bean,实现了自动装配:所以,我们也常在 ...
- 轻松理解之SpringBoot实现原理
一.什么是SpringBoot? SpringBoot是一个快速开发框架,快速的将一些常用的第三方依赖整合(原理:通过Maven子父工程的方式),简化XML配置,全部采用注解形式,内置Http服务器( ...
- Spring Boot:(二)启动原理解析
Spring Boot:(二)启动原理解析 前言 前面几章我们见识了SpringBoot为我们做的自动配置,确实方便快捷,但是对于新手来说,如果不大懂SpringBoot内部启动原理,以后难免会吃亏. ...
- Spring Boot(18)---启动原理解析
Spring Boot(18)---启动原理解析 前言 前面几章我们见识了SpringBoot为我们做的自动配置,确实方便快捷,但是对于新手来说,如果不大懂SpringBoot内部启动原理,以后难免会 ...
- SpringBoot核心原理:自动配置、事件驱动、Condition
点击关注公众号,实用技术文章及时了解 来源:blog.csdn.net/l6108003/article/ details/106966386 前言 SpringBoot是Spring的包装,通过自动 ...
- springboot启动原理及其流程
springboot启动原理精简版 spring,springMVC和spring有什么区别? 一 springboot启动流程及其相关流程概览. 二 springboot的启动类入口 三 sprin ...
- java网关限流_基于网关GateWay实现限流-令牌桶 及原理解析
一.使用流程 1) 引入坐标 org.springframework.boot spring-boot-starter-data-redis-reactive 2.1.3.RELEASE 2) 创建b ...
- SpringMVC @RequestBody和@ResponseBody原理解析
SpringMVC @RequestBody和@ResponseBody原理解析 前言 @RequestBody作用是将http请求解析为对应的对象.例如: http请求的参数(application ...
最新文章
- 虎牙直播在微服务改造方面的实践和总结
- python将数据存入数据库_Python读取NGINX日志将其存入数据库
- vmware安装渗透系统 Linux Kail最新版
- PHP设计模式之建造者模式
- 万元大奖,FlyAI算法新赛事,心理卡牌目标检测
- 点石成金-3-超市大亨
- 华为云再“祭”神器!
- TCP/IP 基础简介
- HBase学习之路 (四)HBase的API操作
- 解决ajaxSubmit无法传递自动回填和下拉框的数据
- lvds接口屏线安装图解_lvds液晶屏幕接口详解
- 夜曲歌词 拼音_夜曲歌词 周杰伦夜曲LRC歌词_九酷音乐
- 18年一剑!德州心脏研究所研制出磁悬浮心脏,每秒2000转,为心衰患者续命
- unity2D:视觉差Parallex
- 2019年日历假期添加
- ggplot2绘制数据分布crossbar图教程
- 半加器 全加器 Verilog描述
- 7-95 深入虎穴 (树的深搜)
- Django个人博客搭建4-配置使用 Bootstrap 4 改写模板文件
- centos6.8经典实用大全、教程