spring-boot-starter-validation 参照文档

引入依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>

常用注解

  • NotNull
  • NotBlank
  • Max
  • Min
  • Size

大部分注解

NotNull

带注解的字段不能为null

NotEmpty

带注解的字段不能为null也不能为空

支持MapCharSequenceCollectionArray

AssertFalse

带注解的字段必须为false

AssertTrue

带注解的字段必须为true

DecimalMax

带注解的元素必须是一个数字,其值必须小于或等于指定的最大值

支持BigDecimalBigIntegerCharSequence

byteshortintlong和它们各自的包装类型

DecimalMin

带注解的元素必须是一个数字,其值必须大于或等于指定的最小值

支持BigDecimalBigIntegerCharSequence

byteshortintlong和它们各自的包装类型`

Digits

带注解的元素必须是可接受范围内的数字

支持BigDecimalBigIntegerCharSequence

byteshortintlong和它们各自的包装类型`

Email

带注解的字符串必须是格式正确的电子邮件地址
null元素被认为是有效的

Future

带注解的元素必须是未来的瞬间、日期或时间

支持的类型有:

  • java.util.Date
  • java.util.Calendar
  • java.time.Instant
  • java.time.LocalDate
  • java.time.LocalDateTime
  • java.time.LocalTime
  • java.time.MonthDay
  • java.time.OffsetDateTime
  • java.time.OffsetTime
  • java.time.Year
  • java.time.YearMonth
  • java.time.ZonedDateTime
  • java.time.chrono.HijrahDate
  • java.time.chrono.JapaneseDate
  • java.time.chrono.MinguoDate
  • java.time.chrono.ThaiBuddhistDate

Max

带注解的元素必须是一个数字,其值必须小于或等于指定的最大值。
支持的类型有:
BigDecimal
BigInteger
byteshortintlong和它们各自的包装器
请注意,由于舍入错误,不支持doublefloat(某些提供程序可能会提供一些近似支持)。
null元素被认为是有效的。

Min

带注解的元素必须是一个数字,其值必须大于或等于指定的最小值。

支持类型以及注意事项见第10条:Max

Negative

带注解的元素必须是严格的负数(即,0 被视为无效值)

支持的类型有:BigDecimal
BigInteger
byteshortintlongfloatdouble和它们各自的包装器
null元素被认为是有效的。

NegativeOrZero

带注解的元素必须是负数或 0

支持类型见第12条:Negative

NotBlank

带注解的元素不能为null并且必须至少包含一个非空白字符。 仅支持CharSequence

Null

带注解的元素必须为null 。 接受任何类型

Past

带注解的元素必须是过去的瞬间、日期或时间

支持的类型见第9条:Future

Pattern

带注解的CharSequence必须匹配指定的正则表达式。 正则表达式遵循 Java 正则表达式约定
null元素被认为是有效的。

Positive

带注解的元素必须是严格的正数(即 0 被视为无效值)。
支持的类型见第12条:Negative

null元素被认为是有效的。

PositiveOrZero

带注解的元素必须是正数或 0。
支持的类型见第12条:Negative

null元素被认为是有效的。

Size

带注解的元素大小必须在指定边界(包括)之间。
支持的类型有:
CharSequence(评估字符序列的长度)
Collection(评估集合大小)
Map(评估Map大小)
Array(评估数组长度)
null元素被认为是有效的。

ScriptAssert

一个类级别的约束,它根据带注释的元素评估脚本表达式。 此约束可用于实现依赖于带注释元素的多个属性的验证例程。
脚本表达式可以用任何脚本或表达式语言编写,在类路径中可以找到与JSR 223 (“Java TM平台脚本”)兼容的引擎。 以下清单显示了一个使用 JDK附带的 JavaScript引擎的示例:

将真正的表达式语言与较短的对象别名结合使用可以实现非常紧凑的表达式:

注:javascript就是java

可直接调用类中方法:

公共方法:_this.methodName(params)

静态方法:com.demo.utils.ValidUtils.LocalDateTimeValid(params)

方法返回值需要布尔类型,错误消息需要指定message属性

@ScriptAssert(lang = "javascript", script = "_this.startDate.before(_this.endDate)")
 public class CalendarEvent {
 	private Date startDate;
 	private Date endDate;
 	//...
 }
@ScriptAssert(lang = "jexl", script = "_.startDate > _.endDate", alias = "_")
 public class CalendarEvent {
 	private Date startDate;
 	private Date endDate;
 	//...
 }

Length

验证字符串是否介于 minmax之间

Range

带注解的元素必须在适当的范围内。 应用于数值或数值的字符串表示

URL

验证带注解的字符串是 URL

参数protocol 、 host和port与 URL 的相应部分匹配。 并且可以使用regexp和flags指定额外的正则表达式以进一步限制匹配条件。
注意:默认情况下,此约束的约束验证器使用java.net.URL构造函数来验证字符串。 这意味着匹配的协议处理程序需要可用

CreditCardNumber

带注解的元素必须代表一个有效的信用卡号。 这是 Luhn算法实现,旨在检查用户错误,而不是信用卡有效性

错误消息模板示例:

例如MaxMinRange

@Max(value = 100, message = "最大不超过{value}")
@Min(value = 0, message = "最小不低于{value}")
@Range(min = 0, max = 100, message = "数值介于{min}~{max}之间")

方法参数校验使用示例:

@Validated
public class Test {
    public void test(@Min(value = 0, message = "最小不低于{value}") Integer age){}
}
public class Test {
    public void test(@RequestBody @Valid User user){}
}

嵌套校验示例:

@Valid
private User user;

分组校验示例:

public interface CreateGroup {}
public interface UpdateGroup {}
public class TestDto {
    @Max(value = 100, groups = {CreateGroup.class}, message = "最大不超过{value}")
    @Min(value = 0, groups = {UpdateGroup.class}, message = "最小不低于{value}")
    private Integer age;
}
public class Test {
    public void test1(@RequestBody @Validated({CreateGroup.class}) TestDto dto){}
    public void test2(@RequestBody @Validated({UpdateGroup.class}) TestDto dto){}
}
// 注解Valid与Validated可以混用
// Validated是Valid的变体
// Valid不支持分组校验

手动触发校验

//注入javax.validation.Validator
@Autowired
private Validator validator;

public void valid(TestDto dto, Class<?> groupClass) {
    Set<ConstraintViolation<TestDto>> validateSet = validator.validate(dto, groupClass);
    if(validateSet.isEmpty()) {
        // 校验通过
        return;
    }
    // 校验失败
    for(ConstraintViolation<TestDto> validate: validateSet) {
        System.out.println(validate);
    }
}

Spring boot配置校验快速失败模式

// 快速失败即校验到一个不符合条件就结束了
@Bean
public Validator validator() {
    ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class)
            .configure()
            // 快速失败模式
            .failFast(true)
            .buildValidatorFactory();
    return validatorFactory.getValidator();
}

RequestBody校验实现原理

可以看看org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor

其中方法resolveArgument

@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);
	}
protected void validateIfApplicable(WebDataBinder binder, MethodParameter parameter) {
		Annotation[] annotations = parameter.getParameterAnnotations();
		for (Annotation ann : annotations) {
			Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class);
             // 如果有Validated注解或者Valid开头的注解则开始校验
			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;
			}
		}
	}
// WebDataBinder.validate实现
public void validate(Object target, Errors errors, Object... validationHints) {
		if (this.targetValidator != null) {
			processConstraintViolations(
                      // 调用Hibernate Validator执行真正的校验
					this.targetValidator.validate(target, asValidationGroups(validationHints)), errors);
		}
	}

方法参数校验实现原理

实现原理其实就是AOP,这种方式实际上可以作用与任何spring bean

可以看看org.springframework.validation.beanvalidation.MethodValidationPostProcessor

public class MethodValidationPostProcessor extends AbstractBeanFactoryAwareAdvisingPostProcessor
		implements InitializingBean {
    private Class<? extends Annotation> validatedAnnotationType = Validated.class;
	@Override
	public void afterPropertiesSet() {
         // 为所有`@Validated`标注的Bean创建切面
		Pointcut pointcut = new AnnotationMatchingPointcut(this.validatedAnnotationType, true);
         // 创建Advisor进行增强
		this.advisor = new DefaultPointcutAdvisor(pointcut, createMethodValidationAdvice(this.validator));
	}
	/**
	 * Create AOP advice for method validation purposes, to be applied
	 * with a pointcut for the specified 'validated' annotation.
	 * @param validator the JSR-303 Validator to delegate to
	 * @return the interceptor to use (typically, but not necessarily,
	 * a {@link MethodValidationInterceptor} or subclass thereof)
	 * @since 4.2
	 */
	protected Advice createMethodValidationAdvice(@Nullable Validator validator) {
		return (validator != null ? new MethodValidationInterceptor(validator) : new MethodValidationInterceptor());
	}

}
public class MethodValidationInterceptor implements MethodInterceptor {
	@Override
	@Nullable
	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<ConstraintViolation<Object>> result;

		Object target = invocation.getThis();
		Assert.state(target != null, "Target must not be null");

		try {
            // 方法入参校验
			result = execVal.validateParameters(target, 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(), target.getClass()));
			result = execVal.validateParameters(target, methodToValidate, invocation.getArguments(), groups);
		}
        // 校验不通过抛出异常
		if (!result.isEmpty()) {
			throw new ConstraintViolationException(result);
		}

		Object returnValue = invocation.proceed();

        // 返回值的校验
		result = execVal.validateReturnValue(target, methodToValidate, returnValue, groups);
        // 校验不通过抛出异常
		if (!result.isEmpty()) {
			throw new ConstraintViolationException(result);
		}

		return returnValue;
	}

	private boolean isFactoryBeanMetadataMethod(Method method) {
		Class<?> clazz = method.getDeclaringClass();

		// Call from interface-based proxy handle, allowing for an efficient check?
		if (clazz.isInterface()) {
			return ((clazz == FactoryBean.class || clazz == SmartFactoryBean.class) &&
					!method.getName().equals("getObject"));
		}

		// Call from CGLIB proxy handle, potentially implementing a FactoryBean method?
		Class<?> factoryBeanType = null;
		if (SmartFactoryBean.class.isAssignableFrom(clazz)) {
			factoryBeanType = SmartFactoryBean.class;
		}
		else if (FactoryBean.class.isAssignableFrom(clazz)) {
			factoryBeanType = FactoryBean.class;
		}
		return (factoryBeanType != null && !method.getName().equals("getObject") &&
				ClassUtils.hasMethod(factoryBeanType, method));
	}

	/**
	 * Determine the validation groups to validate against for the given method invocation.
	 * <p>Default are the validation groups as specified in the {@link Validated} annotation
	 * on the containing target class of the method.
	 * @param invocation the current MethodInvocation
	 * @return the applicable validation groups as a Class array
	 */
	protected Class<?>[] determineValidationGroups(MethodInvocation invocation) {
		Validated validatedAnn = AnnotationUtils.findAnnotation(invocation.getMethod(), Validated.class);
		if (validatedAnn == null) {
			Object target = invocation.getThis();
			Assert.state(target != null, "Target must not be null");
			validatedAnn = AnnotationUtils.findAnnotation(target.getClass(), Validated.class);
		}
		return (validatedAnn != null ? validatedAnn.value() : new Class<?>[0]);
	}

}

不管是requestBody参数校验还是方法参数校验,最终都是调用的Hibernate Validator

闲谈

上述都是以注解的形式对入参进行校验,我们也可以自己书写校验的方法然同样的可以达到目的。
对于一些业务逻辑本文章介绍的校验可能并不太适用。
个人建议是使用注解的形式来校验空参即可,其余的校验我们自己书写校验的方法在业务方法开始之前调用即可。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值