路由器函数中的 REST 请求参数验证

REST request argument Validation In Router Function

在 Spring 控制器方法中,我们可以使用 @Valid 和类似这样的东西

进行 REST 请求参数验证
@PostMapping(REGISTER)
  public ResponseEntity<SomeData> registerSomeData(@RequestBody @Valid final SomeData someData) {
    ...................
  }



public class SomeData {

  @Size(min = 2, max = 20)
  private String firstname;

  @Digits(fraction = 0, integer = 10)
  private Integer customerID;

  @NotNull
  private Customer customer;
}

如果请求不符合这些约束,那么 Spring 框架将抛出​​ Bad Request Exception(400)。

有了Spring5个路由器函数,我不明白我们怎么能做到这一点,因为我们不能在路由器函数中给出@Valid

您不能将基于注释的验证与(功能)Spring Webflux 一起使用。参见

如果您绝对需要基于注释的验证,您应该知道您可以继续使用传统的 Spring MVC 和 Spring 5(或非功能性 Webflux)。

有点恼火的是,这个有用的功能似乎并没有被应用到函数世界中,但是自己实现验证步骤确实并不难。方法如下。

创建一个 bean 来执行验证:

@Component
public class RequestValidator {

  @Inject
  Validator validator;

  public <T> Mono<T> validate(T obj) {

    if (obj == null) {
      return Mono.error(new IllegalArgumentException());
    }

    Set<ConstraintViolation<T>> violations = this.validator.validate(obj);
    if (violations == null || violations.isEmpty()) {
      return Mono.just(obj);
    }

    return Mono.error(new ConstraintViolationException(violations));
  }
}

现在在您的处理程序函数中包含一个执行验证的步骤。在此示例中,FindRequest class 是一个 JSON 域模型 class,其中包含验证注释,例如 @NotEmpty@NotNull 等。调整您的方式根据这个调用反应式数据存储库的虚构示例构建 ServerResponse

  @Component
  public class MyHandler {

    @Inject
    RequestValidator validator;

    public Mono<ServerResponse> findAllPeople(ServerRequest request) {

      return request.bodyToMono(FindRequest.class)
          .flatMap(this.validator::validate)
          .flatMap(fr -> ServerResponse
              .ok()
              .body(this.repo.findAllByName(fr.getName()), Person.class));
    }
  }

可以使用相同的方法来扩展功能以处理 Flux 以及 Mono

我创建了 GeneralValidator class,效果很好 javax.validation.Validator

@Component
@RequiredArgsConstructor
public class GeneralValidator {

    private final Validator validator;

    private <T> void validate(T obj) {

        if (obj == null) {
            throw new IllegalArgumentException();
        }

        Set<ConstraintViolation<T>> violations = this.validator.validate(obj);
        if (violations != null && !violations.isEmpty()) {
            throw new ConstraintViolationException(violations);
        }
    }


     /**
     * @param obj object we will validate
     * @param next Publisher we will call if don't have any validation hits
     */
    public <T> Mono<ServerResponse> validateAndNext(T obj, Mono<ServerResponse> next) {
        try {
            validate(obj);
            return next;
        } catch (IllegalArgumentException ex) {
            return ServerResponse.badRequest()
                    .body(new ErrorResponse("Request body is empty or unable to deserialize"), ErrorResponse.class);
        } catch (ConstraintViolationException ex) {
            return ServerResponse.badRequest()
                    .body(new ValidationErrorResponse(
                            "Request body failed validation",
                            ex.getConstraintViolations()
                                    .stream()
                                    .map(v -> "Field '%s' has value %s but %s"
                                            .formatted(v.getPropertyPath(), v.getInvalidValue(),v.getMessage()))
                                    .collect(Collectors.toList())
                    ), ValidationErrorResponse.class);
        }
    }
}

使用方法:

...
.POST("/", req -> req.bodyToMono(RequestObject.class)
        .flatMap(r -> validator.validateAndNext(r,routeFunction.execute(r)))
)
...

routeFunction.execute:

public @NotNull Mono<ServerResponse> execute(RequestObject request) {
//handling body
}