参考来自www.baeldung.com的多篇文章
Java Bean Validation的历史演变
- 原本以为JSR303就已经是够了,后面才发现这只是initial version,也就是Bean Validation 1.0;
- 后面又推出了JSR349(Bean Validation 1.1)
- 而最新版本是JSR380(Bean 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 | @Test |
校验注解
1 | 空检查 |
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
3public 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 | public class LoginForm { |
定义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 | @Bean |
定义国际化配置文件
1 | # messages.properties |
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 | @SupportedValidationTarget(ValidationTarget.PARAMETERS) |
自定义返回值的校验注解
比如要校验某个复杂的对象Reservation:1
2
3
4
5
6
7public 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
27public 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
16public 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
10public 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
7public class ReservationManagement {
@NotNull
@Size(min = 1)
public List<@NotNull Customer> getAllCustomers() {
return null;
}
}
再比如:1
2
3
4
5public class Customer {
List<@NotBlank(message="Address must not be blank") String> addresses;
// standard getters, setters
}
1 | public class CustomerMap { |
Optional Values
1 | private Integer age; |
非泛型容器的元素(Non-Generic Container Elements)
1 | @Min(1) |