Spring Validation数据校验

时间:2025-02-24 21:24:35

Spring Validation是SpringFramework提供的一种轻量级的数据验证框架,用于Java对象进行校验。Spring Validation(Spring的数据验证组件)其实是一个抽象层,它为数据验证提供了统一的接口和基本的校验功能。Spring Validation默认使用了Hibernate Validator作为其具体的实现,但是也可以通过适配器与其他数据验证框架(如Apache Commons Validator)一起工作。

Spring Validation的主要功能包括:

1、提供了一套注解,用于对Java对象进行校验;

2、支持嵌套校验,用于对一个对象中的属性进行递归校验;

3、支持分组校验,用于根据不同的校验场景,使用不同的校验规则;

4、支持国际化,可以根据不同的语言环境,使用不同的校验提示消息。

5、支持自定义注解和校验器,满足各种复杂的校验需求。

Spring 提供的数据校验方式:

  • 实现接口,调用接口实现类;
  • 通过 注解 方式进行数据校验(按照Bean Validation方式);
  • 基于 方法(函数) 实现数据校验;
  • 自定义校验

依赖引入:

如果springboot版本小于2.,spring-boot-web-starter会自动引入hibernate-validator。如果spring-boot版本为2.,则需要手动引入依赖,如:

<dependency>  
    <groupId></groupId>  
    <artifactId>hibernate-validator</artifactId>  
    <version>6.0.</version>  
</dependency>

1、Validator接口方式

@Data
public class Person {

    private String name;

    private int age;

}

import ;
import ;
import ;

/**
 * @description 实现接口{@link }
 */
public class PersonValidator implements Validator {
    @Override
    public boolean supports(Class<?> clazz) {
        return (clazz);
    }

    @Override
    public void validate(Object obj, Errors errors) {
        //设置name为空时,报错:
        (errors, "name", "");

        //传入对象,强转为实体类Person对象
        Person p = (Person) obj;
        if (() < 0) {     //设置age属性小于零时报错
            ("age", "error (age  < 0)");
        } else if (() > 110) {//设置age属性大于110报错
            ("age", "error (age  > 110) too old !!");
        }
    }
}

/**
 * 测试
 *
 */
@Slf4j
class IomsApplicationTests {
    public static void main(String[] args) {
        Person person = new Person();
//        ("高启强");
//        (29);

        //创建person对象的DataBinder
        DataBinder binder = new DataBinder(person);
        //设置校验
        (new PersonValidator());
        //校验(当person属性值为空时,校验不通过)
        ();

        //输出校验结果
        BindingResult bindingResult = ();
        (());

    }


}

2、基于注解方式(Bean Validation)

使用Bean Validation校验方式,需要将Bean Validation需要的和注入到容器中。Spring默认有一个实现类LocalValidatorFactoryBean,它实现了Bean Validator中的接口和接口。

在springboot2.2.2中已自动注入,源码如下:

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass()
@ConditionalOnResource(resources = "classpath:META-INF/services/")
@Import()
public class ValidationAutoConfiguration {

	@Bean
	@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
	@ConditionalOnMissingBean()
	public static LocalValidatorFactoryBean defaultValidator() {
		LocalValidatorFactoryBean factoryBean = new LocalValidatorFactoryBean();
		MessageInterpolatorFactory interpolatorFactory = new MessageInterpolatorFactory();
		(());
		return factoryBean;
	}

    /**
     * 基于方法的校验方式
     */
	@Bean
	@ConditionalOnMissingBean
	public static MethodValidationPostProcessor methodValidationPostProcessor(Environment environment,
			@Lazy Validator validator) {
		MethodValidationPostProcessor processor = new MethodValidationPostProcessor();
		boolean proxyTargetClass = ("-target-class", , true);
		(proxyTargetClass);
		(validator);
		return processor;
	}

}

Spring Validation常用的注解
@NotNull:检查是否为null,不能为null。

@NotBlank:检查字符串是否为null或空字符串。

@NotEmpty:检查字符串、集合或数组是否为null或空。

@Min:检查数字是否大于等于指定值。

@Max:检查数字是否小于等于指定值。

@DecimalMin:检查数字是否大于等于指定值。

@DecimalMax:检查数字是否小于等于指定值。

@Size:检查字符串、集合或数组的长度是否在指定范围内。

@Digits:检查数字是否符合指定的精度和小数位数。

@Past:检查日期是否在当前时间之前。

@Future:检查日期是否在当前时间之后。

@Pattern:检查字符串是否匹配指定的正则表达式。

@Email:检查是否为有效的电子邮件地址。

@Length:检查字符串的长度是否在指定范围内。

@Range:检查数字是否在指定范围内。

@Positive:检查数字是否为正数。

@PositiveOrZero:检查数字是否为非负数。

@Negative:检查数字是否为负数。

@NegativeOrZero:检查数字是否为非正数。

@AssertTrue:检查是否为true。

@AssertFalse:检查是否为false。

@NotNull(message = “{}”):使用国际化消息提示。

@NotBlank(message = “{}”):使用国际化消息提示。

@NotEmpty(message = “{}”):使用国际化消息提示。

@Email(message = “{}”):使用国际化消息提示。

@Valid:用于嵌套校验,可以对一个对象中的属性进行递归校验。

@ConvertGroup:用于分组校验,可以指定校验的分组,根据不同的分组执行不同的校验规则。

@GroupSequence:用于定义校验分组的顺序,指定不同分组的执行顺序。

手动校验:通过 "校验器+注解校验"
 

import ;
import ;
import ;

@Data
public class User {
    @NotEmpty  //不可为空
    private String name;

    @Min(0)   //最小值
    @Max(110) //最大值
    private int age;
}
 

 使用java原生的校验器

import ;
import ;
import ;
import ;
import ;

/**
 * 使用java原生的校验
 * 
 */
@Service
public class JavaService {

    @Autowired   //自动装配Validator对象
    private Validator validator;

    //校验方法
    public boolean validator(User user){
        //校验后的结果存放进Set集合
        Set<ConstraintViolation<User>> set = (user);
        //若没有校验到错误,集合为空,返回true。
        return ();
    }
}

 使用spring提供的 校验器

import ;
import ;
import ;
import ;

/**
 * 使用spring提供的validate校验方法
 */
@Service
public class SpringService {
    @Autowired
    private Validator validator;

    public boolean validator2(User user){
        BindException bindException = new BindException(user,());
        (user,bindException);            //调用校验方法进行校验
        (());  //输出所有错误信息
        return ();                  //若没有异常,返回false
    }
}

一、基本使用:

对于Web服务来说,为防止非法参数对业务造成影响,在Controller层一定要做好参数校验。大部分情况下,请求参数分为如下两种形式:

1、POST、PUT请求,使用@RequestBody传递参数;

2、GET请求,使用@RequestParam、@PathVariable传递参数;

①、使用@RequestBody传递参数,后端使用DTO(Data Transfer Object 数据传输对象)进行接受,只要给DTO对象加上@Validated注解就能进行自动参数校验。当校验失败时,会抛出MethodArgumentNotValidException异常,Spring 默认会将其转为400(Bad Request)请求。

@Data
@ApiModel("健康度统计")
public class HealthyStatistic {
  
    @Pattern(message = "线路id只能为1-20位数字", regexp = )
    private String lineId;
    
    @Pattern(message = "站点id只能为1-20位数字", regexp = )
    private String stationId;
    
    @Pattern(message = "子系统id只能为1-20位数字", regexp = )
    private String subsystemId;
    
    @EnumValue(message = "月份只能位1-12", intValues = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12})
    private Integer month;
}
/**
 * 在方法参数上声明校验注解@Validated或者@Valid
 */
@PostMapping("/statistic")
public BaseResp healthyStatistic(@Validated @RequestBody HealthyStatistic healthyStatistic) { 
      return ();    
}

②、使用@RequestParam、@PathVariable传递参数,必须在Controller类上标注@Validated注解,

校验失败会抛出ConstraintViolationException异常。

/**
 * 类上标注@Validated注解
 */
@RestController
@RequestMapping("${-name}/alarm/diagnose")
@Validated
public class AlarmDiagnoseController {

    @GetMapping("/get/{userId}")
    public BaseResp getAlarmDiagnose(@PathVariable("userId") @Min(10000000000000000L) Long userId, @RequestParam("alarmId") @NotEmpty String alarmId) {
        
            return (());
    }
}

二、分组校验:

@Data
@EqualsAndHashCode(callSuper = true)
@Accessors(chain = true)
@FieldNameConstants
public class Manufacturer extends Entity {
   
    @NotBlank(message = "厂商id不能为空", groups = )
    private String id;

    private String sn;

    private String name;

    private Integer dockingGatewayId;

    public interface UpdateGroup {
    }
}
@PostMapping("update")
public BaseResp updateManufacturer(@Validated({, }) @RequestBody Manufacturer manufacturer) {
        return (manufacturer);
 }

 三、嵌套校验:

/**
 * Job属性 嵌套校验使用@Valid注解
 */
@Data  
public class UserDTO {    
    @Min(value = 10000000000000000L, groups = )  
    private Long userId;  
  
    @NotNull(groups = {, })  
    @Length(min = 2, max = 10, groups = {, })  
    private String userName;  
  
    @NotNull(groups = {, })  
    @Valid  
    private Job job;  
  
    @Data  
    public static class Job {  
  
        @Min(value = 1, groups = )  
        private Long jobId;  
  
        @NotNull(groups = {, })  
        @Length(min = 2, max = 10, groups = {, })  
        private String jobName;  
  
        @NotNull(groups = {, })  
        @Length(min = 2, max = 10, groups = {, })  
        private String position;  
    }  
  
    public interface Save {  
    }  
    public interface Update {  
    }  
}

四、多字段联合校验:

Hibernate Validator提供了非标准的@GroupSequenceProvider注解。根据当前对象实例的状态,动态来决定加载哪些校验组进入默认校验组。为了实现多字段联合校验,需要借助Hibernate Validator提供的DefaultGroupSequenceProvider接口。

/**
 * 该接口定义了:动态Group序列的协定
 * 要想它生效,需要在T上标注@GroupSequenceProvider注解并且指定此类为处理类
 * 如果`Default`组对T进行验证,则实际验证的实例将传递给此类以确定默认组序列
 */ 
public interface DefaultGroupSequenceProvider<T> {
	/**
     * 合格方法是给T返回默认的组(多个)。因为默认的组是Default
     * 入参T object允许在验证值状态的函数中动态组合默认组序列。(非常强大)
     * object是待校验的Bean。它可以为null哦~(Validator#validateValue的时候可以为null)
     * 返回值表示默认组序列的List。它的效果同@GroupSequence定义组序列,尤其是列表List必须包含类型T
     */
    List<Class<?>> getValidationGroups(T object);
}

实现步骤:

1、实现DefaultGroupSequenceProvider接口

public class PersonGroupSequenceProvider implements DefaultGroupSequenceProvider<Person> {

    @Override
    public List<Class<?>> getValidationGroups(Person bean) {
        List<Class<?>> defaultGroupSequence = new ArrayList<>();
        (); // 这一步不能省,否则Default分组都不会执行了,会抛错的

        if (bean != null) { // 这块判空请务必要做
            Integer age = ();
            if (age < 30) {
                (Person.);
            } else if (age >= 30 && age < 40) {
                (Person.);
            }
        }
        return defaultGroupSequence;
    }
}

2、在待校验的Bean上使用@GroupSequenceProvider注解指定处理器,并定义好校验逻辑(保活分组)

@GroupSequenceProvider()
@Getter
@Setter
@ToString
public class Person {

    @NotNull
    private String name;
    @NotNull
    @Range(min = 10, max = 40)
    private Integer age;

    @NotNull(groups = {, })
    @Size(min = 1, max = 2, groups = )
    @Size(min = 3, max = 5, groups = )
    private List<String> hobbies;

    /**
     * 定义专属的业务逻辑分组
     */
    public interface AgeLt30Group{
    }
    public interface Age30And40Group{
    }
}

五、@GroupSequence(JSR提供),具有控制校验组顺序短路能力

public class User {

    @NotEmpty(message = "firstname may be empty")
    private String firstname;
    @NotEmpty(message = "middlename may be empty", groups = )
    private String middlename;
    @NotEmpty(message = "lastname may be empty", groups = )
    private String lastname;
    @NotEmpty(message = "country may be empty", groups = )
    private String country;


    public interface GroupA {
	}
	public interface GroupB {
	}
	// 组序列
	@GroupSequence({, , })
	public interface Group {
	}
}

3、基于方法的校验(MethodValidationPostProcessor)

@Data
public class User {
    @NotNull
    private String name;
    @Min(0)
    @Max(129)
    private int age;
    //手机号格式 1开头 第二位是(3、4、6、7、9)其一,后面是9位数字
    @Pattern(regexp = "^1(3|4|6|7|9)\\d{9}$", message = "手机号码格式错误")
    @NotBlank(message = "手机号码不能为空")
    private String phone;
}


import ;
import ;

import ;
import ;

@Service
@Validated
public class MethodValidService {

    /**
     * 校验Service层方法参数
     *
     * @param user
     * @return
     */
    public String validParams(@Valid @NotNull User user) {
        return ();
    }
}

 源码解析:

public class MethodValidationPostProcessor extends AbstractBeanFactoryAwareAdvisingPostProcessor
		implements InitializingBean {

	private Class<? extends Annotation> validatedAnnotationType = ;

	@Nullable
	private Validator validator;

	......

    /**
	 * 生成切点AnnotationMatchingPointcut
	 */
	@Override
	public void afterPropertiesSet() {
		Pointcut pointcut = new AnnotationMatchingPointcut(, true);
		 = new DefaultPointcutAdvisor(pointcut, createMethodValidationAdvice());
	}

	/**
	 * 生成切面AOP advice
	 */
	protected Advice createMethodValidationAdvice(@Nullable Validator validator) {
		return (validator != null ? new MethodValidationInterceptor(validator) : new MethodValidationInterceptor());
	}

}
public abstract class AbstractAdvisingBeanPostProcessor extends ProxyProcessorSupport implements BeanPostProcessor {

	@Nullable
	protected Advisor advisor;

	protected boolean beforeExistingAdvisors = false;

	private final Map<Class<?>, Boolean> eligibleBeans = new ConcurrentHashMap<>(256);


	public void setBeforeExistingAdvisors(boolean beforeExistingAdvisors) {
		 = beforeExistingAdvisors;
	}


	@Override
	public Object postProcessBeforeInitialization(Object bean, String beanName) {
		return bean;
	}
    
    /**
     * 通过ProxyFactory返回代理对象
     */
	@Override
	public Object postProcessAfterInitialization(Object bean, String beanName) {
		if ( == null || bean instanceof AopInfrastructureBean) {
			// Ignore AOP infrastructure such as scoped proxies.
			return bean;
		}

		if (bean instanceof Advised) {
			Advised advised = (Advised) bean;
			if (!() && isEligible((bean))) {
				// Add our local Advisor to the existing proxy's Advisor chain...
				if () {
					(0, );
				}
				else {
					();
				}
				return bean;
			}
		}

		if (isEligible(bean, beanName)) {
			ProxyFactory proxyFactory = prepareProxyFactory(bean, beanName);
			if (!()) {
				evaluateProxyInterfaces((), proxyFactory);
			}
			();
			customizeProxyFactory(proxyFactory);
			return (getProxyClassLoader());
		}

		// No proxy needed.
		return bean;
	}

	
	protected boolean isEligible(Object bean, String beanName) {
		return isEligible(());
	}

	protected boolean isEligible(Class<?> targetClass) {
		Boolean eligible = (targetClass);
		if (eligible != null) {
			return eligible;
		}
		if ( == null) {
			return false;
		}
		eligible = (, targetClass);
		(targetClass, eligible);
		return eligible;
	}

	protected ProxyFactory prepareProxyFactory(Object bean, String beanName) {
		ProxyFactory proxyFactory = new ProxyFactory();
		(this);
		(bean);
		return proxyFactory;
	}

	protected void customizeProxyFactory(ProxyFactory proxyFactory) {
	}

}

public class MethodValidationInterceptor implements MethodInterceptor {

	private final Validator validator;

    ......


	@Override
	@SuppressWarnings("unchecked")
	public Object invoke(MethodInvocation invocation) throws Throwable {
		// Avoid Validator invocation on /isSingleton
		if (isFactoryBeanMetadataMethod(())) {
			return ();
		}

		Class<?>[] groups = determineValidationGroups(invocation);

		// Standard Bean Validation 1.1 API
		ExecutableValidator execVal = ();
		Method methodToValidate = ();
		Set<ConstraintViolation<Object>> result;

		try {
			result = (
					(), methodToValidate, (), 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 = (
					((), ().getClass()));
			result = (
					(), methodToValidate, (), groups);
		}
		if (!()) {
			throw new ConstraintViolationException(result);
		}

		Object returnValue = ();

		result = ((), methodToValidate, returnValue, groups);
		if (!()) {
			throw new ConstraintViolationException(result);
		}

		return returnValue;
	}

	......

}

4、自定义校验

        自定义注解,编写校验器实现ConstraintValidator


/**
 * 自定义校验规则的注解,并指定校验器
 *
 */
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = {})
public @interface EnumValue {
    // 默认错误消息
    String message() default "必须为指定值";

    // 字符串类型
    String[] strValues() default {};

    // 整型
    int[] intValues() default {};

    // 枚举类
    Class<?> enumClass() default ;

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

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

    // 指定多个时使用
    @Target({FIELD, METHOD, PARAMETER, ANNOTATION_TYPE})
    @Retention(RUNTIME)
    @Documented
    @interface List {
        EnumValue[] value();
    }
}
import .slf4j.Slf4j;

import ;
import ;
import ;
import ;
import ;
import ;

/**
 * 枚举值校验器
 *
 */
@Slf4j
public class EnumValueValidator implements ConstraintValidator<EnumValue, Object> {

    private String[] strValues;

    private int[] intValues;

    private List<Object> objValues = new ArrayList<>();

    @Override
    public void initialize(EnumValue constraintAnnotation) {
        strValues = ();
        intValues = ();
        Class<?> enumClass = ();
        if (!(enumClass) && (enumClass)) {
            try {
                Method method = ("getId");
                Object[] enumConstants = ();
                for (Object constant : enumConstants) {
                    ((constant));
                }
            } catch (Exception e) {
                ("使用自定义枚举类型校验的时候枚举必须用id来进行范围校验", e);
            }

        }
    }

    @Override
    public boolean isValid(Object value, ConstraintValidatorContext constraintValidatorContext) {
        if ((value)) {
            return true;
        }
        if (() > 0) {
            if ((value)) {
                return true;
            }
        } else if (value instanceof String) {
            for (String s : strValues) {
                if ((value)) {
                    return true;
                }
            }
        } else if (value instanceof Integer) {
            for (Integer s : intValues) {
                if (s == value) {
                    return true;
                }
            }
        }
        return false;
    }
}