在日常的项目开发中,应用在执行业务逻辑之前,为了防止非法参数对业务造成的影响,必须通过校验保证传入数据是合法正确的,但很多时候同样的校验出现了多次,在不同的层,不同的方法上,导致代码冗余,违反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异常,然后增加全局异常处理,这样程序处理很简单:每个方法只需要在第一行增加校验参数即可。