文章整理来源: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;