美文网首页
01.spring-mvc 结合 Java Validation

01.spring-mvc 结合 Java Validation

作者: 脱发的程序员 | 来源:发表于2021-02-10 15:32 被阅读0次

一、痛点?

在应用程序的业务逻辑中,经常会碰到参数校验的情况,通常我们使用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) 检查是否是一种数字的(整数,小数)的位数
Email 被注释的元素必须是电子邮箱地址
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即常见的参数绑定异常


    spring-mvc-bind异常

四、自定义枚举校验器

检验器实现方式有好多种,基本原理就是从自定义枚举注解上边获取原数据,然后使用当前值与枚举所有值进行比较,存在则返回true, 否则返回fase.

  1. 枚举校验注解
@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";
}
  1. 枚举校验器
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;
    }
}

六、原理

  1. 在配置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。

  1. 在 实例化 OptionalValidatorFactoryBean 的时候,初始化了SpringConstraintValidatorFactory 用来代理ConstraintValidatorFactory(JSR-303)。

  2. HandlerMethodArgumentResolver 的实现类 RequestPartMethodArgumentResolver 里边作注解参数的解析及验证。


    image.png

默认检测 @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;
        }
    }
}

相关文章

网友评论

      本文标题:01.spring-mvc 结合 Java Validation

      本文链接:https://www.haomeiwen.com/subject/gfohxltx.html