Spring 50例常见错误(十一)

文章整理来源:Spring编程常见错误50例_spring_spring编程_bean_AOP_SpringCloud_SpringWeb_测试_事务_Data-极客时间

 案例28:请求参数未标记 @Vaild 使校验生效

        对请求的内容进行校验,但实际测试却没有做拦截校验

import javax.validation.constraints.Size;
@Data
public class Student {
    @Size(max = 10)
    private String name;
    private short age;
}
--------------------------------------------------
@RestController
@Slf4j
@Validated
public class StudentController {
    @RequestMapping(path = "students", method = RequestMethod.POST)
    public void addStudent(@RequestBody Student student){
        log.info("add new student: {}", student.toString());
        //省略业务代码
    };
}

        解析:Web 的请求处理时序图

        当一个请求来临时,都会进入 DispatcherServlet,执行其 doDispatch(),此方法会根据 Path、Method 等关键信息定位到负责处理的 Controller 层方法(即 addStudent 方法),然后通过反射去执行这个方法,具体反射执行过程参考下面的代码(InvocableHandlerMethod#invokeForRequest)

public Object invokeForRequest(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer,
      Object... providedArgs) throws Exception {
   //根据请求内容和方法定义获取方法参数实例
   Object[] args = getMethodArgumentValues(request, mavContainer, providedArgs);
   if (logger.isTraceEnabled()) {
      logger.trace("Arguments: " + Arrays.toString(args));
   }
   //携带方法参数实例去“反射”调用方法
   return doInvoke(args);
}

        在 invokeForRequest 的 getMethodArgumentValues() 方法中,会找出处理请求内容和方法合适的解析器 Resolver ,并执行 resolveArgument 方法。根据当前的请求(NativeWebRequest)组装出 Student 对象并对这个对象进行必要的校验,校验的执行参考 AbstractMessageConverterMethodArgumentResolver#validateIfApplicable

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;
      }
   }
}

        要使请求参数进行校验必须匹配下面两个条件的其中之一:

        1. 标记了 org.springframework.validation.annotation.Validated 注解;

        2. 标记了其他类型的注解,且注解名称以 Valid 关键字开头。

        解决:1. 标记 @Validated 或 @Valid

public void addStudent(@Validated @RequestBody Student student)

------------------------------------------------------------

public void addStudent(@Valid @RequestBody Student student)

                2. 使用自定义的 Vaild 开头的注解,注意要显式标记 @Retention(RetentionPolicy.RUNTIME),否则校验仍不生效

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
// 要显式标记 @Retention(RetentionPolicy.RUNTIME),否则校验仍不生效
@Retention(RetentionPolicy.RUNTIME)
public @interface ValidCustomized {
}
---------------------------------------------------------------
public void addStudent(@ValidCustomized @RequestBody Student student)

案例29:级联属性对象未标注校验

        定义 Phone 对象,并关联上 Student 对象,但 Phone 的约束检验却没有生效

public class Student {
    @Size(max = 10)
    private String name;
    private short age;   
    private Phone phone;
}
@Data
class Phone {
    @Size(max = 10)
    private String number;
}

        解析:在上述执行校验的方法 binder.validate()  中,会根据 Student 类型组装出 BeanMetaData,并根据成员字段是否标记了 @Valid 来决定(记录)这个字段以后是否做级联校验,参考代码 AnnotationMetaDataProvider#getCascadingMetaData

@Override
public final <T> Set<ConstraintViolation<T>> validate(T object, Class<?>... groups) {
   //省略部分非关键代码
   Class<T> rootBeanClass = (Class<T>) object.getClass();
   //获取校验对象类型的“信息”(包含“约束”)
   BeanMetaData<T> rootBeanMetaData = beanMetaDataManager.getBeanMetaData( rootBeanClass );

   if ( !rootBeanMetaData.hasConstraints() ) {
      return Collections.emptySet();
   }

   //省略部分非关键代码
   //执行校验
   return validateInContext( validationContext, valueContext, validationOrder );
}
----------------------------------------------------------------

private CascadingMetaDataBuilder getCascadingMetaData(Type type, AnnotatedElement annotatedElement,
      Map<TypeVariable<?>, CascadingMetaDataBuilder> containerElementTypesCascadingMetaData) {
   return CascadingMetaDataBuilder.annotatedObject( type, annotatedElement.isAnnotationPresent( Valid.class ), containerElementTypesCascadingMetaData,
               getGroupConversions( annotatedElement ) );
}

        因此由于案例中 Student 的 phone 字段并没有被 @Valid 标记,所以关于这个字段信息的 cascading 属性肯定是 false,因此在校验 Student 时并不会级联校验它

        解决:在  Student 的 phone 字段上标记 @Valid 

public class Student {
    @Size(max = 10)
    private String name;
    private short age;  
    @Valid 
    private Phone phone;
}
@Data
class Phone {
    @Size(max = 10)
    private String number;
}

案例30:未明确校验的范围

        想要用 @Size(min = 1, max = 10) 去限制 name 字段非空,却限制不了 name 字段未 null

@Size(min = 1, max = 10)
private String name;

        解析:@Size 约束的执行方法,参考 SizeValidatorForCharSequence#isValid 方法

public boolean isValid(CharSequence charSequence, ConstraintValidatorContext constraintValidatorContext) {
      if ( charSequence == null ) {
         return true;
      }
      int length = charSequence.length();
      return length >= min && length <= max;
   }

        当字符串为 null 时,直接通过了校验,而不会做任何进一步的约束检查

        解决:添加 @NotNull 或 @NotEmpty 来加强约束

@NotEmpty
@Size(min = 1, max = 10)
private String name;

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值