跳至主要内容

Spring Boot Bean Validation 示例

Welcome file

简介

在应用的各个环节,最好对输入的参数进行校验,以保持应用的健壮性。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分两步:

  1. 创建注解
  2. 创建校验类

以传入查询参数的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

评论

此博客中的热门博文

国密SM2签名封装成PKCS7格式

在国内做金融行业,难免会有被强制使用国密算法的情况,而且一般还会指定必须使用硬件加密机之类的设备,所以我也稍微的研究了一下国密算法,使用软算法签名并封装 PKCS7 格式(文档中的一个交互)。 以下是基于 Bouncy Castle 的示例,密钥对的生成可以参考 Bouncy Castle 中 test 包下 SM2 相关代码 public static String sign ( ) throws Exception { //加载公钥 byte [ ] plainText = "hello, world" . getBytes ( ) ; FileInputStream input = new FileInputStream ( "F:\\certificate\\public.cer" ) ; CertificateFactory certificateFactory = new CertificateFactory ( ) ; X509Certificate certificate = ( X509Certificate ) certificateFactory . engineGenerateCertificate ( input ) ; input . close ( ) ; //加载私钥,private为换成实际的私钥 PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec ( "private" . getBytes ( ) ) ; //SM2算法实际上为ECC算法,并指定了一些参数值,所以这里的参数是EC KeyFactory factory = KeyFactory . getInstance ( "EC" , "BC" ) ; PrivateKey privateKey = factory . generatePrivate ( spec ) ; //以下为签名并封装成PKCS7格式 byte [ ] signedMessag

Spring Boot Actuator 2 示例

Welcome file 简介 Spring Boot Actuator为应用程序提供了各种开箱即用的运维特性,可以与应用方便的交互和监控。 使用环境:Java 11 和 Spring Boot 2.4.3.RELEASE 集成Spring Boot Actuator 在Spring Boot中集成Spring Boot Actuator与集成其他的框架类似,在 pom.xml 里引入相关的starter就可以: < dependency > < groupId > org.springframework.boot </ groupId > < artifactId > spring-boot-starter-actuator </ artifactId > </ dependency > < dependency > < groupId > org.springframework.boot </ groupId > < artifactId > spring-boot-starter-web </ artifactId > </ dependency > 由于大部分的使用场景还是web,所以这里也用Spring MVC做示例。 配置好 pom.xml 后,默认actuator仅暴露一些基本功能,实际使用中,根据需求暴露对应功能。为了简便测试,这里在 application.yml 中配置暴露全部功能: management : endpoints : web : exposure : include : "*" endpoint : health : enabled : true show-details : always probes : enabled : true shutdown : enabled : true metr

NextCloud数据目录迁移

最近服务器的环境坏了,所以迁移了NextCloud的数据目录。不过在迁移过程中有点小问题。 环境: Ubuntu 18.04 Docker 19.03.7 1.NextCloud页面不正常,Docker日志显示XX目录permission denied 参考了 这里 的做法,不过是把  /var/www/html/   整个目录的权限都修改为  chown -R www-data:www-data ,之后就不再报权限问题了。 2.数据库配置修改 因为NextCloud是在初始化时填的数据库连接信息,所以直接迁移数据目录的情况下,会导致应用连不到新的数据库环境。此时可以找到数据目录下的  config/config.php 文件,直接修改数据库连接配置。