目录
前言
一、基本用法
@RequestBody参数校验
@RequestParam/@PathVariable参数校验
编程式校验
二、进阶用法
自定义验证注解
多属性联合校验
嵌套校验
三、实现原理
@RequestBody参数校验实现原理
@RequestParam/@PathVariable参数校验实现原理
项目源码
附件
前言
平时服务端开发过程中,不可避免的需要对接口参数进行校验,比较常见的比如用户名不能为空、年龄必须大于0、邮箱格式要合规等等。如果通过if else去校验参数,校验代码会跟业务耦合,且显得很冗长。SpringBoot提供了一种简洁、高效的方式,通过@Validated/@Valid注解来做参数校验,大大提高了工作效率
一、基本用法
总共三种方式:
- Controller的@RequestBody参数校验
- Controller的@RequestParam/@PathVariable参数校验
- 编程式校验,直接调用hibernate的validate方法
三种方式都需要加上以下依赖。里面有所需的jakarta.validation-api和hibernate-validator包
org.springframework.boot
spring-boot-starter-validation
-
@RequestBody参数校验
该方式适用于Controller中POST/PUT方法的参数校验,校验失败会抛MethodArgumentNotValidException
1.首先在参数类的属性上声明约束注解,比如@NotBlank、@Email等
@Data
public class UserVo implements Serializable {
@NotBlank(message = "名字不能为空")
@Size(min = 2, max = 50, message = "名字长度的范围为2~50")
private String name;
@Email(message = "邮箱格式不对")
private String email;
@NotNull(message = "年龄不能为空")
@Min(18)
@Max(100)
private Integer age;
@NotEmpty(message = "照片不能为空")
private List photoList;
}
2.接着在Controller方法@RequestBody旁加上@Validated注解
@Slf4j
@RestController
public class UserController {
@ApiOperation("保存用户")
@PostMapping("/save/user")
public Result saveUser(@RequestBody @Validated UserVo user) {
return Result.ok();
}
}
-
@RequestParam/@PathVariable参数校验
该方式适用于Controller中GET方法的参数校验,校验失败会抛ConstraintViolationException。它是通过类上加@Validated注解,方法参数前加@NotBlank等约束注解的方式来实现的,所以其它Spring Bean的方法也适用
1.Controller类上加@Validated注解;@RequestParam/@PathVariable旁加上@NotBlank、@Max等注解
@Slf4j
@RestController
@Validated
public class UserController {
@ApiOperation("查询用户")
@GetMapping("/list/user")
public Result> listUser(
@Min(value = 100, message = "id不能小于100") @RequestParam("id") Long id,
@NotBlank(message = "名称不能为空") @RequestParam("name") String name,
@Max(value = 90, message = "年龄不能大于90") @RequestParam("age") Integer age) {
List list = new ArrayList();
return Result.ok(list);
}
}
-
编程式校验
该方式适用于Service参数的校验,校验失败手动抛ValidationException
1.通过@bean注解初始化Validator对象
public class ValidatorConfig {
@Bean
public Validator validator() {
return Validation.byProvider(HibernateValidator.class)
.configure()
// 快速失败模式
.failFast(true)
.buildValidatorFactory()
.getValidator();
}
}
2.在Service方法中调用hibernate的validate方法对参数进行校验
@Service
@Slf4j
public class UserService {
@Autowired
private Validator validator;
public boolean editUser(UserVo user) {
Set> validateSet = validator.validate(user);
if (CollectionUtils.isNotEmpty(validateSet)) {
StringBuilder errorMessage = new StringBuilder();
for (ConstraintViolation violation : validateSet) {
errorMessage.append("[").append(violation.getPropertyPath().toString()).append("]")
.append(violation.getMessage()).append(";");
}
throw new ValidationException(errorMessage.toString());
}
return Boolean.TRUE;
}
}
二、进阶用法
-
自定义验证注解
jakarta.validation-api和hibernate-validator包中内置的注解有些场景可能不支持,比如添加用户时,需要校验用户名是否重复,这时可以通过自定义注解来实现
1.首先自定义注解
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})
@Retention(RUNTIME)
@Documented
@Repeatable(UniqueName.List.class)
@Constraint(validatedBy = {UniqueNameValidator.class})
public @interface UniqueName {
String message() default "用户名重复了";
// 分组
Class>[] groups() default {};
Class extends Payload>[] payload() default {};
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})
@Retention(RUNTIME)
@Documented
public @interface List {
UniqueName[] value();
}
}
2.接着给自定义注解添加验证器
- 实现ConstraintValidator接口,并指定自定义注解和验证的数据类型
- 重写isValid方法,实现验证逻辑
@Component
public class UniqueNameValidator implements ConstraintValidator {
@Autowired
private UserService userService;
@Override
public boolean isValid(String name, ConstraintValidatorContext context) {
if (StringUtils.isBlank(name)) {
return true;
}
UserVo user = userService.getByName(name);
if (user == null) {
return true;
}
return false;
}
}
3.使用自定义注解
@Data
public class UserVo implements Serializable {
@UniqueName
private String name;
}
-
多属性联合校验
当一个字段的校验依赖另一个字段的值时,需要用到多属性联合校验,或者叫分组校验。举个例子,某个系统提交用户信息时需要做校验,当性别为女时,照片信息不能为空。这时,照片信息能否为空,依赖于性别的取值。hibernate-validator提供了DefaultGroupSequenceProvider接口供我们自定义分组,具体使用如下:
1.首先定义两个组,Boy和Girl
public interface Boy {
}
public interface Girl {
}
2.分组逻辑实现,当性别为女时,将用户分到Girl组
public class CustomGroupSequenceProvider implements DefaultGroupSequenceProvider {
@Override
public List> getValidationGroups(UserVo user) {
List> defaultGroupSequence = new ArrayList();
defaultGroupSequence.add(UserVo.class);
if (user != null) {
String sex = user.getSex();
if ("女".equals(sex)) {
defaultGroupSequence.add(Girl.class);
}
}
return defaultGroupSequence;
}
}
3.使用分组校验photoList字段
- 实体类上添加@GroupSequenceProvider(CustomSequenceProvider.class)注解
- 字段上添加@NotEmpty(message = “性别为女时照片不能为空”, groups = {Girl.class})注解
@Data
@GroupSequenceProvider(CustomSequenceProvider.class)
public class UserVo implements Serializable {
@NotBlank(message = "性别不能为空")
private String sex;
@NotEmpty(message = "性别为女时照片不能为空", groups = {Girl.class})
private List photoList;
}
-
嵌套校验
当VO对象中存在对象属性需要校验时,可以使用嵌套校验,
1.在对象属性上加@Valid注解
@Data
public class UserVo implements Serializable {
@Valid
@NotNull(message = "地址不能为空")
private Address address;
}
2.然后在内嵌对象中声明约束注解
@Data
public class Address implements Serializable {
@NotBlank(message = "地址名称不能为空")
private String name;
private String longitude;
private String latitude;
}
三、实现原理
-
@RequestBody参数校验实现原理
所有@RequestBody注释的参数都要经过RequestResponseBodyMethodProcessor类处理,该类主要用于解析@RequestBody注释方法的参数,以及处理@ResponseBody注释方法的返回值。其中,resolveArgument()方法是解析@RequestBody注释参数的入口
public class RequestResponseBodyMethodProcessor extends AbstractMessageConverterMethodProcessor {
@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);
if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) {
throw new MethodArgumentNotValidException(parameter, binder.getBindingResult());
}
}
if (mavContainer != null) {
mavContainer.addAttribute(BindingResult.MODEL_KEY_PREFIX + name, binder.getBindingResult());
}
}
return adaptArgumentIfNecessary(arg, parameter);
}
}
resolveArgument方法中的validateIfApplicable(binder, parameter)会对带有@valid/@validate注解的参数进行校验
protected void validateIfApplicable(WebDataBinder binder, MethodParameter parameter) {
Annotation[] annotations = parameter.getParameterAnnotations();
for (Annotation ann : annotations) {
Object[] validationHints = ValidationAnnotationUtils.determineValidationHints(ann);
if (validationHints != null) {
binder.validate(validationHints);
break;
}
}
}
//会对@Validated注解或者@Valid开头的注解进行校验
public static Object[] determineValidationHints(Annotation ann) {
Class extends Annotation> annotationType = ann.annotationType();
String annotationName = annotationType.getName();
if ("javax.validation.Valid".equals(annotationName)) {
return EMPTY_OBJECT_ARRAY;
}
Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class);
if (validatedAnn != null) {
Object hints = validatedAnn.value();
return convertValidationHints(hints);
}
if (annotationType.getSimpleName().startsWith("Valid")) {
Object hints = AnnotationUtils.getValue(ann);
return convertValidationHints(hints);
}
return null;
}
Spring通过一圈适配转换后,会把参数校验逻辑落到hibernate-validator中,在ValidatorImpl#validate(T object, Class>… groups)中做校验
public class ValidatorImpl implements Validator, ExecutableValidator {
@Override
public final Set> validate(T object, Class>... groups) {
Contracts.assertNotNull( object, MESSAGES.validatedObjectMustNotBeNull() );
sanityCheckGroups( groups );
@SuppressWarnings("unchecked")
Class rootBeanClass = (Class) object.getClass();
BeanMetaData rootBeanMetaData = beanMetaDataManager.getBeanMetaData( rootBeanClass );
if ( !rootBeanMetaData.hasConstraints() ) {
return Collections.emptySet();
}
BaseBeanValidationContext
validationContext = getValidationContextBuilder().forValidate( rootBeanClass, rootBeanMetaData, object );
ValidationOrder validationOrder = determineGroupValidationOrder( groups );
BeanValueContext, Object> valueContext = ValueContexts.getLocalExecutionContextForBean(
validatorScopedContext.getParameterNameProvider(),
object,
validationContext.getRootBeanMetaData(),
PathImpl.createRootPath()
);
return validateInContext( validationContext, valueContext, validationOrder );
}
}
具体校验过程在validateConstraintsForSingleDefaultGroupElement方法中,它会遍历@NotNull、@NotBlank、@Email这些约束注解,看参数是否符合限制
public class ValidatorImpl implements Validator, ExecutableValidator {
private boolean validateConstraintsForSingleDefaultGroupElement(BaseBeanValidationContext> validationContext, ValueContext valueContext, final Map, Class>> validatedInterfaces,
Class super U> clazz, Set> metaConstraints, Group defaultSequenceMember) {
boolean validationSuccessful = true;
valueContext.setCurrentGroup( defaultSequenceMember.getDefiningClass() );
//metaConstraints是@NotNull、@NotBlank、@Email这些约束注解的集合,一个个验证
for ( MetaConstraint> metaConstraint : metaConstraints ) {
final Class> declaringClass = metaConstraint.getLocation().getDeclaringClass();
if ( declaringClass.isInterface() ) {
Class> validatedForClass = validatedInterfaces.get( declaringClass );
if ( validatedForClass != null && !validatedForClass.equals( clazz ) ) {
continue;
}
validatedInterfaces.put( declaringClass, clazz );
}
boolean tmp = validateMetaConstraint( validationContext, valueContext, valueContext.getCurrentBean(), metaConstraint );
if ( shouldFailFast( validationContext ) ) {
return false;
}
validationSuccessful = validationSuccessful && tmp;
}
return validationSuccessful;
}
}
validator.isValid()是所有验证器的入口,包括hibernate-validator内置的,以及自定义的
public abstract class ConstraintTree {
protected final Optional validateSingleConstraint(
ValueContext, ?> valueContext,
ConstraintValidatorContextImpl constraintValidatorContext,
ConstraintValidator validator) {
boolean isValid;
try {
@SuppressWarnings("unchecked")
V validatedValue = (V) valueContext.getCurrentValidatedValue();
isValid = validator.isValid( validatedValue, constraintValidatorContext );
}
catch (RuntimeException e) {
if ( e instanceof ConstraintDeclarationException ) {
throw e;
}
throw LOG.getExceptionDuringIsValidCallException( e );
}
if ( !isValid ) {
//We do not add these violations yet, since we don't know how they are
//going to influence the final boolean evaluation
return Optional.of( constraintValidatorContext );
}
return Optional.empty();
}
}
以下是@NotBlank约束注解验证器的具体实现
public class NotBlankValidator implements ConstraintValidator {
/**
* Checks that the character sequence is not {@code null} nor empty after removing any leading or trailing
* whitespace.
*
* @param charSequence the character sequence to validate
* @param constraintValidatorContext context in which the constraint is evaluated
* @return returns {@code true} if the string is not {@code null} and the length of the trimmed
* {@code charSequence} is strictly superior to 0, {@code false} otherwise
*/
@Override
public boolean isValid(CharSequence charSequence, ConstraintValidatorContext constraintValidatorContext) {
if ( charSequence == null ) {
return false;
}
return charSequence.toString().trim().length() > 0;
}
}
-
@RequestParam/@PathVariable参数校验实现原理
该方式本质是通过类上加@Validated注解,方法参数前加@NotBlank等约束注解来实现的。底层使用的是Spring AOP,具体来说是通过MethodValidationPostProcessor动态注册AOP切面,然后使用MethodValidationInterceptor对切点方法织入增强。以下是容器启动时初始化@Validated切点,以及MethodValidationInterceptor增强
public class MethodValidationPostProcessor extends AbstractBeanFactoryAwareAdvisingPostProcessor
implements InitializingBean {
private Class extends Annotation> validatedAnnotationType = Validated.class;
@Nullable
private Validator validator;
@Override
public void afterPropertiesSet() {
Pointcut pointcut = new AnnotationMatchingPointcut(this.validatedAnnotationType, true);
this.advisor = new DefaultPointcutAdvisor(pointcut, createMethodValidationAdvice(this.validator));
}
protected Advice createMethodValidationAdvice(@Nullable Validator validator) {
return (validator != null ? new MethodValidationInterceptor(validator) : new MethodValidationInterceptor());
}
}
具体增强逻辑在MethodValidationInterceptor中
public class MethodValidationInterceptor implements MethodInterceptor {
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
// Avoid Validator invocation on FactoryBean.getObjectType/isSingleton
if (isFactoryBeanMetadataMethod(invocation.getMethod())) {
return invocation.proceed();
}
Class>[] groups = determineValidationGroups(invocation);
// Standard Bean Validation 1.1 API
ExecutableValidator execVal = this.validator.forExecutables();
Method methodToValidate = invocation.getMethod();
Set> result;
try {
result = execVal.validateParameters(
invocation.getThis(), methodToValidate, invocation.getArguments(), groups);
}
catch (IllegalArgumentException ex) {
// Probably a generic type mismatch between interface and impl as reported in SPR-12237 / HV-1011
// Let's try to find the bridged method on the implementation class...
methodToValidate = BridgeMethodResolver.findBridgedMethod(
ClassUtils.getMostSpecificMethod(invocation.getMethod(), invocation.getThis().getClass()));
result = execVal.validateParameters(
invocation.getThis(), methodToValidate, invocation.getArguments(), groups);
}
if (!result.isEmpty()) {
throw new ConstraintViolationException(result);
}
Object returnValue = invocation.proceed();
result = execVal.validateReturnValue(invocation.getThis(), methodToValidate, returnValue, groups);
if (!result.isEmpty()) {
throw new ConstraintViolationException(result);
}
return returnValue;
}
}
其中execVal.validateParameters()方法是用来做参数校验的,最终会进到hibernate-validator中。后面的逻辑跟上面类似,此处就不再赘述
public class ValidatorImpl implements Validator, ExecutableValidator {
@Override
public Set> validateParameters(T object, Method method, Object[] parameterValues, Class>... groups) {
Contracts.assertNotNull( object, MESSAGES.validatedObjectMustNotBeNull() );
Contracts.assertNotNull( method, MESSAGES.validatedMethodMustNotBeNull() );
Contracts.assertNotNull( parameterValues, MESSAGES.validatedParameterArrayMustNotBeNull() );
return validateParameters( object, (Executable) method, parameterValues, groups );
}
}
项目源码
https://github.com/layfoundation/spring-param-validate
附件
jakarta.validation-api(版本2.0.1)所有注解
注解 | 说明 |
---|---|
@AssertFalse | 验证 boolean 类型值是否为 false |
@AssertTrue | 验证 boolean 类型值是否为 true |
@DecimalMax(value) | 验证数字的大小是否小于等于指定的值,小数存在精度 |
@DecimalMin(value) | 验证数字的大小是否大于等于指定的值,小数存在精度 |
@Digits(integer, fraction) | 验证数字是否符合指定格式 |
验证字符串是否符合电子邮件地址的格式 | |
@Future | 验证一个日期或时间是否在当前时间之后 |
@FutureOrPresent | 验证一个日期或时间是否在当前时间之后或等于当前时间 |
@Max(value) | 验证数字的大小是否小于等于指定的值 |
@Min(value) | 验证数字的大小是否大于等于指定的值 |
@Negative | 验证数字是否是负整数,0无效 |
@NegativeOrZero | 验证数字是否是负整数 |
@NotBlank | 验证字符串不能为空null或””,只能用于字符串验证 |
@NotEmpty | 验证对象不得为空,可用于Map和数组 |
@NotNull | 验证对象不为 null |
@Null | 验证对象必须为 null |
@past | 验证一个日期或时间是否在当前时间之前。 |
@PastOrPresent | 验证一个日期或时间是否在当前时间之前或等于当前时间。 |
@Pattern(value) | 验证字符串是否符合正则表达式的规则 |
@Positive | 验证数字是否是正整数,0无效 |
@PositiveOrZero | 验证数字是否是正整数 |
@Size(max, min) | 验证对象(字符串、集合、数组)长度是否在指定范围之内 |
hibernate-validator(版本6.0.17.Final)补充的常用注解
注解 | 说明 |
---|---|
@Length | 被注释的字符串的大小必须在指定的范围内 |
@Range | 被注释的元素必须在合适的范围内 |
@SafeHtml | 被注释的元素必须是安全Html |
@URL | 被注释的元素必须是有效URL |