- 一、开发使用`@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
最后会把BindingResult结果放到ModelAndViewContainer对象中保存起来,记住BindingResult.MODEL_KEY_PREFIX这个key prefix。
BindingResult结果也已经拿到了,该怎么传递给方法呢?
InvocableHandlerMethod.getMethodArgumentValues()请求,通过DispatcherServlet.doDispatch() ,
RequestHandlerMappingAdapter,
InvocableHandlerMethod.getMethodArgumentValues()为请求获取入参信息,
HandlerMethodArgumentResolverComposite#resolveArgument获取对应方法解析器RequestResponseBodyMethodProcessor,并解析方法参数,
这里遍历方法参数,逐个解析,当解析完@Validated @RequestBody RequestData
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());
}



