案例代码:spring-web-validation-demo
请区分规范和实现
名词解释:
javax.validation
包org.springframework.validation
包,Designed for convenient use with Spring’s JSR-303 support but not JSR-303 specific。hibernate-validator
hibernate-validator
原文请参考 JSR 303: Bean Validation。
Goals: To avoid duplication of these validations from the presentation layer to the persistence layer. This JSR defines a metadata model and API for JavaBean validation. not limited to web tier or persistence tier, available for both server-side application and client Swing application.
该规范主要包含以下几个方面:
约束主要围绕@Constraint
注解和javax.validation.ConstraintValidator
接口展开。
一个校验规则由一个 javax.validation.Constraint
注解 和 一组 javax.validation.ConstraintValidator
接口实现类组成。
校验过程中会出现两类异常 :
IllegalArgumentException
(Spring Web此处做了处理,将抛出MethodArgumentNotValidException
,在统一异常处理时需要注意,参见RequestResponseBodyMethodProcessor
)javax.validation.ValidationException
而观察javax.validation.Constraint
发现它只能注解在注解上,等于它是一个元注解,而我们放在参数上标识校验的注解需要自行编码提供。javax提供了几个常用的如:@Email
@Max
@Min
@NotEmpty
@NotNull
,而更复杂的业务校验注解通过自定义注解实现,如下:
// 自定义ConstraintValidator
@Constraint(validatedBy = SignupUserInfoConstraintValidator.class)
// 规范强调必须支持FIELD METHOD TYPE ANNOTAION_TYPE,其他的不做要求,是否支持得看具体实现
@Target({ElementType.METHOD,ElementType.TYPE,ElementType.FIELD,ElementType.PARAMETER})
// 必须为RUNTIME
@Retention(RetentionPolicy.RUNTIME)
public @interface SignupUserInfoConstraint {
// 添加自定义字段
boolean enabled() default false;
// ***************************************
// message, groups and payload 三个必要字段
// ***************************************
// 错误提示信息message建议写成"全类名.message" 主要是为了i18n
String message() default "{com.example.validation.annotation.SignupUserInfoConstraint.message}";
// 默认必须为空
// groups通常用来控制constraint执行顺序,或者对javabean分组做校验
Class<?>[] groups() default {};
// 默认必须为空
// payload通常用来关联元信息,比如用内部类标识,用Class而不用string主要是为了易用和类型安全
Class<? extends Payload>[] payload() default {};
}
关于更多示例,请参考规范原文。
校验参数的标记由自定义注解完成,而校验逻辑需要自定义javax.validation.ConstraintValidator
接口实现类:
// 校验注册用户信息
public class SignupUserInfoConstraintValidator implements ConstraintValidator<SignupUserInfoConstraint, UserInfo> {
private boolean enabled = false;
@Override
public void initialize(SignupUserInfoConstraint constraintAnnotation) {
this.enabled = constraintAnnotation.enabled();
}
@Override
public boolean isValid(UserInfo userInfo, ConstraintValidatorContext context) {
...
}
}
注意:关于该接口的泛型建议使用 raw type
Consider the situation where bean X contains a field of type Y. By annotating field Y with the @Valid annotation, the Validator will validate Y (and its properties) when X is validated.
The @Valid annotation is applied recursively.
案例如以上的SignupBody
中的SignupSource
字段
另外对于所有注解了@Valid的Collection-valued, array-valued and Iterable fields and properties会校验其内容(但Map的key不会被校验)
During the validation call, one or more groups are validated. All the constraints belonging to this set of group is evaluated on the object graph.
@Valid
注解 在Spring Validation的实现上不支持分组;需要用Spring Validation自己的@Validated
注解,或自定义Valid开头的注解并添加Class[] value()
字段
注意:这里填的Class类型只能是接口的Class
以上的场景是基于web的,实际上不管是Jsr 303规范本身的目的还是Spring Validation中提供的Jsr 303支持都不仅仅可以做web数据校验,像Swing或Ui层等都可以进行校验。Spring Validation更像是一种适配Spring体系的领域建模。
关于Spring Validation的关系,我们可以参看 Spring Validation提供的包说明:
package | package-info |
---|---|
org.springframework.validation | Provides data binding and validation functionality, for usage in business and/or UI layers. |
org.springframework.validation.annotation | Support classes for annotation-based constraint evaluation, e.g. using a JSR-303 Bean Validation provider. Provides an extended variant of JSR-303’s @Valid supporting the specification of validation groups. |
org.springframework.validation.beanvalidation | Support classes for integrating a JSR-303 Bean Validation provider (such as Hibernate Validator) into a Spring ApplicationContext and in particular with Spring’s data binding and validation APIs. The central class is LocalValidatorFactoryBean which defines a shared ValidatorFactory /Validator setup for availability to other Spring components. |
org.springframework.validation.support | Support classes for handling validation results. |
可以看到,Spring Validation的目的有两个:
以下源码流程以Spring Boot+Spring Web为例,Spring Web利用Spring Validation来校验请求体和响应体中的内容,即Spring Web提供对@RequestBody
和@ResponseBody
注解的参数的校验,且校验过程兼容Jsr303,至于Jsr303的实现是什么,Spring Web不在乎,像这里我引入spring-boot-starter-validation
,里面Jsr303的实现就是hibernate-validator
。
该类用于解析@RequestBody
的方法参数和@ResponseBody
的返回值,利用HttpMessageConverter
将请求体和响应体内容解析并生成对应的DTO和VO,在解析过程中会利用Spring Validation来完成校验。
public class RequestResponseBodyMethodProcessor extends AbstractMessageConverterMethodProcessor {
...
@Override
public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
...
// web数据绑定类,即将请求体内容绑定到@RequestBody注解的实体类上
// WebDataBinder父类DataBinder中组合了List
WebDataBinder binder = binderFactory.createBinder(...);
// 父抽象类AbstractMessageConverterMethodArgumentResolver中的方法,这里直接卸载该类中方便查看
// 用binder进行校验并完成数据绑定
validateIfApplicable(binder, parameter);
if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) {
// 捕获异常,抛出MethodArgumentNotValidException
throw new MethodArgumentNotValidException(parameter, binder.getBindingResult());
}
}
// 父抽象类AbstractMessageConverterMethodArgumentResolver的方法
protected void validateIfApplicable(WebDataBinder binder, MethodParameter parameter) {
Annotation[] annotations = parameter.getParameterAnnotations();
for (Annotation ann : annotations) {
// 利用org.springframework.validation.annotation.ValidationAnnotationUtils判断是否进行校验
// validationHints是指定的groups,必须为接口的class
// ValidationAnnotationUtils.determineValidationHints方法如果是@Valid直接返回空数组,所以@Valid不支持分组
// 而自定义Valid开头注解,该方法提供处理,是可以的
Object[] validationHints = ValidationAnnotationUtils.determineValidationHints(ann);
if (validationHints != null) {
// 利用WebDataBinder中的List进行校验
binder.validate(validationHints);
break;
}
}
}
...
}
该类属于Spring Validation,不是Spring Web的。
public abstract class ValidationAnnotationUtils {
...
/**
* 以下情况需要校验:
* 1. jsr303规范的javax.validation.Valid注解需要
* 2. Spring的org.springframework.validation.annotation.Validated注解需要
* 3. 自定义以"Valid"开头的注解需要
*/
public static Object[] determineValidationHints(Annotation ann) {
Class<? extends Annotation> annotationType = ann.annotationType();
String annotationName = annotationType.getName();
// 1.
if ("javax.validation.Valid".equals(annotationName)) {
// 直接返会空数组,不支持分组
return EMPTY_OBJECT_ARRAY;
}
// 2.
Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class);
if (validatedAnn != null) {
Object hints = validatedAnn.value();
// 支持分组
return convertValidationHints(hints);
}
// 3.
if (annotationType.getSimpleName().startsWith("Valid")) {
Object hints = AnnotationUtils.getValue(ann);
// 支持分组
return convertValidationHints(hints);
}
return null;
}
...
}
public class DataBinder implements PropertyEditorRegistry, TypeConverter {
...
public void validate(Object... validationHints) {
// 获取被校验对象,如SignupBody
Object target = getTarget();
// 绑定的结果
BindingResult bindingResult = getBindingResult();
// 开始校验
// getValidators用SpringBooot的话,只有一个ValidatorAdapter implements SmartValidator
for (Validator validator : getValidators()) {
// SmartValidator是Validator的扩展,也是spring的,adding support for validation 'hints'
// 但是这里如果是"javax.validation.Valid"注解的hints是空,所以走下面的else
// 这里实现了jsr 303的@Valid 和 spring的@Validated注解处理的分流
if (!ObjectUtils.isEmpty(validationHints) && validator instanceof SmartValidator) {
((SmartValidator) validator).validate(target, bindingResult, validationHints);
}
else if (validator != null) {
// ValidatorAdaptor#validate -> SpringValidator#validate -> javax.validation.Validator#validate
// javax.validation.Validator#validate默认实现类org.hibernate.validator.internal.engine.ValidatorImpl#validate返回一系列ConstraintViolation
// ConstraintViolation代表着校验结果
validator.validate(target, bindingResult);
}
}
}
...
}