Spring数据校验Validation详解,JSR-330使用,

一、概述

​ JSR 303中提出了Bean Validation,表示JavaBean的校验,Hibernate Validation是其具体实现,并对其进行了一些扩展,添加了一些实用的自定义校验注解。

​ Spring中集成了这些内容,你可以在Spring中以原生的手段来使用校验功能,当然Spring也对其进行了一点简单的扩展,以便其更适用于Java web的开发。

1、Spring 校验使用场景

Spring 常规校验(Validator)
Spring 数据绑定(DataBinder)
Spring Web 参数绑定(WebDataBinder)
Spring WebMVC/WebFlux 处理方法参数校验

2、JSR 303 Bean Validation

​ JSR 303中提供了诸多实用的校验注解,这里简单罗列:

//校验类型(message="错误提示")
//1、@Null                       校验对象是否为null
//2、@NotNull                    校验对象是否不为null
//3、@NotBlank                   校验字符串去头尾空格后的长度是否大于0或是否为null
//4、@NotEmpty                   校验字符串是否为null或是否为empty
//
//5、@AssertTrue                 校验Boolean是否为true
//6、@AssertFalse                校验Boolean是否为false
//
//7、@UniqueElements             校验数组/集合的元素是否唯一
//8、@Size(min,max)              校验数组/集合/字符串长度是否在范围之内
//9、@Length(min,max)            校验数组/集合/字符串长度是否在范围之内
//10、@Range(min,max)            校验Integer/Short/Long是否在范围之内
//11、@Min(number)               校验Integer/Short/Long是否大于等于value
//12、@Max(number)               校验Integer/Short/Long是否小于等于value
//13、@Positive                  校验Integer/Short/Long是否为正整数
//14、@PositiveOrZero            校验Integer/Short/Long是否为正整数或0
//15、@Negative                  校验Integer/Short/Long是否为负整数
//16、@NogativeOrZero            校验Integer/Short/Long是否为负整数或0
//
//17、@DecimalMin(decimal)       校验Float/Double是否大于等于value
//18、@DecimalMax(decimal)       校验Float/Double是否小于等于value
//19、@Digits(integer,fraction)  校验数字是否符合整数位数精度和小数位数精度
//
//20、@Past(date)                校验Date/Calendar是否在当前时间之前
//21、@PastOrPresent(date)       校验Date/Calendar是否在当前时间之前或当前时间
//22、@Future(date)              校验Date/Calendar是否在当前时间之后
//23、@FutureOrPresent(date)     校验Date/Calendar是否在当前时间之后或当前时间
//
//24、@Email                     校验字符串是否符合电子邮箱格式
//25、@URL(protocol,host,port)   校验字符串是否符合URL地址格式
//26、@CreditCardNumber          校验字符串是否符合信用卡号格式
//
//27、@Pattern(regexp)           校验字符串是否符合正则表达式的规则
//
//除此之外,我们还可以自定义一些数据校验规则

二、Validator 接口设计

接口职责:
Spring 内部校验器接口,通过编程的方式校验目标对象

核心方法:
supports(Class):校验目标类能否校验
validate(Object,Errors):校验目标对象,并将校验失败的内容输出至 Errors 对象

配套组件:
错误收集器:org.springframework.validation.Errors
Validator 工具类:org.springframework.validation.ValidationUtils

我们分析一下org.springframework.validation.Validator接口的源码:

public interface Validator {
    
    
	// 是否支持
	boolean supports(Class<?> clazz);

	void validate(Object target, Errors errors);
}

从它doc文档中基本可以看出,基本只适用于某个model类,其doc中有一个实例,我们一起来看一下:

 public class UserLoginValidator implements Validator {
    
    
  
      private static final int MINIMUM_PASSWORD_LENGTH = 6;
  
      public boolean supports(Class clazz) {
    
    
         return UserLogin.class.isAssignableFrom(clazz);
      }
  
      public void validate(Object target, Errors errors) {
    
    
         ValidationUtils.rejectIfEmptyOrWhitespace(errors, "userName", "field.required");
         ValidationUtils.rejectIfEmptyOrWhitespace(errors, "password", "field.required");
         UserLogin login = (UserLogin) target;
         if (login.getPassword() != null
               && login.getPassword().trim().length() < MINIMUM_PASSWORD_LENGTH) {
    
    
            errors.rejectValue("password", "field.min.length",
                  new Object[]{
    
    Integer.valueOf(MINIMUM_PASSWORD_LENGTH)},
                  "The password must be at least [" + MINIMUM_PASSWORD_LENGTH + "] characters in length.");
         }
      }
   }

其中用到了ValidationUtils这个类,这个类中封装了一些简单的方法可以供我们使用。

综合来看,Validator接口总体来说是一个过时、落后的接口,适用度并不强。

三、Errors 接口设计

接口职责:
数据绑定和校验错误收集接口,与 Java Bean 和其属性有强关联性

核心方法:
reject 方法(重载):收集错误文案
rejectValue 方法(重载):收集对象字段中的错误文案

配套组件:
Java Bean 错误描述:org.springframework.validation.ObjectError
Java Bean 属性错误描述:org.springframework.validation.FieldError

我们分析一下org.springframework.validation.Errors源码,似乎看起来挺难以理解的。

它与Spring的数据绑定有着强关联性,后续再慢慢分析。

1、Errors 文案来源

Errors 文案生成步骤
(1)选择 Errors 实现(如:org.springframework.validation.BeanPropertyBindingResult)
(2)调用 reject 或 rejectValue 方法
(3)获取 Errors 对象中 ObjectError 或 FieldError
(4)将 ObjectError 或 FieldError 中的 code 和 args,关联 MessageSource 实现(如:ResourceBundleMessageSource)

2、代码实例

import org.springframework.context.MessageSource;
import org.springframework.context.support.StaticMessageSource;
import org.springframework.validation.BeanPropertyBindingResult;
import org.springframework.validation.Errors;
import org.springframework.validation.FieldError;
import org.springframework.validation.ObjectError;

import java.util.List;
import java.util.Locale;

/**
 * 错误文案示例
 * @see Errors
 */
public class ErrorsMessageDemo {
    
    

    public static void main(String[] args) {
    
    

        // 0. 创建 User 对象
        User user = new User();
        user.setName("张三");
        // 1. 选择 Errors - BeanPropertyBindingResult
        Errors errors = new BeanPropertyBindingResult(user, "user");
        // 2. 调用 reject 或 rejectValue
        // reject 会生成 ObjectError
        // rejectValue 会生成 FieldError
        errors.reject("user.properties.not.null");
        // user.name = user.getName()
        errors.rejectValue("name", "name.required");

        // 3. 获取 Errors 中 ObjectError 和 FieldError
        // FieldError is ObjectError
        List<ObjectError> globalErrors = errors.getGlobalErrors();
        List<FieldError> fieldErrors = errors.getFieldErrors();
        List<ObjectError> allErrors = errors.getAllErrors();

        // 4. 通过 ObjectError 和 FieldError 中的 code 和 args 来关联 MessageSource 实现
        MessageSource messageSource = createMessageSource();

        for (ObjectError error : allErrors) {
    
    
            String message = messageSource.getMessage(error.getCode(), error.getArguments(), Locale.getDefault());
            System.out.println(message);
        }
    }

    static MessageSource createMessageSource() {
    
    
        StaticMessageSource messageSource = new StaticMessageSource();
        messageSource.addMessage("user.properties.not.null", Locale.getDefault(), "User 所有属性不能为空");
        messageSource.addMessage("id.required", Locale.getDefault(), "the id of User must not be null.");
        messageSource.addMessage("name.required", Locale.getDefault(), "the name of User must not be null.");
        return messageSource;
    }
}

四、自定义 Validator

实现 org.springframework.validation.Validator 接口:

  • 实现 supports 方法
  • 实现 validate 方法
    • 通过 Errors 对象收集错误
      • ObjectError:对象(Bean)错误:
      • FieldError:对象(Bean)属性(Property)错误
    • 通过 ObjectError 和 FieldError 关联 MessageSource 实现获取最终文案
import org.springframework.context.MessageSource;
import org.springframework.context.support.StaticMessageSource;
import org.springframework.validation.*;
import java.util.Locale;

/**
 * 自定义 Spring {@link Validator} 示例
 * @see Validator
 */
public class ValidatorDemo {
    
    


    public static void main(String[] args) {
    
    
        // 1. 创建 Validator
        Validator validator = new UserValidator();
        // 2. 判断是否支持目标对象的类型
        User user = new User();
        System.out.println("user 对象是否被 UserValidator 支持检验:" + validator.supports(user.getClass()));
        // 3. 创建 Errors 对象
        Errors errors = new BeanPropertyBindingResult(user, "user");
        validator.validate(user, errors);

        // 4. 获取 MessageSource 对象
        MessageSource messageSource = createMessageSource();

        // 5. 输出所有的错误文案
        for (ObjectError error : errors.getAllErrors()) {
    
    
            String message = messageSource.getMessage(error.getCode(), error.getArguments(), Locale.getDefault());
            System.out.println(message);
        }
    }

    static class UserValidator implements Validator {
    
    

        @Override
        public boolean supports(Class<?> clazz) {
    
    
            return User.class.isAssignableFrom(clazz); // class校验
        }

        @Override
        public void validate(Object target, Errors errors) {
    
    
            User user = (User) target;
            ValidationUtils.rejectIfEmptyOrWhitespace(errors, "id", "id.required");
            ValidationUtils.rejectIfEmptyOrWhitespace(errors, "name", "name.required");
            String userName = user.getName();
            // ... 其他校验
        }
    }

    static MessageSource createMessageSource() {
    
    
        StaticMessageSource messageSource = new StaticMessageSource();
        messageSource.addMessage("user.properties.not.null", Locale.getDefault(), "User 所有属性不能为空");
        messageSource.addMessage("id.required", Locale.getDefault(), "the id of User must not be null.");
        messageSource.addMessage("name.required", Locale.getDefault(), "the name of User must not be null.");
        return messageSource;
    }
}

五、Spring与Validator的适配

实际上,Spring Validator 接口用起来是比较难用的,更何况还得创建Error对象,所以Spring和Validation 进行了强绑定。

Bean Validation 与 Validator 适配:
• 核心组件 - org.springframework.validation.beanvalidation.LocalValidatorFactoryBean
• 依赖 Bean Validation - JSR-303 或 JSR-349 provider
• Bean 方法参数校验 - org.springframework.validation.beanvalidation.MethodValidationPostProcessor

1、代码实例

使用JSR-303进行校验

import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.validation.annotation.Validated;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
import org.springframework.validation.beanvalidation.MethodValidationPostProcessor;

import javax.validation.Valid;
import javax.validation.Validator;
import javax.validation.constraints.NotNull;

/**
 * Spring Bean Validation 整合示例
 *
 * @see Validator
 * @see LocalValidatorFactoryBean
 */

public class SpringBeanValidationAnnoDemo {
    
    

    public static void main(String[] args) {
    
    
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
        // 注册当前类作为 Configuration Class
        context.register(SpringBeanValidationAnnoDemo.class, UserProcessor.class);
        // 启动 Spring 应用上下文
        context.refresh();

        UserProcessor userProcessor = context.getBean(UserProcessor.class);
        userProcessor.process(new User());

        // 关闭 Spring 应用上下文
        context.close();
    }

    @Bean
    public LocalValidatorFactoryBean validator(){
    
    
        return new LocalValidatorFactoryBean();
    }

    @Bean
    public MethodValidationPostProcessor methodValidationPostProcessor(Validator validator){
    
    
        MethodValidationPostProcessor methodValidationPostProcessor = new MethodValidationPostProcessor();
        methodValidationPostProcessor.setValidator(validator);
        return methodValidationPostProcessor;
    }



    static class User {
    
    

        @NotNull
        private String name;

        public String getName() {
    
    
            return name;
        }

        public void setName(String name) {
    
    
            this.name = name;
        }

        @Override
        public String toString() {
    
    
            return "User{" +
                    "name='" + name + '\'' +
                    '}';
        }
    }
}
@Validated
class UserProcessor {
    
    

    public void process(@Valid SpringBeanValidationAnnoDemo.User user) {
    
    
        System.out.println(user);
    }

}

如果校验失败,会提示国际化文案:

Exception in thread "main" javax.validation.ConstraintViolationException: process.user.name: 不能为null

2、与国际化整合

从LocalValidatorFactoryBean(Bean Validation 适配器)类中有一个setValidationMessageSource方法中注入MessageSource。

3、Jakarta Bean Validation(JSR-303)自定义Validator注解

这里提供一个简单的实例,关于Jakarta Bean Validation更多后续出一篇文章来介绍,可以先看官方文档:

https://jakarta.ee/specifications/bean-validation/3.0/jakarta-bean-validation-spec-3.0.html

import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

/**
 * null 或 指定长度范围内
 */
@Target({
    
     METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = {
    
    SizeOrNullValidator.class})
public @interface SizeOrNull {
    
    
    String message() default "{javax.validation.constraints.Size.message}";

    Class<?>[] groups() default {
    
     };

    Class<? extends Payload>[] payload() default {
    
     };

    /**
     * @return size the element must be higher or equal to
     */
    int min() default 0;

    /**
     * @return size the element must be lower or equal to
     */
    int max() default Integer.MAX_VALUE;

    boolean notBlank() default false;
}



import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;

/**
 * null 或 指定长度范围内
 */
public class SizeOrNullValidator implements ConstraintValidator<SizeOrNull, Object> {
    
    

    private SizeOrNull annotation;

    @Override
    public void initialize(SizeOrNull constraintAnnotation) {
    
    
        this.annotation = constraintAnnotation;
    }

    @Override
    public boolean isValid(Object value, ConstraintValidatorContext context) {
    
    
        if (value == null) {
    
    
            return true;
        }

        if (value instanceof CharSequence) {
    
    
            CharSequence cs = (CharSequence) value;
            if (annotation.notBlank() && StringUtils.isBlank(cs)) {
    
    
                return false;
            }
            int length = StringUtils.length(cs);
            return length >= annotation.min() && length <= annotation.max();
        } else {
    
    
            int size = CollectionUtils.size(value);
            return size >= annotation.min() && size <= annotation.max();
        }
    }
}
@CaptchaReceiver(groups = {
    
    CAPTCHA.class}) // 检查用于接收验证码的手机或邮箱
public class AuthenticationPacket {
    
    
	// 省略属性
}


import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

/**
 * 校验单个Bean
 */
@Target({
    
     METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = {
    
    CaptchaReceiverValidator.class})
public @interface CaptchaReceiver {
    
    

    String message() default "参数错误";

    Class<?>[] groups() default {
    
    };

    Class<? extends Payload>[] payload() default {
    
     };
}




import org.apache.commons.lang3.StringUtils;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;

public class CaptchaReceiverValidator implements ConstraintValidator<CaptchaReceiver, AuthenticationPacket> {
    
    

//    private CaptchaReceiver annotation;

    @Override
    public void initialize(CaptchaReceiver constraintAnnotation) {
    
    
//        this.annotation = constraintAnnotation;
    }

    @Override
    public boolean isValid(AuthenticationPacket packet, ConstraintValidatorContext context) {
    
    
        if (packet.getMode() == AuthenticationMode.MobileCaptcha && StringUtils.isBlank(packet.getMobile())) {
    
    
            context.disableDefaultConstraintViolation();
            context.buildConstraintViolationWithTemplate("请填写手机号码。").addConstraintViolation();
            return false;
        }
        if (packet.getMode() == AuthenticationMode.EmailCaptcha && StringUtils.isBlank(packet.getEmail())) {
    
    
            context.disableDefaultConstraintViolation();
            context.buildConstraintViolationWithTemplate("请填写邮箱地址。").addConstraintViolation();
            return false;
        }
        return true;
    }
}

参考资料

JSR-303校验注解汇总
极客时间-《小马哥讲 Spring 核心编程思想》

猜你喜欢

转载自blog.csdn.net/A_art_xiang/article/details/128712194
今日推荐