栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

SpringBoot @Validated原理解析

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

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,参数校验逻辑在其中。


    org.springframework.boot
    spring-boot-starter-web
    
        
            jackson-databind
            com.fasterxml.jackson.core
        
    

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 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进行入参属性绑定,然后再进行校验。

@Override
	public 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> localViolations中,接着设置到 constraintViolations 中,

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

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

请求,通过DispatcherServlet.doDispatch() ,

RequestHandlerMappingAdapter,

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

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

InvocableHandlerMethod.getMethodArgumentValues()

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

	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(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());
}
转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/424528.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号