0%

Java Bean Validation Basics

参考来自www.baeldung.com的多篇文章

Java Bean Validation的历史演变

  • 原本以为JSR303就已经是够了,后面才发现这只是initial version,也就是Bean Validation 1.0;
  • 后面又推出了JSR349(Bean Validation 1.1)
  • 而最新版本是JSR380Bean Validation 2.0

依赖包

在不使用第三方的框架提供的校验功能(比如Spring,Spring默认会引入依赖并对标准做了一些具体的实现)的情况下,想要使用Java Bean Validation的功能需要先引入标准API依赖(这里以2.0版本为准):

1
2
3
4
5
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
<version>2.0.0.Final</version>
</dependency>

除了引入标准之外,还要引入标准的实现:

1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
<version>6.0.2.Final</version>
</dependency>
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator-annotation-processor</artifactId>
<version>6.0.2.Final</version>
</dependency>

这里只是引入的hibernate的validator部分,没有引入持久化的那部分。
再引入el表达式的依赖:

1
2
3
4
5
6
7
8
9
10
11
<dependency>
<groupId>javax.el</groupId>
<artifactId>javax.el-api</artifactId>
<version>3.0.0</version>
</dependency>

<dependency>
<groupId>org.glassfish.web</groupId>
<artifactId>javax.el</artifactId>
<version>2.2.6</version>
</dependency>

如果没有引入el表达式的依赖可能会报错:

1
HV000183: Unable to load ‘javax.el.ExpressionFactory'. Check that you have the EL dependencies on the classpath, or use ParameterMessageInterpolator instead

可以通过Validator对Bean进行数据校验

1
2
3
4
5
6
7
8
9
10
11
12
@Test
public void test6() {
FetchWordResultDTO fetchWordResultDTO = new FetchWordResultDTO();
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
Validator validator = factory.getValidator();
Set<ConstraintViolation<FetchWordResultDTO>> results = validator.validate(fetchWordResultDTO);
for (ConstraintViolation<FetchWordResultDTO> result : results) {
System.out.println(result.getPropertyPath());
System.out.println(result.getMessage());
System.out.println(result.getInvalidValue());
}
}

校验注解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
空检查
@Null 验证对象是否为null
@NotNull 验证对象是否不为null, 无法查检长度为0的字符串
@NotBlank 检查约束字符串是不是Null还有被Trim的长度是否大于0,只对字符串,且会去掉前后空格.
@NotEmpty 检查约束元素是否为NULL或者是EMPTY.

Booelan检查
@AssertTrue 验证 Boolean 对象是否为 true
@AssertFalse 验证 Boolean 对象是否为 false

长度检查
@Size(min=, max=) 验证对象(Array,Collection,Map,String)长度是否在给定的范围之内
@Length(min=, max=) Validates that the annotated string is between min and max included.

日期检查
@Past 验证 Date 和 Calendar 对象是否在当前时间之前
@Future 验证 Date 和 Calendar 对象是否在当前时间之后
@Pattern 验证 String 对象是否符合正则表达式的规则
@Past and @PastOrPresent – validate that a date value is in the past or the past including the present; can be applied to date types including those added in Java 8
@Future and @FutureOrPresent – validates that a date value is in the future, or in the future including the present

数值检查,建议使用在Stirng,Integer类型,不建议使用在int类型上,因为表单值为“”时无法转换为int,但可以转换为Stirng为"",Integer为null
@Min 验证 Number 和 String 对象是否大等于指定的值
@Max 验证 Number 和 String 对象是否小等于指定的值
@DecimalMax 被标注的值必须不大于约束中指定的最大值. 这个约束的参数是一个通过BigDecimal定义的最大值的字符串表示.小数存在精度
@DecimalMin 被标注的值必须不小于约束中指定的最小值. 这个约束的参数是一个通过BigDecimal定义的最小值的字符串表示.小数存在精度
@Digits 验证 Number 和 String 的构成是否合法
@Digits(integer=,fraction=) 验证字符串是否是符合指定格式的数字,interger指定整数精度,fraction指定小数精度。
@Positive and @PositiveOrZero – apply to numeric values and validate that they are strictly positive, or positive including 0
@Negative and @NegativeOrZero – apply to numeric values and validate that they are strictly negative, or negative including 0

@Range(min=, max=) 检查数字是否介于min和max之间.
@Range(min=10000,max=50000,message="range.bean.wage")
private BigDecimal wage;

@Valid 递归的对关联对象进行校验, 如果关联对象是个集合或者数组,那么对其中的元素进行递归校验,如果是一个map,则对其中的值部分进行校验.(是否进行递归验证)
@CreditCardNumber信用卡验证
@Email 验证是否是邮件地址,如果为null,不进行验证,算通过验证。
@ScriptAssert(lang= ,script=, alias=)
@URL(protocol=,host=, port=,regexp=, flags=)
// 参考 https://www.cnblogs.com/yangzhilong/p/3724967.html

Validation in Spring Boot

利用@Valid对数据做优雅的校验

校验异常会抛出MethodArgumentNotValidException,然后可以Spring的机制定义全局异常处理器。
Demo of Controller:

1
2
3
4
@PostMapping("/storeFetchWordResult")
public R storeFetchWordResult(@RequestBody @Valid FetchWordResultDTO fetchWordResultDTO) {
// do something
}

全局异常处理器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
@Slf4j
@ControllerAdvice
public class GlobalValidExceptionHandler {

@ResponseBody
@ExceptionHandler({MethodArgumentNotValidException.class})
public R handleValidException(MethodArgumentNotValidException e){
//日志记录错误信息
log.error(Objects.requireNonNull(e.getBindingResult().getFieldError()).getDefaultMessage());
//将错误信息返回给前台
return R.failed(CommonConstants.FAIL, Objects.requireNonNull(e.getBindingResult().getFieldError()).getDefaultMessage());
}

}

// 优化之后的全局异常处理器

@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {


@ExceptionHandler({MethodArgumentNotValidException.class})
@ResponseStatus(HttpStatus.BAD_REQUEST)
public R handleValidException(MethodArgumentNotValidException e){
//日志记录错误信息
final String errorMessage = EnhancedExceptionUtils.getLogMessage(e);
log.error(errorMessage);
//将错误信息返回给前台
return R.failed(CommonConstants.FAIL, errorMessage);
}

}

某知名博客文章的全局异常处理器demo(多个字段的错误校验可以更加直观地展示出来):

1
2
3
4
5
6
7
8
9
10
11
12
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(MethodArgumentNotValidException.class)
public Map<String, String> handleValidationExceptions(
MethodArgumentNotValidException ex) {
Map<String, String> errors = new HashMap<>();
ex.getBindingResult().getAllErrors().forEach((error) -> {
String fieldName = ((FieldError) error).getField();
String errorMessage = error.getDefaultMessage();
errors.put(fieldName, errorMessage);
});
return errors;
}

@Validated vs @Valid

两者的区别

@Validated应用在ctl方法的参数的好处

可以直接在形参上注解:

1
2
3
public ResponseMessage<IPage<PostPhoneInfo>> queryPage(@NotNull Long pageNum, @NotNull Long pageSize, PostPhoneInfo postPhoneInfo) {
return ResponseMessage.success(postPhoneInfoService.queryPage(pageNum, pageSize, postPhoneInfo));
}

之前我直接想直接采用注解校验入参,不想将校验逻辑带到业务中去,但是使用@Valid无法做到,而Spring封装好的@Validated可以做到。

要注意的地方

  • 校验注解是在方法入参上,则需要在该方法所在的类上添加 @org.springframework.validation.annotation.Validated 注解,在入参前或是在方法上添加启用校验注解都不生效。
  • 如果ctl方法的入参是一个Java Bean承载,那么无需使用@NotNull校验,Spring模式会给一个空实例,@NotNull注解没有意义
  • 使用@Validated分组的时候,比如只需要在某个ctl的方法上指定分组校验时,代码可以这样:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    /**
    * 新增 or 更新
    */
    @ApiOperation(notes = "保存单条记录", value = "保存查询单条记录")
    @PostMapping("/saveOne")
    @Validated(Default.class)
    public ResponseMessage<Void> saveOne(@Valid PostPhoneInfoVO postPhoneInfoVO) {
    postPhoneInfoService.saveOne(postPhoneInfoVO);
    return ResponseMessage.success();
    }

Custom Validation MessageSource in Spring Boot

MessageSource是Spring Boot很强大的一个特性,最为明显的是可以用于国际化处理。

一个登陆表单的demo

1
2
3
4
5
6
7
8
9
10
11
public class LoginForm {

@NotEmpty(message = "{email.notempty}")
@Email
private String email;

@NotNull
private String password;

// standard getter and setters
}

定义MessageSource Bean

ReloadableResourceBundleMessageSource是最常用的MessageSource实现类

1
2
3
4
5
6
7
8
9
@Bean
public MessageSource messageSource() {
ReloadableResourceBundleMessageSource messageSource
= new ReloadableResourceBundleMessageSource();

messageSource.setBasename("classpath:messages");
messageSource.setDefaultEncoding("UTF-8");
return messageSource;
}

定义LocalValidatorFactoryBean来注册上面的MessageSource Bean

1
2
3
4
5
6
@Bean
public LocalValidatorFactoryBean getValidator() {
LocalValidatorFactoryBean bean = new LocalValidatorFactoryBean();
bean.setValidationMessageSource(messageSource());
return bean;
}

定义国际化配置文件

1
2
3
4
# messages.properties
email.notempty=Please provide valid email id.
# messages_fr.properties
email.notempty=Veuillez fournir un identifiant de messagerie valide.

Method Constraints with Bean Validation 2.0

这个特性可以用于哪些比较复杂又常用的数据校验,而且校验逻辑是JSR380的注解不能实现的,所以需要自定义校验。

  • Single-parameter Constraints
  • Cross-Parameter Constraints
  • return constraints

自定义校验注解实现Cross-Parameter Constraints

比如以下Demo:

1
2
3
4
5
@ConsistentDateParameters
public void createReservation(LocalDate begin,
LocalDate end, Customer customer) {
// ...
}

如要有个业务场景是要求:begin必须比当前时间大,而且end必须比begin大的情况,那么使用JSR380的内置注解就无法实现了,所以要自定义校验注解和注解校验器。

1
2
3
4
5
6
7
8
9
10
11
12
13
@Constraint(validatedBy = ConsistentDateParameterValidator.class)
@Target({ METHOD, CONSTRUCTOR })
@Retention(RUNTIME)
@Documented
public @interface ConsistentDateParameters {

String message() default
"End date must be after begin date and both must be in the future";

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@SupportedValidationTarget(ValidationTarget.PARAMETERS)
public class ConsistentDateParameterValidator
implements ConstraintValidator<ConsistentDateParameters, Object[]> {

@Override
public boolean isValid(
Object[] value,
ConstraintValidatorContext context) {

if (value[0] == null || value[1] == null) {
return true;
}

if (!(value[0] instanceof LocalDate)
|| !(value[1] instanceof LocalDate)) {
throw new IllegalArgumentException(
"Illegal method signature, expected two parameters of type LocalDate.");
}

return ((LocalDate) value[0]).isAfter(LocalDate.now())
&& ((LocalDate) value[0]).isBefore((LocalDate) value[1]);
}
}

自定义返回值的校验注解

比如要校验某个复杂的对象Reservation:

1
2
3
4
5
6
7
public class ReservationManagement {
 
    @ValidReservation
    public Reservation getReservationsById(int id) {
        return null;
    }
}

自定义注解,注意注解的范围可以是构造器CONSTRUCTOR:

1
2
3
4
5
6
7
8
9
10
11
12
@Constraint(validatedBy = ValidReservationValidator.class)
@Target({ METHOD, CONSTRUCTOR })
@Retention(RUNTIME)
@Documented
public @interface ValidReservation {
    String message() default "End date must be after begin date "
      + "and both must be in the future, room number must be bigger than 0";
 
    Class<?>[] groups() default {};
 
    Class<? extends Payload>[] payload() default {};
}

校验器的具体实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class ValidReservationValidator
implements ConstraintValidator<ValidReservation, Reservation> {

@Override
public boolean isValid(
Reservation reservation, ConstraintValidatorContext context) {

if (reservation == null) {
return true;
}

if (!(reservation instanceof Reservation)) {
throw new IllegalArgumentException("Illegal method signature, "
+ "expected parameter of type Reservation.");
}

if (reservation.getBegin() == null
|| reservation.getEnd() == null
|| reservation.getCustomer() == null) {
return false;
}

return (reservation.getBegin().isAfter(LocalDate.now())
&& reservation.getBegin().isBefore(reservation.getEnd())
&& reservation.getRoom() > 0);
}
}

然后就可以在构造器方法上使用注解了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Reservation {

@ValidReservation
public Reservation(
LocalDate begin,
LocalDate end,
Customer customer,
int room) {
this.begin = begin;
this.end = end;
this.customer = customer;
this.room = room;
}

// properties, getters, and setters
}

级联校验 || 嵌套校验(Cascaded Validation)

想要校验对象里面的成员属性内部的相关校验,需要再成员属性声明加上@Valid注解,如:

1
2
3
4
5
6
7
8
9
10
public class Reservation {

@Valid
private Customer customer;

@Positive
private int room;

// further properties, constructor, getters and setters
}

Validating Container Elements with Bean Validation 2.0

注意内置的校验还可以用于集合的泛型定义

比如下面Customer前面的@NotNull:

1
2
3
4
5
6
7
public class ReservationManagement {
@NotNull
@Size(min = 1)
public List<@NotNull Customer> getAllCustomers() {
return null;
}
}

再比如:

1
2
3
4
5
public class Customer {    
List<@NotBlank(message="Address must not be blank") String> addresses;

// standard getters, setters
}

1
2
3
4
5
6
public class CustomerMap {

private Map<@Email String, @NotNull Customer> customers;

// standard getters, setters
}

Optional Values

1
2
3
4
5
private Integer age;

public Optional<@Min(18) Integer> getAge() {
return Optional.ofNullable(age);
}

非泛型容器的元素(Non-Generic Container Elements)

1
2
@Min(1)
private OptionalInt numberOfOrders;

Problem Solution

HV000030: No validator could be found for type: java.lang.Integer.

https://blog.csdn.net/xiaobingtao/article/details/41866235