Bean Validation 现名为 Jakarta Bean Validation,官网为: https://beanvalidation.org/ 。
Jakarta Bean Validation 是一套 Java EE 规范,提供了以下功能:
- 通过注解表达对对象模型的约束
- 可自定义约束
- 提供 API 来校验对象和对象图(Object Graph)
- 提供 API 来校验方法和构造器的参数和返回值
- 报告一组违规(即校验不通过的信息,经过本地化)
- 在 Java SE 上运行,并集成在 Jakarta EE 9 和 10 中
- lets you express constraints on object models via annotations
- lets you write custom constraints in an extensible way
- provides the APIs to validate objects and object graphs
- provides the APIs to validate parameters and return values of methods and constructors
- reports the set of violations (localized)
- runs on Java SE and is integrated in Jakarta EE 9 and 10
Jakarta Bean Validation 版本历史:
- Bean Validation 1.0 (JSR 303) 是 Java 对象验证标准的第一个版本,发布于 2009 年,是 Java EE 6 的一部分,可以与 Java SE 一同使用。
- Bean Validation 1.1 (JSR 349) 于 2013 年完成,是 Java EE 7 的一部分,可以与 Java SE 一同使用。
- Bean Validation 2.0 (JSR 380) 于 2017 年 8 月完成,是 Java EE 8 的一部分,可以与 Java SE 一同使用。
- Jakarta Bean Validation 2.0 于 2019 年 8 月发布。它是 Jakarta EE 8 的一部分,可以与 Java SE 一同使用。Jakarta Bean Validation 2.0 与 Bean Validation 2.0 除了 GAV(GroupId 、ArtifactId 、Version) 以外没有任何不同,现在的 GAV 是:jakarta.validation:jakarta.validation-api。
Hibernate Validator 是对 Bean Validation 的实现,并进行了扩展。其官网为:https://hibernate.org/validator/ 。
官方文档地址为:https://hibernate.org/validator/documentation/ 。
2.1 引入依赖2.2 Validatorjakarta.validation jakarta.validation-api 2.0.2 org.hibernate hibernate-validator 6.2.0.Final org.apache.tomcat.embed tomcat-embed-el 9.0.52
校验需要 Validator(类) 与 Constraint(注解) 配合才能实现。Constraint 以注解的形式标注在需要被校验的结构,表示此结构需满足的条件,不同的 Constraint 对应的不同的 Validator(一般命名规则为:注解xx对应xxValidator),Validator 进行真实的校验工作。内置的 Constraint 与 Validator 是由 ConstraintHelper 确定处理的。
package com.xumenghao.model; import lombok.Data; import javax.validation.constraints.NotNull; @Data public class User { @NotNull private String name; }
package com.xumenghao.util; import com.xumenghao.model.User; import javax.validation.ConstraintViolation; import javax.validation.Validation; import javax.validation.Validator; import java.util.List; import java.util.Set; import java.util.stream.Collectors; public class ValidatorUtil { // 此接口线程安全 private static Validator validator; static { // 获取 ValidatorFactory ,再通过其获得 Validator validator = Validation.buildDefaultValidatorFactory().getValidator(); } public static Listvalid(User user){ // 寻找对应的 Validator 对其属性进行校验,若是通过,返回的 Set 为空 Set > validateInfo = validator.validate(user); return validateInfo.stream().map(v -> "属性:" + v.getPropertyPath() + ",属性值:" + v.getInvalidValue() + ",提示信息:" + v.getMessage()).collect(Collectors.toList()); } }
使用 Validator 时,只是声明了接口,具体的实现(Hibernate Validator)是通过 SPI 机制找到的。
package com.xumenghao; import com.xumenghao.model.User; import com.xumenghao.util.ValidatorUtil; import java.util.List; public class App { public static void main(String[] args) { User user = new User(); List2.3 Constraintvalid = ValidatorUtil.valid(user); System.out.println(valid); } } // 输出: // [属性:name,属性值:null,提示信息:不能为null]
注意:约束是可以重复的,但不能矛盾。
2.3.1 Bean Validation Constraints 非空校验约束 | 作用 | 支持数据类型 |
---|---|---|
@NotNull | 标注的结构必须不为 null | 任何类型 |
@Null | 标注的结构必须为 null | 任何类型 |
约束 | 作用 |
---|---|
@DecimalMax(value=, inclusive=) | 如果 inclusive 为 false,标注的结构必须小于 value;如果 inclusive 为 true,标注的结构必须小于等于 value。 |
@DecimalMin(value=, inclusive=) | 如果 inclusive 为 false,标注的结构必须大于 value;如果 inclusive 为 true,标注的结构必须大于等于 value。 |
@Digits(integer=, fraction=) | 标注的结构必须有整数部分位数上限为integer、小数部分位数上限为fraction。 |
上述3个注解支持的数据类型皆为:
- BigDecimal
- BigInteger
- CharSequence
- byte、short、int、long 及对应的包装类
- any sub-type of Number and javax.money.MonetaryAmount (if the JSR 354 API and an implementation is on the class path)
约束 | 作用 |
---|---|
@Max(value=) | 标注的结构必须小于等于value |
@Min(value=) | 标注的结构必须大于等于value |
@Negative | 标注的结构必须小于0 |
@NegativeOrZero | 标注的结构必须小于等于0 |
@Positive | 标注的结构必须大于0 |
@PositiveOrZero | 标注的结构必须大于等于0 |
上述6个注解支持的数据类型皆为:
- BigDecimal
- BigInteger
- byte、short、int、long 及对应的包装类
- any sub-type of CharSequence (the numeric value represented by the character sequence
is evaluated) - any sub-type of Number and javax.money.MonetaryAmount
约束 | 作用 | 支持数据类型 |
---|---|---|
@AssertFalse | 标注的结构必须为 false | Boolean, boolean |
@AssertTrue | 标注的结构必须为 true | Boolean, boolean |
约束 | 作用 | 支持数据类型 |
---|---|---|
@NotBlank | 标注的字符序列必须非 null 且长度大于 0(经过trim) | CharSequence |
@NotEmpty | 标注的结构必须非null且非长度大于0(不经过trim) | CharSequence、Collection、Map、arrays |
@Size(min=, max=) | 标注的结构的长度(元素个数)必须介于[min,max] | CharSequence、Collection、Map、arrays |
约束 | 作用 |
---|---|
@Future | 标注的日期必须是未来 |
@FutureOrPresent | 标注的日期必须是未来或现在 |
@Past | 标注的日期必须是过去 |
@PastOrPresent | 标注的日期必须是过去或现在 |
上述6个注解支持的数据类型皆为:
- java.util.Date
- java.util.Calendar
- java.time.Instant
- java.time.LocalDate
- java.time.LocalDateTime
- java.time.LocalTime
- java.time.MonthDay
- java.time.OffsetDateTime
- java.time.OffsetTime
- java.time.Year
- java.time.YearMonth, java.time.ZonedDateTime
- java.time.chrono.HijrahDate
- java.time.chrono.JapaneseDate
- java.time.chrono.MinguoDate
- java.time.chrono.ThaiBuddhistDate;
- additionally supported by HV, if the Joda Time date/time API is on the classpath: any implementations of ReadablePartial and ReadableInstant
约束 | 作用 | 支持数据类型 |
---|---|---|
标注的字符序列必须为有效的电子邮箱地址。可选参数 regexp、flags可以指定电子邮件必须匹配的附加正则表达式(包括正则表达式标志) | CharSequence | |
@Pattern(regex=, flags=) | 检查标注的字符串是否与给定 flag 匹配的正则表达式regex 匹配 | CharSequence |
常用的扩展约束:
约束 | 作用 | 支持数据类型 |
---|---|---|
@Length(min=, max=) | 标注的字符序列长度在[min,max]之间 | CharSequence |
@Range(min=, max=) | 标注的结构的值在[min,max]之间 | BigDecimal,BigInteger,CharSequence,byte、short、 int、 long 及其包装类 |
@URL(protocol=, host=, port=, regexp=, flags=) | 检查标注的字符序列是否是一个有效的URL(根据RFC2396),如果protocol、host、port有指定值,则相应URL片段需要完全匹配。 | CharSequence |
@Valid 是 Bean Validation 提供的注解,可用于方法、属性、构造器、参数、任何使用类型的语句。
作用:
- 在属性、方法参数或方法返回值使用此注解进行级联校验。
- 当属性、方法参数或方法返回值被校验的时候,被 Constraint 标记的对象及其属性也会被校验。
- 此行为是递归的。
Marks a property, method parameter or method return type for validation cascading.
Constraints defined on the object and its properties are be validated when the property, method parameter or method return type is validated.
This behavior is applied recursively.
当一个属性、方法参数或者返回值为一个 Bean ,此 Bean 的属性也有约束,希望对此 Bean 的属性进行校验,则需要 @Valid 对属性、方法参数或者返回值进行标注。
@Data public class User { @NotBlank private String name; @Valid @NotNull private Car car; }
@Data public class Car { @NotBlank private String brand; @NotBlank private String type; @Min(10000) private Double price; }
public class App { public static void main(String[] args) { User user = new User(); user.setName("Jack"); user.setCar(new Car()); List2.4 消息模板valid = ValidatorUtil.valid(user); System.out.println(valid); } } // 对于上述测试 // 未使用 @Valid ,输出:[] // 使用了 @Valid ,输出:[属性:car.brand,属性值:null,提示信息:不能为空, 属性:car.type,属性值:null,提示信息:不能为空]
可以通过约束的 message方法参数指定此校验不通过时输出的信息,还可以使用 EL 表达式。
@Data public class Car { @NotBlank private String brand; @NotBlank private String type; @Min(value = 10000, message = "价格要高于 ${value} !") private Double price; }2.5 分组校验
可通过约束的 groups方法参数进行分组,约束只有在指定的组下才生效,不指定时默认属于 javax.validation.groups.Default组。
@Data public class User { // 接口作为分组标识 public interface Add{}; public interface Update{}; @Null(groups = {Add.class}) @NotNull(groups = {Update.class}) private Long id; @NotBlank private String name; @Valid @NotNull private Car car; @InSet({"男","女"}) private String gender; }
public static Listvalid(User user, Class> group){ Set > validateInfo = validator.validate(user, group, Default.class); return validateInfo.stream().map(v -> "属性:" + v.getPropertyPath() + ",属性值:" + v.getInvalidValue() + ",提示信息:" + v.getMessage()).collect(Collectors.toList()); }
public class App { public static void main(String[] args) { User user = new User(); user.setId(123L); user.setName("Jack"); Car car = new Car(); car.setBrand("BMW"); car.setType("SUV"); car.setPrice(20000000.0); user.setCar(car); user.setGender("男"); List2.6 自定义约束valid = ValidatorUtil.valid(user, User.Add.class); System.out.println(valid); } } // 当分组为 Add.class 时,输出:[属性:id,属性值:123,提示信息:必须为null] // 当分组为 Update.class 时,输出:[]
@Documented @Constraint(validatedBy = {InSetValidator.class}) // 指明处理此约束的Validator @Target({ElementType.FIELD}) @Retention(RetentionPolicy.RUNTIME) public @interface InSet { String message() default "当前值不在允许范围内"; Class>[] groups() default { }; Class extends Payload>[] payload() default { }; String[] value(); }
package com.xumenghao.validator; import com.xumenghao.constraint.InSet; import javax.validation.ConstraintValidator; import javax.validation.ConstraintValidatorContext; public class InSetValidator implements ConstraintValidator{ private String[] strings; @Override public void initialize(InSet constraintAnnotation) { // 获取注解的 value strings = constraintAnnotation.value(); } @Override public boolean isValid(String s, ConstraintValidatorContext constraintValidatorContext) { // String s 为被注解标注的属性 // 如果被标注的值为空,则不校验,直接返回 if (s == null){ return true; } for(String str : strings){ if (str.equals(s)){ return true; } } return false; } }
@Data public class User { @NotBlank private String name; @Valid @NotNull private Car car; @InSet({"男","女"}) private String gender; }2.8 快速失败模式
快速失败(fail fast)模式:第一次校验不通过就不再校验后面的。
// 设置快速失败模式 fastValidator = Validation.byProvider(HibernateValidator.class) .configure().failFast(true) .buildValidatorFactory().getValidator();2.9 非 Bean 入参校验
情景:上述都是对自定义的 Bean 进行校验。如果方法参数是 Bean,因为 Bean 中的属性已被约束标注,可以通过直接将 Bean (实例对象)传入 ValidatorUtil 的 valid 方法进行校验。但如果方法参数并不是 Bean,是String、Interger 等类型,该如何校验?
方式一:方法参数前使用约束注解,再通过反射获取相关信息,传给校验方法。
public class ValidatorUtil { private final static Validator VALIDATOR; private final static ExecutableValidator EXECUTABLE_VALIDATOR; static { VALIDATOR = Validation.buildDefaultValidatorFactory().getValidator(); EXECUTABLE_VALIDATOR = VALIDATOR.forExecutables(); } public staticList valid(T object, Method method, Object[] parameterValues, Class>... groups){ Set > validateInfo = EXECUTABLE_VALIDATOR.validateParameters(object, method, parameterValues, groups); return validateInfo.stream().map(v -> "属性:" + v.getPropertyPath() + ",属性值:" + v.getInvalidValue() + ",提示信息:" + v.getMessage()).collect(Collectors.toList()); } }
public class UserService { public ListgetByName(@NotNull String name){ StackTraceElement st = Thread.currentThread().getStackTrace()[1]; String methodName = st.getMethodName(); Method method = null; try { method = this.getClass().getDeclaredMethod(methodName, String.class); } catch (NoSuchMethodException e) { e.printStackTrace(); } return ValidatorUtil.valid(this, method, new Object[]{name}); } }
方式二:AOP
可以看出,上述方式十分复杂,还不如传统使用 if-else 的校验方法。
可以使用 AOP 编程思想进行非 Bean 入参校验,具体见
Spring Validation 从两个方向提供了校验功能:
- 提供接口 org.springframework.validation.Validator
- 支持 Bean Validation(使用 Hibernate Validator)
官方文档:https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#validation
3.1 使用 Spring Validator 进行校验Spring 提供了 org.springframework.validation.Validator 接口,可以通过实现此接口对特定的 Bean 进行校验。
Spring 还提供了ValidationUtils 工具类,里面提供了通用的校验方法。
org.springframework spring-context 5.3.20
@Data public class User { private Long id; private String name; }
package com.xumenghao.validator; import com.xumenghao.model.User; import org.springframework.validation.Errors; import org.springframework.validation.ValidationUtils; import org.springframework.validation.Validator; public class UserValidator implements Validator { @Override public boolean supports(Class> clazz) { return User.class.equals(clazz); } @Override public void validate(Object target, Errors errors) { ValidationUtils.rejectIfEmpty(errors, "name", "name empty"); User user = (User) target; if(user.getId() < 0){ errors.reject("id","negative value"); } } }
public class App { public static void main(String[] args) { // 准备 User 对象 User user = new User(); user.setName(""); user.setId(-10L); // 通过 DataBinder 将 User 对象与 UserValidator 绑定到一起 DataBinder dataBinder = new DataBinder(user); dataBinder.setValidator(new UserValidator()); // 通过 DataBinder 调用 UserValidator 对 User 进行校验 dataBinder.validate(); // 获得校验结果并输出 BindingResult bindingResult = dataBinder.getBindingResult(); List3.2 使用 Bean Validation (Hibernate Validator)进行校验allErrors = bindingResult.getAllErrors(); System.out.println(allErrors); } }
3.2.1 校验 Beanorg.springframework spring-context 5.3.20 org.hibernate hibernate-validator 6.2.0.Final
使用 Bean Validation 需要 javax.validation.ValidatorFactory和 javax.validation.Validator,Spring默认有一个实现类 LocalValidatorFactoryBean 实现了上述接口,并且也实现了org.springframework.validation.Validator 接口。可以将 LocalValidatorFactoryBean 注入 IOC。
@Configuration public class AppConfig { @Bean public LocalValidatorFactoryBean localValidatorFactoryBean(){ return new LocalValidatorFactoryBean(); } }使用 javax.validation.Validator
与前文 2.2 使用方式完全相同,只不过因为 Spring 的存在,可以不用手动创建。
import javax.validation.Validator; @Service public class MyService { @Autowired private Validator validator; public boolean validator(Person person){ Set使用 org.springframework.validation.Validator> sets = validator.validate(person); return sets.isEmpty(); } }
import org.springframework.validation.Validator; @Service public class MyService { @Autowired private Validator validator; public boolean validaPersonByValidator(Person person) { BindException bindException = new BindException(person, person.getName()); validator.validate(person, bindException); return bindException.hasErrors(); } }3.2.3 校验方法参数
使用 MethodValidationPostProcessor 与 @Validated 注解配合进行方法参数的校验。
通过注入 MethodValidationPostProcessor将 Bean Validation 支持的方法验证功能整合到 Spring 中。
import org.springframework.validation.beanvalidation.MethodValidationPostProcessor; @Configuration @ComponentScan(basePackages = {"com.xumenghao"}) public class AppConfig { @Bean public MethodValidationPostProcessor validationPostProcessor() { return new MethodValidationPostProcessor(); } }
为了使得 Spring 驱动的方法验证生效,需要在目标类上使用@Validated注解,目标类中的方法参数如果有 Bean Validation 的注解并标注,则会自动被校验,如果校验不通过,则会抛出 ConstraintViolationException。
@Data public class User { public interface Add{}; public interface Update{}; @NotNull(groups = Update.class) @Null(groups = Add.class) private Long id; @NotBlank private String name; }
@Service @Validated public class UserService { public void printUser(@NotNull @Valid User user){ System.out.println(user); } }
public class App { public static void main(String[] args) { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class); UserService userService = context.getBean(UserService.class); User user = new User(); userService.printUser(user); } }3.2.4 @Validated
@Validated 注解是由 Spring 提供的,除了上述用于方法参数校验外,其功能与 Bean Validation 的 @Valid 相同:级联验证,但是 @Validated支持分组校验。实例见 3.2.5。
3.2.5 Spring MVC Validation默认情况下,如果 Bean Validation 在 classPath 上存在(例如 Hibernate Validator),则将 LocalValidatorFactoryBean 注册为用于在 Controller 方法参数上 @Valid、 @Validated的全局校验器。
对于 Spring MVC 下的 Controller 中的方法参数:
- 如果方法入参为 Bean,参数前使用了 @Valid或@Validated 注解,则会自动对 Bean 进行校验(其余层不支持),即使用 LocalValidatorFactoryBean,无需在类上标注 @Validated;如果校验不通过会抛出 BindException。
- 方法入参无论是 Bean 还是非 Bean,如果使用约束注解,比如 @Null、@NotNull,则是进行参数校验,即使用 ConstraintViolationException,需要在类上标注 @Validated;如果校验不通过会抛出 ConstraintViolationException。
@RestController @Validated public class UserController { @Autowired UserService userService; @GetMapping("/getByName") public String getByName(@NotBlank String name){ return name + ": ok"; } @GetMapping("/addUser") public String addUser(@Validated({User.Add.class, Default.class}) User user){ userService.printUser(user); return "成功!"; } }3.3 整合 SpringBoot
可以引入org.springframework.boot:spring-boot-starter-validation 会将
org.hibernate.validator:hibernate-validator、org.springframework.boot:spring-boot-starter引入并进行自动配置,如果已存在
org.springframework.boot:spring-boot-starter-web,可以只引入 org.hibernate.validator:hibernate-validator(有些版本只需要引入 stater-web 即可)。
@Configuration( proxyBeanMethods = false ) @ConditionalOnClass({ExecutableValidator.class}) @ConditionalOnResource( resources = {"classpath:META-INF/services/javax.validation.spi.ValidationProvider"} ) @Import({PrimaryDefaultValidatorPostProcessor.class}) public class ValidationAutoConfiguration { public ValidationAutoConfiguration() { } @Bean @Role(2) @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, ObjectProvider3.4 设置快速失败模式excludeFilters) { FilteredMethodValidationPostProcessor processor = new FilteredMethodValidationPostProcessor(excludeFilters.orderedStream()); boolean proxyTargetClass = (Boolean)environment.getProperty("spring.aop.proxy-target-class", Boolean.class, true); processor.setProxyTargetClass(proxyTargetClass); processor.setValidator(validator); return processor; } }
@Configuration(proxyBeanMethods = false) public class AppConfig { @Bean public LocalValidatorFactoryBean localValidatorFactoryBean(){ LocalValidatorFactoryBean factoryBean = new LocalValidatorFactoryBean(); MessageInterpolatorFactory interpolatorFactory = new MessageInterpolatorFactory(); factoryBean.setMessageInterpolator(interpolatorFactory.getObject()); factoryBean.getValidationPropertyMap() .put(BaseHibernateValidatorConfiguration.FAIL_FAST, Boolean.TRUE.toString()); return factoryBean; } }3.5 统一异常管理
结合统一异常管理处理校验未通过时的异常,使得代码更加整洁。
未完待续…
笔者才疏学浅,若有纰漏,欢迎指证!