Validate校验框架-自定义校验注解

[TOC]

对于后端开发, 有很多表单数据需要被校验是否合理, 如果在代码中编写大量的if-else会严重影响阅读体验, 所以需要寻找一个更好的参数校验方式来解决参数校验问题

javax提供的校验API

@NotNull, @NotEmpty 这些注解平时也经常使用, 这些也是经常见, 经常使用的
这些注解在javax提供的校验包中: javax.validation.constraints
由javax提供的校验注解中总共包含如下这些:

注解 作用数据类型 描述
@AssertFalse 布尔值 被校验的元素必须为False
@AssertTrue 布尔值 被校验的元素必须为True
@DecimalMax 浮点数 被校验的元素必须小于等于配置值
@DecimalMin 浮点数 被校验的元素必须大于等于配置值
@Digits 数值 被校验的元素整数位不能超过Integer配置值, 浮点位数不能超过fractional配置值
@Email 字符串 被校验元素符合邮箱规则
@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[] 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();
    }
}

这个注解基本上就是从其他注解上抄过来的, 但是还要完善几个地方

  1. @Repeatable(NotRequire.List.class) 记得修改为你的注解类名
  2. @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;
    }
}
  1. initialize方法是用来初始化数据, 也就是存放注解里面的配置信息
  2. isValid 方法是真实用来进行校验逻辑的方法

完成如上两件工作之后, 就可以像使用@NotEmpty这样的注解一样使用自定义注解了

如果想看看Hibernate是如何完成校验逻辑的可以在IDEA中搜索 ConstraintHelper 这个类, 这个类的构造方法中记录了所有的校验器. 可以分类去查看.


关于Spring是怎么做校验的逻辑

  1. Spring会为Controller添加一个拦截器, 拦截器叫 : MethodValidationInterceptor
  2. 当请求数据时, 会经过拦截器, 拦截器判断 Çlass 或者 方法上是否标记有 Validated 注解
  3. 当请求被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]); } }

你可能感兴趣的:(Validate校验框架-自定义校验注解)