一、痛点?
在应用程序的业务逻辑中,经常会碰到参数校验的情况,通常我们使用spring-mvc来接收用户请求数据一般会封装成一个Bean,需要校验字段值是否空,长度,枚举格式等情况下,如果使用SringUtils或者if判断来解决,代码会阅读不友好,维护成本大,代码冗余。 因此有了JSR 303.
Bean Validation为JavaBean提供了相应的API来给我们做参数的验证。通过Bean Validation比如@NotNull @Pattern等方法来对我们字段的值做进一步的教研。
Bean Validation 是一个运行时框架,在验证之后错误信息会直接返回。
二、使用
1. 添加maven依赖
<!--添加依赖-->
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
<version>2.0.1.Final</version>
</dependency>
注: hibernate-validator 扩展了些自定义的validator可供参考。
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
<version>6.2.0.Final</version>
</dependency>
2. 常用注解说明
- constraints 包下边即定义好的注解,可直接使用
注解 | 用途 |
---|---|
AssertFalse | 用于boolean字段,该字段的值只能为false |
AssertTrue | 用于boolean字段,该字段只能为true |
DecimalMax(value) | 被注释的元素必须是一个数字,只能大于或等于该值 |
DecimalMin(value) | 被注释的元素必须是一个数字,只能小于或等于该值 |
Digits(integer,fraction) | 检查是否是一种数字的(整数,小数)的位数 |
被注释的元素必须是电子邮箱地址 | |
Future | 检查该字段的日期是否是属于将来的日期 |
FutureOrPresent | 判断日期是否是将来或现在日期 |
Max(value) | 该字段的值只能小于或等于该值 |
Min(value) | 该字段的值只能大于或等于该值 |
Negative | 判断负数 |
NegativeOrZero | 判断负数或0 |
NotBlank | 只能用于字符串不为null,并且字符串trim()以后length要大于0 |
NotEmpty | 集合对象的元素不为0,即集合不为空,也可以用于字符串不为null |
NotNull | 不能为null |
Null | 必须为 null |
Past | 检查该字段的日期是在过去 |
PastOrPresent | 判断日期是否是过去或现在日期 |
Pattern(value) | 被注释的元素必须符合指定的正则表达式 |
Positive | 判断正数 |
PositiveOrZero | 判断正数或0 |
Size(max, min) | 检查该字段的size是否在min和max之间,可以是字符串、数组、集合、Map等 |
Length(max, min) | 判断字符串长度 |
CreditCardNumber | 被注释的字符串必须通过Luhn校验算法,银行卡,信用卡等号码一般都用Luhn计算合法性 |
三、自定义注解
1. 自定义Mobile注解
//注解作用范围
@Target({
ElementType.METHOD,
ElementType.FIELD,
ElementType.ANNOTATION_TYPE,
ElementType.CONSTRUCTOR,
ElementType.PARAMETER,
ElementType.TYPE_USE
})
//注解保留阶段
@Retention(RetentionPolicy.RUNTIME)
@Documented
//指定验证器
@Constraint(
validatedBy = MobileValidator.class
)
public @interface Mobile {
String message() default "手机号格式不正确";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
2. 验证器 MobileValidator
实现ConstraintValidator 的isValid方法即可
public class MobileValidator implements ConstraintValidator<Mobile, String> {
@Override
public void initialize(Mobile annotation) {
}
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
// 如果手机号为空,默认不校验,即校验通过
if (!StringUtils.hasText(value)) {
return true;
}
// 校验手机
return ValidationUtil.isMobile(value);
}
}
3. 编写UserVo
public class UserVo {
@NotEmpty(message = "用户名不能为空")
@Length(min = 4, max = 6, message = "手机验证码长度为 4-6 位")
@Pattern(regexp = "^[0-9]+$", message = "手机验证码必须都是数字")
private String name;
@Mobile()
private String mobile;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getMobile() {
return mobile;
}
public void setMobile(String mobile) {
this.mobile = mobile;
}
@Override
public String toString() {
return new org.apache.commons.lang3.builder.ToStringBuilder(this)
.append("name", name)
.append("mobile", mobile)
.toString();
}
}
4. 测试Controller
@RequestMapping("/validTest")
@ResponseBody
public String validTest(@Valid @RequestBody UserVo userVo) {
String print = userVo.toString();
log.info(print);
return print;
}
注意:如果輸入的参数不正确,会抛出MethodArgumentNotValidException 异常,并会返回400错误给前端, 我们进一步美化输出统一格式的错误。
如果controller签名方法 参数使用
@GetMapping("/label/list")
@ResponseBody
@Validated //加上才能被spring处理
public RdfaResult<List<LabelVo>> label_list_parent( @NotEmpty(message = "缺少参数") @RequestParam("kpi_level") String kpi_level) throws Exception {
return RdfaResult.success(ResponseErrorEnum.SUCESS.getCode(), ResponseErrorEnum.SUCESS.getVal(), resp);
}
5. 定义全局异常处理器
/**
* 全局异常处理器,将 Exception 翻译成 CommonResult + 对应的异常编号
*/
@RestControllerAdvice
public class GlobalExceptionHandler {
private Logger logger = LoggerFactory.getLogger(getClass());
/**
* 处理 SpringMVC 参数校验异常 Validator 校验
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
public CommonResult MethodArgumentNotValidException(MethodArgumentNotValidException ex) {
logger.warn("[MethodArgumentNotValidException]", ex);
FieldError fieldError = ex.getBindingResult().getFieldError();
//断言,避免告警
assert fieldError != null;
String format = String.format("请求参数不正确:%s", fieldError.getDefaultMessage());
CommonResult<String> commonResult =CommonResult.fail(msg);
return commonResult;
}
}
-
spring-web 组件, org.springframework.web.bind即常见的参数绑定异常
四、自定义枚举校验器
检验器实现方式有好多种,基本原理就是从自定义枚举注解上边获取原数据,然后使用当前值与枚举所有值进行比较,存在则返回true, 否则返回fase.
- 枚举校验注解
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = EnumCheckValidator.class)
public @interface EnumCheck {
/**
* 是否必填 默认是必填的
* @return
*/
boolean required() default true;
/**
* 验证失败的消息
* @return
*/
String message() default "枚举的验证失败";
/**
* 分组的内容
* @return
*/
Class<?>[] groups() default {};
/**
* 错误验证的级别
* @return
*/
Class<? extends Payload>[] payload() default {};
/**
* 枚举的Class
* @return
*/
Class<? extends Enum<?>> enumClass();
/**
* 枚举中的验证方法
* @return
*/
String enumMethod() default "validation";
}
- 枚举校验器
public class EnumCheckValidator implements ConstraintValidator<EnumCheck, Object> {
private static final Logger logger = LoggerFactory.getLogger(EnumCheckValidator.class);
private EnumCheck enumCheck;
@Override
public void initialize(EnumCheck enumCheck) {
this.enumCheck =enumCheck;
}
@Override
public boolean isValid(Object value, ConstraintValidatorContext constraintValidatorContext) {
// 注解表明为必选项 则不允许为空,否则可以为空
if (value == null) {
return !this.enumCheck.required();
}
Boolean result = Boolean.FALSE;
Class<?> valueClass = value.getClass();
try {
//通过反射执行枚举类中validation方法
Method method = this.enumCheck.enumClass().getMethod(this.enumCheck.enumMethod(), valueClass);
result = (Boolean)method.invoke(null, value);
if(result == null){
return false;
}
} catch (Exception e) {
logger.error("custom EnumCheckValidator error", e);
}
return result;
}
}
六、原理
- 在配置spring-mvc 时有个核心类 WebMvcConfigurationSupport.java 会配置 Validator 的一个实例。
@Bean
public Validator mvcValidator() {
Validator validator = getValidator();
if (validator == null) {
if (ClassUtils.isPresent("javax.validation.Validator", getClass().getClassLoader())) {
Class<?> clazz;
try {
String className = "org.springframework.validation.beanvalidation.OptionalValidatorFactoryBean";
clazz = ClassUtils.forName(className, WebMvcConfigurationSupport.class.getClassLoader());
}
catch (ClassNotFoundException | LinkageError ex) {
throw new BeanInitializationException("Failed to resolve default validator class", ex);
}
validator = (Validator) BeanUtils.instantiateClass(clazz);
}
else {
validator = new NoOpValidator();
}
}
return validator;
}
这个实例的作用是:用于验证标注了@ModelAttribute和@RequestBody注解的方法参数。实例化时,首先委托getValidator(),如果返回null,则检查类路径上是否存在 JSR-303实现(javax.validation.Validator), 存在则实例化OptionalValidatorFactoryBean,否则返回一个无操作的Validator。
在 实例化 OptionalValidatorFactoryBean 的时候,初始化了SpringConstraintValidatorFactory 用来代理ConstraintValidatorFactory(JSR-303)。
-
HandlerMethodArgumentResolver 的实现类 RequestPartMethodArgumentResolver 里边作注解参数的解析及验证。
默认检测 @javax.validation.Valid、Spring's Validated、自定义以 "Valid" 开头的注解
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;
}
}
}