[TOC]
对于后端开发, 有很多表单数据需要被校验是否合理, 如果在代码中编写大量的if-else
会严重影响阅读体验, 所以需要寻找一个更好的参数校验方式来解决参数校验问题
javax提供的校验API
@NotNull
, @NotEmpty
这些注解平时也经常使用, 这些也是经常见, 经常使用的
这些注解在javax提供的校验包中: javax.validation.constraints
由javax提供的校验注解中总共包含如下这些:
注解 | 作用数据类型 | 描述 |
---|---|---|
@AssertFalse | 布尔值 | 被校验的元素必须为False |
@AssertTrue | 布尔值 | 被校验的元素必须为True |
@DecimalMax | 浮点数 | 被校验的元素必须小于等于配置值 |
@DecimalMin | 浮点数 | 被校验的元素必须大于等于配置值 |
@Digits | 数值 | 被校验的元素整数位不能超过Integer配置值, 浮点位数不能超过fractional配置值 |
字符串 | 被校验元素符合邮箱规则 | |
@Future | 时间类型 | 被校验的元素必须是一个未来时间 |
@FutureOrPresent | 时间类型 | 被校验的元素必须是当前或未来时间 |
@Max | 整数类型 | 被校验的数值最大不能超过配置值 |
@Min | 整数类型 | 被校验额数值最小不能超过配置值 |
@Negative | 整数类型 | 被校验的值必须为负整数 |
@NegativeOrZero | 整数类型 | 被校验的值必须小于等于0 |
@NotBlank | 字符串 | 被校验的元素至少包含一个非空白的字符 |
@NotEmpty | 字符串/数组/集合 | 被校验的元素不是空, 被校验的集合不是空 |
@NotNull | Object | 被校验的对象不为null |
@Null | Object | 被校验的对象必须为null |
@Past | 时间类型 | 被校验的元素必须是一个过去的时间 |
@PastOrPresent | 时间类型 | 被校验的元素必须是当前或过去的某个时间 |
@Pattern | 字符串 | 被校验的元素必须符合正则校验规则 |
@Positive | 整数类型 | 被校验的数值必须为正整数 |
@PositiveOrZero | 整数类型 | 被校验的数值必须大于等于0 |
@Size | 字符串/数组/集合 | 字符串长度必须满足规则, 数组/集合元素个数必须满足min, max的配置规则 |
- javax仅仅是定义了这些注解, 真正的实现者是hibernate. 一般情况下Spring的项目都会自动的引入hibernate校验包, 如果没有引入的话, 需要自行maven 仓库找一下相关包.
org.hibernate.validator:hibernate-validator
Hibernate提供的校验API
除了Javax提供的校验API定义之外, Hibernate也提供了一些校验注解, 仅罗列一些常用的, 如下:
注解 | 作用数据类型 | 描述 |
---|---|---|
@Length | 字符串 | 被校验的字符串必须满足min, max配置 |
@Range | 整数类型/字符串 | 被校验的元素必须在min, max范围内 |
@URL | 字符串 | 被校验的元素必须满足URL配置规则 |
除了这些我也看到了一些别的校验注解, 虽然没有使用过, 不过应该会想去尝试尝试
以上应该就是常用的校验注解了
使用
我经常使用的web框架是SpringBoot, 所以正常情况下, 这些注解都是可以直接使用, 并且Validator已经被正确配置上了. 我们只需要考虑如何正确编写校验配置
案例1 Query参数的校验
Query参数只的是跟随在URL后面的请求参数例如: http://hostname.com/search?q=balabala
其中q就是query请求参数
首先我们需要在Controller上添加注解
@Validated (org.springframework.validation.annotation.Validated)
有了这个注解Spring才能帮我们在调用Controller之前去完成参数的校验
Controller方法的定义:
public void getCaptcha(@NotEmpty(message = "设备ID不能为空") String deviceId,
HttpServletResponse response) throws IOException {
}
不出意外的话, 如果请求参数缺少就会抛出异常
ConstraintViolationException
记得做全局异常处理~取出异常消息进行返回
if (e instanceof ConstraintViolationException) {
Set> constraintViolations = ((ConstraintViolationException) e).getConstraintViolations();
Iterator> iterator = constraintViolations.iterator();
if (iterator.hasNext()) {
ConstraintViolation> next = iterator.next();
message = next.getMessage();
}
}
案例2 form请求或json请求的校验
当参数比较多的时候, 我们通常会选择用JavaBean来接收参数, 这种情况下, 我们只需要在JavaBean的Field上编写相应的注解并完成一些简单的配置即可,
JavaBean Field (我用了lombok)
@NotEmpty(message = "用户名不能为空")
@Length(max = 50, message = "最大长度50个字符")
private String name;
@NotEmpty(message = "密码不能为空")
@Length(max = 30, message = "用户名密码错误")
private String pwd;
@NotEmpty(message = "设备ID不能为空")
private String deviceId;
@NotNull(message = "验证码不能为空")
@Length(min = 6, max = 6, message = "验证码错误")
private String captcha;
Contoller 方法定义
public @ResponseBody CommonResponse login(@Validated @RequestBody AdminLoginReqDTO adminLoginReqDTO) {
}
同样不出意外的话可能会遇到
MethodArgumentNotValidException
异常
全局异常处理
} else if (e instanceof MethodArgumentNotValidException) {
BindingResult br = ((MethodArgumentNotValidException) e).getBindingResult();
FieldError fieldError = br.getFieldError();
message = fieldError == null ? null : fieldError.getDefaultMessage();
}
上面的内容只能算一个基础使用
下面单独讲讲自定义校验注解的编写
自定义校验注解
注解的内容就是定义元素的校验规则, 以我自己的项目为例, 有些参数不是必填的, 但是如果填写了就需要校验, 所以我定义了一个注解
@Documented
@Constraint(validatedBy = { NotRequireValidator.class })
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Repeatable(NotRequire.List.class)
public @interface NotRequire {
/**
* 可用于字符串的校验, 也可用于数值范围的校验
* @return
*/
long min() default 0;
/**
* 可用于字符串的校验, 也可用于数值范围的校验
* @return
*/
long max() default Long.MAX_VALUE;
String message() default "{art.yitongxue.ytxinterface.common.valid.NonNullValid}";
Class>[] groups() default { };
Class extends Payload>[] payload() default { };
/**
* Defines several {@code @NotEmpty} constraints on the same element.
*
* @see NotEmpty
*/
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Documented
public @interface List {
NotRequire[] value();
}
}
这个注解基本上就是从其他注解上抄过来的, 但是还要完善几个地方
-
@Repeatable(NotRequire.List.class)
记得修改为你的注解类名 -
@Constraint(validatedBy = { NotRequireValidator.class })
一定要指定你的校验类 (我之前就没指定, 导致我的注解一直不生效)
自定义校验类
校验类的作用就是去实现注解里面的校验内容
public class NotRequireValidator implements ConstraintValidator {
private long min;
private long max;
@Override
public void initialize(NotRequire constraintAnnotation) {
this.min = constraintAnnotation.min();
this.max = constraintAnnotation.max();
}
@Override
public boolean isValid(Object value, ConstraintValidatorContext context) {
if (null == value) {
return true;
}
if (value instanceof String) {
int length = ((String) value).length();
return min <= length && length <= max;
} else if (value instanceof Integer || value instanceof Long) {
long val = ((Number) value).longValue();
return min <= val && val <= max;
}
return false;
}
}
-
initialize
方法是用来初始化数据, 也就是存放注解里面的配置信息 -
isValid
方法是真实用来进行校验逻辑的方法
完成如上两件工作之后, 就可以像使用@NotEmpty
这样的注解一样使用自定义注解了
如果想看看Hibernate是如何完成校验逻辑的可以在IDEA中搜索 ConstraintHelper
这个类, 这个类的构造方法中记录了所有的校验器. 可以分类去查看.
关于Spring是怎么做校验的逻辑
- Spring会为Controller添加一个拦截器, 拦截器叫 :
MethodValidationInterceptor
- 当请求数据时, 会经过拦截器, 拦截器判断 Çlass 或者 方法上是否标记有
Validated
注解 - 当请求被AOP拦截并执行校验逻辑, 并存在校验不通过情况时, 则会抛出
ConstraintViolationException
异常
源码如下:
/**
* An AOP Alliance {@link MethodInterceptor} implementation that delegates to a
* JSR-303 provider for performing method-level validation on annotated methods.
*
* Applicable methods have JSR-303 constraint annotations on their parameters
* and/or on their return value (in the latter case specified at the method level,
* typically as inline annotation).
*
*
E.g.: {@code public @NotNull Object myValidMethod(@NotNull String arg1, @Max(10) int arg2)}
*
*
Validation groups can be specified through Spring's {@link Validated} annotation
* at the type level of the containing target class, applying to all public service methods
* of that class. By default, JSR-303 will validate against its default group only.
*
*
As of Spring 5.0, this functionality requires a Bean Validation 1.1 provider.
*
* @author Juergen Hoeller
* @since 3.1
* @see MethodValidationPostProcessor
* @see javax.validation.executable.ExecutableValidator
*/
public class MethodValidationInterceptor implements MethodInterceptor {
private final Validator validator;
/**
* Create a new MethodValidationInterceptor using a default JSR-303 validator underneath.
*/
public MethodValidationInterceptor() {
this(Validation.buildDefaultValidatorFactory());
}
/**
* Create a new MethodValidationInterceptor using the given JSR-303 ValidatorFactory.
* @param validatorFactory the JSR-303 ValidatorFactory to use
*/
public MethodValidationInterceptor(ValidatorFactory validatorFactory) {
this(validatorFactory.getValidator());
}
/**
* Create a new MethodValidationInterceptor using the given JSR-303 Validator.
* @param validator the JSR-303 Validator to use
*/
public MethodValidationInterceptor(Validator validator) {
this.validator = validator;
}
@Override
@SuppressWarnings("unchecked")
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;
}
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.getName(), method.getParameterTypes()));
}
/**
* Determine the validation groups to validate against for the given method invocation.
* 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) {
validatedAnn = AnnotationUtils.findAnnotation(invocation.getThis().getClass(), Validated.class);
}
return (validatedAnn != null ? validatedAnn.value() : new Class>[0]);
}
}