简介
在应用的各个环节,最好对输入的参数进行校验,以保持应用的健壮性。Java的Bean Validation API对此提供了较好的支持,下面展示在Spring Boot中使用Bean Validation的示例。
使用环境:Java 11 和 Spring Boot 2.4.3.RELEASE
集成Bean Validation
在Spring Boot中集成Bean Validation与集成其他的框架类似,在pom.xml
里引入相关的starter就可以:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
这里用springmvc做示例。
在Spring Boot老版本中,validation包不用单独引入。在新版本中,validation被拆分,需要单独引入。引入的实现是hibernate validator。
对Controller的入参校验
通常来说,一个Controller有下面几种方式接收入参:
- 通过request body,如解析json请求
- 用url path作为参数,如
order/{id}
- get请求的url形式的
?key=value
传参,和post的类似key value传参等
Java Bean参数
通过request body和get、post的传参都可以通过一个java bean接收。我们可以在java bean中的字段上,加上校验的注解[1]:
@Data
@FieldDefaults(level = AccessLevel.PRIVATE)
public class OrderDto {
@NotNull
String orderId;
@NotNull
@Min(value = 0,message = "price can't less than 0")
@Max(value = 99999,message = "price can't greater than 99999")
Integer price;
@NotNull
@PastOrPresent
LocalDateTime startDate;
@FutureOrPresent
LocalDateTime closeDate;
}
在Controller中:
@Slf4j
@RestController
public class OrderController {
@PostMapping("order")
public String saveOrder(@Valid @RequestBody OrderDto orderDto){
log.info("order={}", orderDto);
return "success";
}
}
@RequestBody注解表示解析请求体中的参数。开启校验的关键是加上@Valid注解,加上之后,Spring就会对OrderDto进行参数的校验。
如果在OrderDto对象中,还有另一个复杂对象,比如OrderItem,那么需要在OrderItem字段上也加上@Valid注解,bean validation才会校验OrderItem。
现在可以尝试用非法的输入请求一下(即price为-1),看看结果:
curl --header "Content-Type: application/json" \
--request POST \
--data '{"orderId":"123456","price":-1,"startDate":"2021-01-01T00:00:00"}' \
http://localhost:8080/order
请求之后可以看到服务器返回http的状态码为400,以及一些默认的json响应。在这个例子中,参数校验失败之后,Spring会抛出MethodArgumentNotValidException
,而这个异常又会被Spring的DefaultHandlerExceptionResolver
处理,处理之后返回400状态码和默认的json响应。通常可以自己注册一个全局异常处理器来统一处理。
Path参数和方法参数
对于简单的单个参数或者Path参数,不需要专门建一个java bean接收参数,对这类参数的校验方法如下:
@Slf4j
@Validated
@RestController
public class OrderQueryController {
@GetMapping("order/{orderId}")
public String getOrder(@PathVariable @Min(0) String orderId){
log.info("orderId={}", orderId);
return orderId;
}
}
只需在controller类上加上@Validated注解,再在对应的参数前加上校验注解就可以,在本例中是加上了最小值@Min(0)的校验。
请求该接口查看校验结果:
curl http://localhost:8080/order/-1
此时会发现服务器返回了500的状态码。这是因为这时抛出的异常是ConstraintViolationException
,这种场景下最好自己注册一个异常处理器来处理ConstraintViolationException
,并统一返回400的状态码和消息格式。
对Service的入参校验
除了对controller层进行校验,还可以用上述方法对service层校验。比如使用了一些RPC框架直接调用service方法时,可以用Bean Validation对入参校验。
自定义Validator
如果内置的校验注解不满足需求,还可以创建自定义的validation注解。创建一个自定义Validator分两步:
- 创建注解
- 创建校验类
以传入查询参数的OrderNumber为例,OrderNumber必须要以为on开头。
创建注解
创建Validator注解有固定的模版:
@Target({ElementType.FIELD,ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint(validatedBy = OrderNumberValidator.class)
public @interface OrderNumber {
String message() default "order number must start with on";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
- @Constraint注解来表示校验类实现
- message()表示当违反了校验规则时,出现的提示信息
- groups表示分组,校验可以按组进行,但一般情况下最好一个类就一种校验规则
- payload使用的地方较少,所以不在这里介绍
创建校验类
校验类只需实现ConstraintValidator接口就可以:
public class OrderNumberValidator implements ConstraintValidator<OrderNumber, String> {
@Override
public void initialize(OrderNumber constraintAnnotation) {
}
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
return StringUtils.startsWithIgnoreCase(value, "on");
}
}
initialize方法会初始化校验类
这时就可以像内置注解一样使用了:
@GetMapping("order")
public String getOrderByNumber(@OrderNumber String orderNumber){
log.info("order number={}", orderNumber);
return orderNumber;
}
按类作为整体校验
除了在类的字段上一个一个加注解来校验,还可以以类作为一个整体来校验。
创建的方式与上面类似,首先创建一个Validator的自定义注解:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint(validatedBy = OrderValidator.class)
public @interface OrderValidation {
String message() default "order validation failed";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
再创建一个校验实现类:
public class OrderValidator implements ConstraintValidator<OrderValidation,Order> {
@Override
public boolean isValid(Order order, ConstraintValidatorContext constraintValidatorContext) {
if (order == null) {
return false;
}
LocalDateTime startDate = order.getStartDate();
LocalDateTime closeDate = order.getCloseDate();
if (startDate == null || closeDate == null) {
return false;
}
return !startDate.isAfter(closeDate);
}
}
最后创建一个被校验的类:
@Data
@FieldDefaults(level = AccessLevel.PRIVATE)
@OrderValidation
public class Order {
String orderId;
Integer price;
LocalDateTime startDate;
LocalDateTime closeDate;
}
这时就可以像之前的java bean校验一样,只需加上@Valid注解:
@PostMapping("order/validation")
public Order orderValidation(@Valid @RequestBody Order order){
log.info("order={}", order);
return order;
}
在代码中显式校验
有些情况下,需要更加灵活地在代码中校验,这时可以显式调用校验方法:
@Slf4j
@RestController
public class OrderController {
@Autowired
private Validator validator;
@PostMapping("order/validateDto")
public String orderValidateDto(@RequestBody OrderDto orderDto){
log.info("order={}", orderDto);
Set<ConstraintViolation<OrderDto>> violations = validator.validate(orderDto);
if (!violations.isEmpty()) {
throw new ConstraintViolationException(violations);
}
return "success";
}
}
Spring已经配置好了一个Validator的bean,可以直接注入使用。
也可以自己实现一个Validate的工具类来实现校验:
@UtilityClass
public class ValidatorUtils {
private final ValidatorFactory vf = Validation.buildDefaultValidatorFactory();
private final Validator validator = vf.getValidator();
public static <T> Set<ConstraintViolation<T>> validate(T bean){
return validator.validate(bean);
}
}
Spring的全局统一异常处理示例
上面提到,参数校验的异常最好能统一处理,下面是处理示例:
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(ConstraintViolationException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ResponseBody
public Map<String,String> handleError(ConstraintViolationException e){
return Map.of("msg", "invalid parameter :: " +
e.getConstraintViolations().stream()
.map(v -> v.getPropertyPath().toString() + ":" + v.getMessage())
.collect(Collectors.joining(",")));
}
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ResponseBody
public Map<String,String> handleError(MethodArgumentNotValidException e){
String msg = e.getBindingResult().getFieldErrors().isEmpty() ? e.getMessage()
: e.getBindingResult().getFieldErrors().stream()
.map(f -> f.getField() + ":" + f.getDefaultMessage())
.collect(Collectors.joining(","));
return Map.of("msg", "invalid parameter :: " + msg);
}
}
总结
本文展示了bean validation的常用功能示例和一些扩展,示例工程可以在Github上找到。
参考资料
[1]Hibernate Validation内置的注解列表:https://docs.jboss.org/hibernate/beanvalidation/spec/2.0/api/javax/validation/constraints/package-summary.html
评论
发表评论