Spring Validation数据校验详解
作者:weixin_44012070
Spring Validation数据校验
Spring Validation是SpringFramework提供的一种轻量级的数据验证框架,用于Java对象进行校验。
Spring Validation(Spring的数据验证组件)其实是一个抽象层,它为数据验证提供了统一的接口和基本的校验功能。
Spring Validation默认使用了Hibernate Validator作为其具体的实现,但是也可以通过适配器与其他数据验证框架(如Apache Commons Validator)一起工作。
Spring Validation的主要功能包括:
1、提供了一套注解,用于对Java对象进行校验;
2、支持嵌套校验,用于对一个对象中的属性进行递归校验;
3、支持分组校验,用于根据不同的校验场景,使用不同的校验规则;
4、支持国际化,可以根据不同的语言环境,使用不同的校验提示消息。
5、支持自定义注解和校验器,满足各种复杂的校验需求。
Spring提供的数据校验方式:
- 实现org.springframework.validation.Validator接口,调用接口实现类;
- 通过 注解 方式进行数据校验(按照Bean Validation方式);
- 基于 方法(函数) 实现数据校验;
- 自定义校验
依赖引入:
如果springboot版本小于2.3.x,spring-boot-web-starter会自动引入hibernate-validator。
如果spring-boot版本为2.3.x,则需要手动引入依赖,如:
<dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-validator</artifactId> <version>6.0.1.Final</version> </dependency>
1、Validator接口方式
@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()); } }
2、基于注解方式(Bean Validation)
使用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<ConstraintViolation<User>> 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> { /** * 合格方法是给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<>(); 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<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 = 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 { } }
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 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<Class<?>, 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<ConstraintViolation<Object>> 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; } ...... }
4、自定义校验
自定义注解,编写校验器实现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<EnumValue, Object> { private String[] strValues; private int[] intValues; private List<Object> objValues = new ArrayList<>(); @Override public void initialize(EnumValue constraintAnnotation) { strValues = constraintAnnotation.strValues(); intValues = constraintAnnotation.intValues(); Class<?> enumClass = constraintAnnotation.enumClass(); if (!Objects.isNull(enumClass) && Enum.class.isAssignableFrom(enumClass)) { try { Method method = enumClass.getMethod("getId"); Object[] enumConstants = enumClass.getEnumConstants(); for (Object constant : enumConstants) { objValues.add(method.invoke(constant)); } } catch (Exception e) { log.error("使用自定义枚举类型校验的时候枚举必须用id来进行范围校验", e); } } } @Override public boolean isValid(Object value, ConstraintValidatorContext constraintValidatorContext) { if (Objects.isNull(value)) { return true; } if (objValues.size() > 0) { if (objValues.contains(value)) { return true; } } else if (value instanceof String) { for (String s : strValues) { if (s.equals(value)) { return true; } } } else if (value instanceof Integer) { for (Integer s : intValues) { if (s == value) { return true; } } } return false; } }
总结
以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。