Spring Validation是SpringFramework提供的一种轻量级的数据验证框架,用于Java对象进行校验。Spring Validation(Spring的数据验证组件)其实是一个抽象层,它为数据验证提供了统一的接口和基本的校验功能。Spring Validation默认使用了Hibernate Validator作为其具体的实现,但是也可以通过适配器与其他数据验证框架(如Apache Commons Validator)一起工作。
Spring Validation的主要功能包括:
1、提供了一套注解,用于对Java对象进行校验;
2、支持嵌套校验,用于对一个对象中的属性进行递归校验;
3、支持分组校验,用于根据不同的校验场景,使用不同的校验规则;
4、支持国际化,可以根据不同的语言环境,使用不同的校验提示消息。
5、支持自定义注解和校验器,满足各种复杂的校验需求。
Spring 提供的数据校验方式:
依赖引入:
如果springboot版本小于2.3.x,spring-boot-web-starter会自动引入hibernate-validator。如果spring-boot版本为2.3.x,则需要手动引入依赖,如:
org.hibernate
hibernate-validator
6.0.1.Final
@Data
public class Person {
private String name;
private int age;
}
import org.springframework.validation.Errors;
import org.springframework.validation.ValidationUtils;
import org.springframework.validation.Validator;
/**
* @description 实现接口{@link org.springframework.validation.Validator}
*/
public class PersonValidator implements Validator {
@Override
public boolean supports(Class> clazz) {
return Person.class.equals(clazz);
}
@Override
public void validate(Object obj, Errors errors) {
//设置name为空时,报错:name.empty
ValidationUtils.rejectIfEmpty(errors, "name", "name.empty");
//传入对象,强转为实体类Person对象
Person p = (Person) obj;
if (p.getAge() < 0) { //设置age属性小于零时报错
errors.rejectValue("age", "error (age < 0)");
} else if (p.getAge() > 110) {//设置age属性大于110报错
errors.rejectValue("age", "error (age > 110) too old !!");
}
}
}
/**
* 测试
*
*/
@Slf4j
class IomsApplicationTests {
public static void main(String[] args) {
Person person = new Person();
// person.setName("高启强");
// person.setAge(29);
//创建person对象的DataBinder
DataBinder binder = new DataBinder(person);
//设置校验
binder.setValidator(new PersonValidator());
//校验(当person属性值为空时,校验不通过)
binder.validate();
//输出校验结果
BindingResult bindingResult = binder.getBindingResult();
System.out.println(bindingResult.getAllErrors());
}
}
使用Bean Validation校验方式,需要将Bean Validation需要的javax.validation.ValidatorFactory和javax.validation.Validator注入到容器中。Spring默认有一个实现类LocalValidatorFactoryBean,它实现了Bean Validator中的接口和org.springframework.validation.Validator接口。
在springboot2.2.2中已自动注入,源码如下:
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(ExecutableValidator.class)
@ConditionalOnResource(resources = "classpath:META-INF/services/javax.validation.spi.ValidationProvider")
@Import(PrimaryDefaultValidatorPostProcessor.class)
public class ValidationAutoConfiguration {
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
@ConditionalOnMissingBean(Validator.class)
public static LocalValidatorFactoryBean defaultValidator() {
LocalValidatorFactoryBean factoryBean = new LocalValidatorFactoryBean();
MessageInterpolatorFactory interpolatorFactory = new MessageInterpolatorFactory();
factoryBean.setMessageInterpolator(interpolatorFactory.getObject());
return factoryBean;
}
/**
* 基于方法的校验方式
*/
@Bean
@ConditionalOnMissingBean
public static MethodValidationPostProcessor methodValidationPostProcessor(Environment environment,
@Lazy Validator validator) {
MethodValidationPostProcessor processor = new MethodValidationPostProcessor();
boolean proxyTargetClass = environment.getProperty("spring.aop.proxy-target-class", Boolean.class, true);
processor.setProxyTargetClass(proxyTargetClass);
processor.setValidator(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 = “{user.name.notnull}”):使用国际化消息提示。
@NotBlank(message = “{user.name.notblank}”):使用国际化消息提示。
@NotEmpty(message = “{user.name.notempty}”):使用国际化消息提示。
@Email(message = “{user.email.format}”):使用国际化消息提示。
@Valid:用于嵌套校验,可以对一个对象中的属性进行递归校验。
@ConvertGroup:用于分组校验,可以指定校验的分组,根据不同的分组执行不同的校验规则。
@GroupSequence:用于定义校验分组的顺序,指定不同分组的执行顺序。
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotNull;
@Data
public class User {
@NotEmpty //不可为空
private String name;
@Min(0) //最小值
@Max(110) //最大值
private int age;
}
使用java原生的jakarta.validation.Validator校验器
import jakarta.validation.ConstraintViolation;
import jakarta.validation.Validator;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.Set;
/**
* 使用java原生的jakarta.validation.Validator校验
*
*/
@Service
public class JavaService {
@Autowired //自动装配Validator对象
private Validator validator;
//校验方法
public boolean validator(User user){
//校验后的结果存放进Set集合
Set> set = validator.validate(user);
//若没有校验到错误,集合为空,返回true。
return set.isEmpty();
}
}
使用spring提供的 org.springframework.validation.Validator校验器
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.validation.BindException;
import org.springframework.validation.Validator;
/**
* 使用spring提供的validate校验方法
*/
@Service
public class SpringService {
@Autowired
private Validator validator;
public boolean validator2(User user){
BindException bindException = new BindException(user,user.getName());
validator.validate(user,bindException); //调用校验方法进行校验
System.out.println(bindException.getAllErrors()); //输出所有错误信息
return bindException.hasErrors(); //若没有异常,返回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 = RegexConstants.ID)
private String lineId;
@Pattern(message = "站点id只能为1-20位数字", regexp = RegexConstants.ID)
private String stationId;
@Pattern(message = "子系统id只能为1-20位数字", regexp = RegexConstants.ID)
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 BaseResp.getSuccessResult();
}
②、使用@RequestParam、@PathVariable传递参数,必须在Controller类上标注@Validated注解,
校验失败会抛出ConstraintViolationException异常。
/**
* 类上标注@Validated注解
*/
@RestController
@RequestMapping("${zte.usp.app-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 IomsBaseResp.getSuccessResult(responseEntity.getData());
}
}
二、分组校验:
@Data
@EqualsAndHashCode(callSuper = true)
@Accessors(chain = true)
@FieldNameConstants
public class Manufacturer extends Entity {
@NotBlank(message = "厂商id不能为空", groups = UpdateGroup.class)
private String id;
private String sn;
private String name;
private Integer dockingGatewayId;
public interface UpdateGroup {
}
}
@PostMapping("update")
public BaseResp updateManufacturer(@Validated({Manufacturer.UpdateGroup.class, Default.class}) @RequestBody Manufacturer manufacturer) {
return manufacturerService.updateManufacturer(manufacturer);
}
三、嵌套校验:
/**
* Job属性 嵌套校验使用@Valid注解
*/
@Data
public class UserDTO {
@Min(value = 10000000000000000L, groups = Update.class)
private Long userId;
@NotNull(groups = {Save.class, Update.class})
@Length(min = 2, max = 10, groups = {Save.class, Update.class})
private String userName;
@NotNull(groups = {Save.class, Update.class})
@Valid
private Job job;
@Data
public static class Job {
@Min(value = 1, groups = Update.class)
private Long jobId;
@NotNull(groups = {Save.class, Update.class})
@Length(min = 2, max = 10, groups = {Save.class, Update.class})
private String jobName;
@NotNull(groups = {Save.class, Update.class})
@Length(min = 2, max = 10, groups = {Save.class, Update.class})
private String position;
}
public interface Save {
}
public interface Update {
}
}
四、多字段联合校验:
Hibernate Validator提供了非标准的@GroupSequenceProvider注解。根据当前对象实例的状态,动态来决定加载哪些校验组进入默认校验组。为了实现多字段联合校验,需要借助Hibernate Validator提供的DefaultGroupSequenceProvider接口。
/**
* 该接口定义了:动态Group序列的协定
* 要想它生效,需要在T上标注@GroupSequenceProvider注解并且指定此类为处理类
* 如果`Default`组对T进行验证,则实际验证的实例将传递给此类以确定默认组序列
*/
public interface DefaultGroupSequenceProvider {
/**
* 合格方法是给T返回默认的组(多个)。因为默认的组是Default
* 入参T object允许在验证值状态的函数中动态组合默认组序列。(非常强大)
* object是待校验的Bean。它可以为null哦~(Validator#validateValue的时候可以为null)
* 返回值表示默认组序列的List。它的效果同@GroupSequence定义组序列,尤其是列表List必须包含类型T
*/
List> getValidationGroups(T object);
}
实现步骤:
1、实现DefaultGroupSequenceProvider接口
public class PersonGroupSequenceProvider implements DefaultGroupSequenceProvider {
@Override
public List> getValidationGroups(Person bean) {
List> defaultGroupSequence = new ArrayList<>();
defaultGroupSequence.add(Person.class); // 这一步不能省,否则Default分组都不会执行了,会抛错的
if (bean != null) { // 这块判空请务必要做
Integer age = bean.getAge();
if (age < 30) {
defaultGroupSequence.add(Person.AgeLt30Group.class);
} else if (age >= 30 && age < 40) {
defaultGroupSequence.add(Person.Age30And40Group.class);
}
}
return defaultGroupSequence;
}
}
2、在待校验的Bean上使用@GroupSequenceProvider注解指定处理器,并定义好校验逻辑(保活分组)
@GroupSequenceProvider(PersonGroupSequenceProvider.class)
@Getter
@Setter
@ToString
public class Person {
@NotNull
private String name;
@NotNull
@Range(min = 10, max = 40)
private Integer age;
@NotNull(groups = {AgeLt30Group.class, Age30And40Group.class})
@Size(min = 1, max = 2, groups = AgeLt30Group.class)
@Size(min = 3, max = 5, groups = Age30And40Group.class)
private List 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 = Default.class)
private String middlename;
@NotEmpty(message = "lastname may be empty", groups = GroupA.class)
private String lastname;
@NotEmpty(message = "country may be empty", groups = GroupB.class)
private String country;
public interface GroupA {
}
public interface GroupB {
}
// 组序列
@GroupSequence({Default.class, GroupA.class, GroupB.class})
public interface Group {
}
}
@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 org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import javax.validation.Valid;
import javax.validation.constraints.NotNull;
@Service
@Validated
public class MethodValidService {
/**
* 校验Service层方法参数
*
* @param user
* @return
*/
public String validParams(@Valid @NotNull User user) {
return user.toString();
}
}
源码解析:
public class MethodValidationPostProcessor extends AbstractBeanFactoryAwareAdvisingPostProcessor
implements InitializingBean {
private Class extends Annotation> validatedAnnotationType = Validated.class;
@Nullable
private Validator validator;
......
/**
* 生成切点AnnotationMatchingPointcut
*/
@Override
public void afterPropertiesSet() {
Pointcut pointcut = new AnnotationMatchingPointcut(this.validatedAnnotationType, true);
this.advisor = new DefaultPointcutAdvisor(pointcut, createMethodValidationAdvice(this.validator));
}
/**
* 生成切面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, Boolean> eligibleBeans = new ConcurrentHashMap<>(256);
public void setBeforeExistingAdvisors(boolean beforeExistingAdvisors) {
this.beforeExistingAdvisors = beforeExistingAdvisors;
}
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) {
return bean;
}
/**
* 通过ProxyFactory返回代理对象
*/
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) {
if (this.advisor == null || bean instanceof AopInfrastructureBean) {
// Ignore AOP infrastructure such as scoped proxies.
return bean;
}
if (bean instanceof Advised) {
Advised advised = (Advised) bean;
if (!advised.isFrozen() && isEligible(AopUtils.getTargetClass(bean))) {
// Add our local Advisor to the existing proxy's Advisor chain...
if (this.beforeExistingAdvisors) {
advised.addAdvisor(0, this.advisor);
}
else {
advised.addAdvisor(this.advisor);
}
return bean;
}
}
if (isEligible(bean, beanName)) {
ProxyFactory proxyFactory = prepareProxyFactory(bean, beanName);
if (!proxyFactory.isProxyTargetClass()) {
evaluateProxyInterfaces(bean.getClass(), proxyFactory);
}
proxyFactory.addAdvisor(this.advisor);
customizeProxyFactory(proxyFactory);
return proxyFactory.getProxy(getProxyClassLoader());
}
// No proxy needed.
return bean;
}
protected boolean isEligible(Object bean, String beanName) {
return isEligible(bean.getClass());
}
protected boolean isEligible(Class> targetClass) {
Boolean eligible = this.eligibleBeans.get(targetClass);
if (eligible != null) {
return eligible;
}
if (this.advisor == null) {
return false;
}
eligible = AopUtils.canApply(this.advisor, targetClass);
this.eligibleBeans.put(targetClass, eligible);
return eligible;
}
protected ProxyFactory prepareProxyFactory(Object bean, String beanName) {
ProxyFactory proxyFactory = new ProxyFactory();
proxyFactory.copyFrom(this);
proxyFactory.setTarget(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 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> 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;
}
......
}
自定义注解,编写校验器实现ConstraintValidator
/**
* 自定义校验规则的注解,并指定校验器
*
*/
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = {EnumValueValidator.class})
public @interface EnumValue {
// 默认错误消息
String message() default "必须为指定值";
// 字符串类型
String[] strValues() default {};
// 整型
int[] intValues() default {};
// 枚举类
Class> enumClass() default Class.class;
// 分组
Class>[] groups() default {};
// 负载
Class extends Payload>[] payload() default {};
// 指定多个时使用
@Target({FIELD, METHOD, PARAMETER, ANNOTATION_TYPE})
@Retention(RUNTIME)
@Documented
@interface List {
EnumValue[] value();
}
}
import lombok.extern.slf4j.Slf4j;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
/**
* 枚举值校验器
*
*/
@Slf4j
public class EnumValueValidator implements ConstraintValidator {
private String[] strValues;
private int[] intValues;
private List