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<ConstraintViolation<?>> constraintViolations = ((ConstraintViolationException) e).getConstraintViolations();
            Iterator<ConstraintViolation<?>> 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();
    }
}

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

  1. @Repeatable(NotRequire.List.class) 记得修改为你的注解类名
  2. @Constraint(validatedBy = { NotRequireValidator.class }) 一定要指定你的校验类 (我之前就没指定, 导致我的注解一直不生效)

自定义校验类

校验类的作用就是去实现注解里面的校验内容

public class NotRequireValidator implements ConstraintValidator<NotRequire, Object> {

    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.
 *
 * <p>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).
 *
 * <p>E.g.: {@code public @NotNull Object myValidMethod(@NotNull String arg1, @Max(10) int arg2)}
 *
 * <p>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.
 *
 * <p>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<ConstraintViolation<Object>> 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.
     * <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) {
            validatedAnn = AnnotationUtils.findAnnotation(invocation.getThis().getClass(), Validated.class);
        }
        return (validatedAnn != null ? validatedAnn.value() : new Class<?>[0]);
    }

}

原网址: 访问
创建于: 2021-08-24 20:45:15
目录: default
标签: 无

请先后发表评论
  • 最新评论
  • 总共0条评论