Bean Validation 现名为 Jakarta Bean Validation,官网为: https://beanvalidation.org/ 。
Jakarta Bean Validation 是一套 Java EE 规范,提供了以下功能:
- 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 版本历史:
jakarta.validation:jakarta.validation-api
。Hibernate Validator 是对 Bean Validation 的实现,并进行了扩展。其官网为:https://hibernate.org/validator/ 。
官方文档地址为:https://hibernate.org/validator/documentation/ 。
<!-- Jakarta Bean Validation-->
<dependency>
<groupId>jakarta.validation</groupId>
<artifactId>jakarta.validation-api</artifactId>
<version>2.0.2</version>
</dependency>
<!-- Hibernate Validator,引入此依赖后,无需引入 Bean Validation -->
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>6.2.0.Final</version>
</dependency>
<!-- el解释器,用于处理消息模板中的 el 表达式 -->
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-el</artifactId>
<version>9.0.52</version>
</dependency>
校验需要 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 List<String> valid(User user){
// 寻找对应的 Validator 对其属性进行校验,若是通过,返回的 Set 为空
Set<ConstraintViolation<User>> 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();
List<String> valid = ValidatorUtil.valid(user);
System.out.println(valid);
}
}
// 输出:
// [属性:name,属性值:null,提示信息:不能为null]
注意:约束是可以重复的,但不能矛盾。
约束 | 作用 | 支持数据类型 |
---|---|---|
@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个注解支持的数据类型皆为:
约束 | 作用 |
---|---|
@Max(value=) | 标注的结构必须小于等于value |
@Min(value=) | 标注的结构必须大于等于value |
@Negative | 标注的结构必须小于0 |
@NegativeOrZero | 标注的结构必须小于等于0 |
@Positive | 标注的结构必须大于0 |
@PositiveOrZero | 标注的结构必须大于等于0 |
上述6个注解支持的数据类型皆为:
约束 | 作用 | 支持数据类型 |
---|---|---|
@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个注解支持的数据类型皆为:
约束 | 作用 | 支持数据类型 |
---|---|---|
@Email | 标注的字符序列必须为有效的电子邮箱地址。可选参数 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 提供的注解,可用于方法、属性、构造器、参数、任何使用类型的语句。
作用:
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());
List<String> 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;
}
可通过约束的 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 List<String> valid(User user, Class<?> group){
Set<ConstraintViolation<User>> 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("男");
List<String> 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<InSet, String> {
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;
}
快速失败(fail fast)模式:第一次校验不通过就不再校验后面的。
// 设置快速失败模式
fastValidator = Validation.byProvider(HibernateValidator.class)
.configure().failFast(true)
.buildValidatorFactory().getValidator();
情景:上述都是对自定义的 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 static <T> List<String> valid(T object, Method method, Object[] parameterValues, Class<?>... groups){
Set<ConstraintViolation<T>> 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 List<String> getByName(@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
官方文档:https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#validation
Spring 提供了 org.springframework.validation.Validator
接口,可以通过实现此接口对特定的 Bean 进行校验。
Spring 还提供了ValidationUtils
工具类,里面提供了通用的校验方法。
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.3.20</version>
</dependency>
@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 {
/**
* 用来检查被校验的对象是否支持,此 Validator 只支持 User
* @param clazz 对象的类实例
* @return true 表示支持,反之不支持
*/
@Override
public boolean supports(Class<?> clazz) {
return User.class.equals(clazz);
}
/**
* 用来校验的方法
* @param target 被校验的对象
* @param errors 用来封装校验的错误,如果通过校验,则空
*/
@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();
List<ObjectError> allErrors = bindingResult.getAllErrors();
System.out.println(allErrors);
}
}
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.3.20</version>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>6.2.0.Final</version>
</dependency>
使用 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<ConstraintViolation<Person>> sets = validator.validate(person);
return sets.isEmpty();
}
}
org.springframework.validation.Validator
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();
}
}
使用 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);
}
}
@Validated
注解是由 Spring 提供的,除了上述用于方法参数校验外,其功能与 Bean Validation 的 @Valid
相同:级联验证,但是 @Validated
支持分组校验。实例见 3.2.5。
默认情况下,如果 Bean Validation 在 classPath 上存在(例如 Hibernate Validator),则将 LocalValidatorFactoryBean
注册为用于在 Controller 方法参数上 @Valid
、 @Validated
的全局校验器。
对于 Spring MVC 下的 Controller 中的方法参数:
@Valid
或@Validated
注解,则会自动对 Bean 进行校验(其余层不支持),即使用 LocalValidatorFactoryBean
,无需在类上标注 @Validated
;如果校验不通过会抛出 BindException
。@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 "成功!";
}
}
可以引入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, ObjectProvider<MethodValidationExcludeFilter> 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;
}
}
结合统一异常管理处理校验未通过时的异常,使得代码更加整洁。
未完待续…
笔者才疏学浅,若有纰漏,欢迎指证!