一起学SF框架系列5.11-spring-beans-数据校验validation

    在日常的项目开发中,应用在执行业务逻辑之前,为了防止非法参数对业务造成的影响,必须通过校验保证传入数据是合法正确的,但很多时候同样的校验出现了多次,在不同的层,不同的方法上,导致代码冗余,违反DRY原则。
    Java提供了数据校验规范来解决这个问题,可极大的简化校验实现,节省大量的工作量。Spring作为开发框架,支持相关规范,除了规范要求外,并提供了额外的增强。

Java数据校验规范

规范定义

    Java为Bean数据合法性校验提供了标准框架规范,它定义了一套可标注在成员变量,属性方法上的校验注解。最初版本为 Java Bean Validation1.0(JSR-303)、Java Bean Validation1.1(JSR-349),Java Bean Validation2.0(JSR-380),目前最新版本为Java Bean Validation3.0(同2.0最要变化是命名空间变为"jakarta.validation.*)。需要注意的是,JSR只是一项标准,它规定了一些校验注解的规范,但没有实现。
规范官网地址:https://beanvalidation.org/

常见注解

---- Java Bean Validation1.0 ----
@Null 被注释的元素必须为 null
@NotNull 被注释的元素必须不为 null
@AssertTrue 被注释的元素必须为 true
@AssertFalse 被注释的元素必须为 false
@Min(value) 被注释的元素必须是一个数字,其值必须大于等于指定的最小值
@Max(value) 被注释的元素必须是一个数字,其值必须小于等于指定的最大值
@DecimalMin(value) 被注释的元素必须是一个数字,其值必须大于等于指定的最小值
@DecimalMax(value) 被注释的元素必须是一个数字,其值必须小于等于指定的最大值
@Size(max=, min=) 被注释的元素的大小必须在指定的范围内
@Digits (integer, fraction) 被注释的元素必须是一个数字,其值必须在可接受的范围内
@Past 被注释的元素必须是一个过去的日期
@Future 被注释的元素必须是一个将来的日期
@Pattern(regex=,flag=) 被注释的元素必须符合指定的正则表达式

---- Java Bean Validation1.1 ----
@Valid 用于嵌套校验,可以对一个对象中的属性进行递归校验。
@ConvertGroup 用于分组校验,可以指定校验的分组,根据不同的分组执行不同的校验规则。
@GroupSequence 用于定义校验分组的顺序,指定不同分组的执行顺序。
增加了允许对任意方法和构造函数的参数和返回值进行约束
提供了一种在进行级联(嵌套)验证时更改目标组的方法。
约束消息可以使用EL表达式进行更灵活的呈现和字符串格式设置。同样,在EL上下文中也可以使用验证值。
支持容器的校验,通过TYPE_USE类型的注解实现对容器内容的约束:List<@Email String>

---- Java Bean Validation2.0 ----
@Negative 被注释的元素必须为 负数
@NegativeOrZero 被注释的元素必须为 负数或0
@Positive 被注释的元素必须为 正数(0为非法值)
@PositiveOrZero 被注释的元素必须为 正数或0
@PastOrPresent 被注释的元素必须是一个过去或当前的日期
@Future OrPresent 被注释的元素必须是一个将来或当前的日期
@NotEmpty 被注释的元素不能为NULL或者是EMPTY
@NotBlank 被注释的元素(字符串)不能为Null且至少包含一个非空格字符(去掉前后空格判断)
@Email 被注释的元素(字符串)必须满足email格式

---- Hibernate Validator增强 ----
@Range(min=, max=) 被注释的元素必须位于(包括)指定的最小值和最大值之间
@Length(min=, max=) 被注释的元素(字符串)长度必须在给定的范围之内,包含两端

---- Spring Validator增强 ----
@Validated Spring Validator提供的增强,支持分组校验

@Valid和@Validated区别

1、@Valid是JSR标准规范,@Validated是Spring Validator提供的。
2、@Valid可支持METHOD, FIELD, CONSTRUCTOR, PARAMETER, TYPE_USE标注,不支持类上注解;@Validated支持TYPE,METHOD,PARAMETER上注解,支持类上注解,不支持属性上注解。
3、@Valid直接嵌套对象自动校验,@Validated不支持(因为不支持属性上注解)。
4、@Valid 不支持分组校验,@Validated支持。

规范实现

Hibernate Validator是Java规范官方认证的实现(如下图),实现见:https://hibernate.org/validator/
在这里插入图片描述

Spring Validator

Spring提供了数据校验功能:Spring提供了Validator接口契约,可用于应用程序的每一层。

Validator接口

Spring提供了一个Validator接口(org.springframework.validation.Validator)(代码如下),使用它来验证对象;验证类需实现Validator接口,通过validate方法完成具体验证。

public interface Validator {
    
    
	// 是否当前当前验证器支持的类
	boolean supports(Class<?> clazz);
	// 验证给定的对象,如果出现验证错误,则将这些对象注册到给定的errors对象中 注1
	void validate(Object target, Errors errors);
}

注1:Errors是Spring用来存储并公开特定对象的数据绑定和验证错误信息接口,方法较多,默认实现BeanPropertyBindingResult,其类关系图如下:
在这里插入图片描述
另:SmartValidator接口继承于Validator接口,主要增强支持来自验证器外部提示的功能。

工具类

Spring提供实用工具类ValidationUtils,主要处理拒绝空字段的方法。为防止实例化,定义为抽象类,所有实现方法均是静态方法。

public abstract class ValidationUtils {
    
    
	private static final Log logger = LogFactory.getLog(ValidationUtils.class);


	/**
	 * 简易方式调用具体验证器进行验证
	 * validator-验证器
	 * target-验证对象
	 * errors-保存错误的errors对象
	 */
	public static void invokeValidator(Validator validator, Object target, Errors errors) {
    
    
		invokeValidator(validator, target, errors, (Object[]) null);
	}
	/**
	 * 调用具体验证器进行验证(实现)
	 * validator-验证器
	 * target-验证对象
	 * errors-保存错误的errors对象
	 * validationHints-提示对象(可多个)
	 */
	public static void invokeValidator(
			Validator validator, Object target, Errors errors, @Nullable Object... validationHints) {
    
    

		//对应参数不能空,否则程序会直接结束
		Assert.notNull(validator, "Validator must not be null");
		Assert.notNull(target, "Target object must not be null");
		Assert.notNull(errors, "Errors object must not be null");

		if (logger.isDebugEnabled()) {
    
    
			logger.debug("Invoking validator [" + validator + "]");
		}
		if (!validator.supports(target.getClass())) {
    
    
			throw new IllegalArgumentException(
					"Validator [" + validator.getClass() + "] does not support [" + target.getClass() + "]");
		}

		if (!ObjectUtils.isEmpty(validationHints) && validator instanceof SmartValidator smartValidator) {
    
    
			// 如果校验器是SmartValidator,调用smartValidator.validate
			smartValidator.validate(target, errors, validationHints);
		}
		else {
    
    
			// 如果校验器不是SmartValidator,调用validator.validate
			validator.validate(target, errors);
		}

		if (logger.isDebugEnabled()) {
    
    
			if (errors.hasErrors()) {
    
    
				logger.debug("Validator found " + errors.getErrorCount() + " errors");
			}
			else {
    
    
				logger.debug("Validator found no errors");
			}
		}
	}

    //字符串空验证(用错误代码获取错误信息)
	public static void rejectIfEmpty(Errors errors, String field, String errorCode) {
    
    
		rejectIfEmpty(errors, field, errorCode, null, null);
	}
    //字符串空验证(defaultMessage-提供默认错误信息)
	public static void rejectIfEmpty(Errors errors, String field, String errorCode, String defaultMessage) {
    
    
		rejectIfEmpty(errors, field, errorCode, null, defaultMessage);
	}
    //字符串空验证(errorArgs-格式化错误信息对应参数)
	public static void rejectIfEmpty(Errors errors, String field, String errorCode, Object[] errorArgs) {
    
    
		rejectIfEmpty(errors, field, errorCode, errorArgs, null);
	}
    //字符串空验证真正实现
	public static void rejectIfEmpty(Errors errors, String field, String errorCode,
			@Nullable Object[] errorArgs, @Nullable String defaultMessage) {
    
    

		Assert.notNull(errors, "Errors object must not be null");
		Object value = errors.getFieldValue(field);
		if (value == null || !StringUtils.hasLength(value.toString())) {
    
    
			errors.rejectValue(field, errorCode, errorArgs, defaultMessage);
		}
	}

    //字符串空或仅仅空格 验证
	public static void rejectIfEmptyOrWhitespace(Errors errors, String field, String errorCode) {
    
    
		rejectIfEmptyOrWhitespace(errors, field, errorCode, null, null);
	}
    //字符串空或仅仅空格 验证(defaultMessage-提供默认错误信息)
	public static void rejectIfEmptyOrWhitespace(
			Errors errors, String field, String errorCode, String defaultMessage) {
    
    

		rejectIfEmptyOrWhitespace(errors, field, errorCode, null, defaultMessage);
	}
    //字符串空或仅仅空格 验证(errorArgs-格式化错误信息对应参数)
	public static void rejectIfEmptyOrWhitespace(
			Errors errors, String field, String errorCode, @Nullable Object[] errorArgs) {
    
    

		rejectIfEmptyOrWhitespace(errors, field, errorCode, errorArgs, null);
	}
    //字符串空或仅仅空格 验证实现
	public static void rejectIfEmptyOrWhitespace(
			Errors errors, String field, String errorCode, @Nullable Object[] errorArgs, @Nullable String defaultMessage) {
    
    

		Assert.notNull(errors, "Errors object must not be null");
		Object value = errors.getFieldValue(field);
		if (value == null ||!StringUtils.hasText(value.toString())) {
    
    
			errors.rejectValue(field, errorCode, errorArgs, defaultMessage);
		}
	}
}

WEB项目应用

validation在web项目中的应用是最经典的,这也是validation的初衷。针对spring-boot或springMVC项目,如何使用Spring Validation在网上非常多,在此不赘述。

非WEB项目应用

非web项目应用,网上资料较少,本文着重讲这方面示例。

配置

在pom.xnl引入如下配置:

    <!-- hibernate-validator7对应tomcat-embed-el10支持jakarta;hibernate-validator6对应tomcat-embed-el8支持javax -->
    <dependency>
        <groupId>org.hibernate</groupId>
        <artifactId>hibernate-validator</artifactId>
        <version>7.0.5.Final</version>
    </dependency>
    <dependency>
	    <groupId>org.apache.tomcat.embed</groupId>
	    <artifactId>tomcat-embed-el</artifactId>
	    <version>10.0.5</version>
    </dependency>

提醒要特别注意版本匹配问题:
hibernate-validator7.0.5对应tomcat-embed-el10.0.5支持jakarta;
hibernate-validator6.2.5对应tomcat-embed-el8.5.29支持javax

原始方式应用

原始方式不是主要的应用方式,但理解原始方式有助于理解Spring实现校验的机制。

校验对象

public class Address {
    
    
	String country="CN";
	String province;
	String city;
	String county;
	// 省略get/set方法
}
public class Driver {
    
    
	String surname;  // 姓
	String name; // 名
	Integer age;
	Address address;
	// 省略get/set方法
}

实现校验类

@Component
public class AddressValidator implements Validator {
    
    

	//必须实现的接口方法-指定验证的类
	public boolean supports(Class clazz) {
    
    
		return Address.class.equals(clazz);
	}

	//必须实现的接口方法-指定验证的类
	public void validate(Object obj, Errors e) {
    
    
		ValidationUtils.rejectIfEmpty(e, "country", "country.empty");
		ValidationUtils.rejectIfEmpty(e, "province", "province.empty");
		ValidationUtils.rejectIfEmpty(e, "city", "city.empty");
		ValidationUtils.rejectIfEmpty(e, "county", "county.empty");
		ValidationUtils.rejectIfEmpty(e, "desc", "desc.empty");
	}
}
@Component
public class DriverValidator  implements Validator {
    
    
	@Autowired
	AddressValidator addressValidator;
	
	public boolean supports(Class clazz) {
    
    
		return Driver.class.isAssignableFrom(clazz);
	}

	public void validate(Object target, Errors e) {
    
    
		ValidationUtils.rejectIfEmptyOrWhitespace(e, "surname", "field.required");
		ValidationUtils.rejectIfEmptyOrWhitespace(e, "name", "field.required");
		Driver driver = (Driver) target;
		if (driver.getAge() < 18) {
    
    
			e.rejectValue("age", "negativevalue","未满18岁不能领取驾照");
		} else if (driver.getAge() > 70) {
    
    
			e.rejectValue("age", "too.darn.old","超过70岁不能领取驾照");
		}
		try {
    
    
			//嵌套验证路径入栈
			e.pushNestedPath("address");
			ValidationUtils.invokeValidator(this.addressValidator, driver.getAddress(), e);
		} finally {
    
    
			//嵌套验证路径出栈
			e.popNestedPath();
		}
	}
}

校验

	public void demo() {
    
    
		Address addr=new Address();
		addr.setProvince("四川");
		addr.setCity("成都");
		addr.setCounty("高新区");
		addr.setDesc("详细地址");

		Driver driver=new Driver();
		driver.setSurname("wang");
		driver.setName("wang");
		driver.setAge(20);
		driver.setAddress(addr);
		
		// 用DataBinder实现校验 注1
        DataBinder binder = new DataBinder(driver);
        binder.setValidator(driverValidator);
        // 调用校验
        binder.validate();
     	// 获取校验结果
        BindingResult results = binder.getBindingResult();
        
        System.out.println(results);
	}

注1:DataBinder见https://blog.csdn.net/davidwkx/article/details/131913078

注解方式应用

注解方式是应用实现的主要方式,简单高效。

校验对象

public class AddressAnnotateValidator {
    
    
	@NotBlank(message = "country 不能为空",groups = ValidGroupUpdate.class)
	String country="CN";
	@NotBlank
	String province;
	@NotBlank
	String city;
	@NotBlank
	String county;
	// 区(county)以下的地址描述
	@NotBlank
	String desc;
	// 省略get/set方法
}
public class DriverAnnotateValidator {
    
    
    private Integer id;
    @NotBlank(message = "姓不可以为空")
    @Length(min = 1, max = 20, message = "姓长度需要在20个字以内")
    private String surname;
    @NotBlank(message = "名不可以为空")
    @Length(min = 1, max = 20, message = "名长度需要在20个字以内")
    private String name;
    // 电话号码校验器是自己实现的 注1
	@NotBlank(message = "电话不可以为空")
    @Length(min = 1, max = 11, message = "电话长度需要在11个字以内")
    @MobilePhoneCheck
    private String mobilePhone;
    @NotBlank(message = "邮箱不允许为空")
    @Email(message = "邮箱格式不正确")
    @Length(min = 5, max = 50, message = "邮箱长度需要在50个字符以内")
    private String mail;
    @Max(70)
    @Min(18)
    private int age;
    @NotNull(message = "联系地址不可以为空")
    @Valid
    private AddressAnnotateValidator addr;
    // 省略get/set方法
}

注1:利用注解,可处理大量空、必须输入等繁琐校验。但有些校验很复杂,规范中注解不能支持,就需要自定义校验注解。

自定义校验注解

下面以定义移动电话校验注解为例,示例如何自定义校验注解。
1、定义校验注解MobilePhoneCheck

//我们可以直接拷贝系统内的注解如@Min,复制到我们新的注解中,然后根据需要修改。
@Target({
    
    METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})
@Retention(RUNTIME)
@Documented
// 关联注解的实现类
@Constraint(validatedBy = {
    
    MobilePhoneValidator.class})
public @interface MobilePhoneCheck {
    
    
	//校验错误的默认信息
	String message() default "手机号码格式有问题";
	//是否强制校验
	boolean isRequired() default false;
	Class<?>[] groups() default {
    
    };
	Class<? extends Payload>[] payload() default {
    
    };
}

2、注解实现类MobilePhoneValidator(class)

// 实现的接口是MobilePhoneCheck注解
@Component
public class MobilePhoneValidator  implements ConstraintValidator<MobilePhoneCheck, String> {
    
    	 
	 private boolean required = false;
	 // 用正则表达式判断手机号码格式合法性
	 private static final Pattern mobile_pattern = Pattern.compile("^[1](([3][0-9])|([4][5-9])|([5][0-3,5-9])|([6][5,6])|([7][0-8])|([8][0-9])|([9][1,8,9]))[0-9]{8}$");
	 
	 // 注解接口方法的标准实现(必须)
	 @Override
	 public void initialize(MobilePhoneCheck constraintAnnotation) {
    
    
		 required = constraintAnnotation.isRequired();
	 }
	 // 注解接口方法的实现(必须)-验证时真正使用的接口
	 @Override
	 public boolean isValid(String phone, ConstraintValidatorContext constraintValidatorContext) {
    
    
		  //是否为手机号的实现
		  if (required) 
			  return isMobile(phone);
		  if (!StringUtils.hasText(phone)) 
			  return true;
		  return isMobile(phone);
	  }

	 //手机号码合法性判断
	 public static boolean isMobile(String src) {
    
    
		  if (!StringUtils.hasText(src)) 
			  return false;
		  Matcher m = mobile_pattern.matcher(src);
		  return m.matches();
	 }
}

校验对象

	public void demo() {
    
    
		AddressAnnotateValidator addr=new AddressAnnotateValidator();
		addr.setProvince("四川");
		addr.setCity("成都");
//		addr.setCounty("高新区");
		addr.setDesc("详细地址");
		DriverAnnotateValidator driver=new DriverAnnotateValidator();
		driver.setSurname("wang");
		driver.setName("wang");
		driver.setAge(15);
		driver.setMobilePhone("13300333691");
		driver.setAddr(addr);
		
		//获取校验结果(如何集合为空,表示无错误)  注1
		 Map<String,String> validateResult=BeanValidatorUtil.validate(driver);
 		 System.out.println(validateResult);
	}

注1:需项目提供校验方式。附BeanValidatorUtil代码:

public class BeanValidatorUtil {
    
    
	//用spring的校验器
//    private static final ValidatorFactory validatorFactory = Validation.buildDefaultValidatorFactory();
	//用Hibernate的校验器
    private static final ValidatorFactory validatorFactory = Validation.byProvider( HibernateValidator.class )
            .configure()
            .addProperty( "hibernate.validator.fail_fast", "true" )  //快速检测模式:不检测全部错误,碰到第一个错误就返回
            .buildValidatorFactory();
	
    //单个对象校验
    public static <T> Map<String,String> validate(T t, Class... groups){
    
    
    	//取校验器
        Validator validator=validatorFactory.getValidator();
        
        //校验
        Set<ConstraintViolation<T>> validateResult=validator.validate(t,groups);
        //如果为空
        if (validateResult.isEmpty()){
    
    
            return Collections.emptyMap();
        }else{
    
    
            //不为空时表示有错误(属性为key,错误信息为value的map) 注1
        	return validateResult.stream().collect(Collectors.toMap(k -> k.getPropertyPath().toString(), v -> v.getPropertyPath() + " " + v.getMessage() + ": " + v.getInvalidValue(), (key1, key2) -> key2));
        }
    }
    //集合对象校验
    public static Map<String,String> validateList(Collection<?> collection){
    
    
        if(collection==null || collection.size()<1)
            return Collections.emptyMap();

        Map<String,String> errors=Collections.emptyMap();
        for(Object el:collection) {
    
    
        	errors=validate(el,new Class[0]);
        	if(!errors.isEmpty())  //有错
        		break;
        }
        return errors;
    }

     // 校验某一对象是否合法
    public static Map<String,String> validateObject(Object first,Object... objects){
    
    
        if (objects !=null && objects.length > 0 ){
    
    
            return validateList(Arrays.asList(first,objects));
        } else {
    
    
            return validate(first , new Class[0]);
        }
    }
}

注1:实际应用最好是抛出ConstraintViolationException异常,然后增加全局异常处理,这样程序处理很简单:每个方法只需要在第一行增加校验参数即可。

猜你喜欢

转载自blog.csdn.net/davidwkx/article/details/131401699