1. 概述

在 API 开发中,对输入数据进行校验是非常有必要的,能够有效避免后续处理过程中因非法数据引发的异常。

遗憾的是,在 Spring 6 中,函数式接口(Functional Endpoints)无法像注解式接口那样自动执行校验逻辑,我们必须手动处理。

不过,借助 Spring 提供的一些工具,我们依然可以优雅、清晰地实现输入校验逻辑。

2. 使用 Spring 校验机制

在深入校验逻辑之前,我们先构建一个可以正常运行的函数式接口示例。

假设我们有如下 RouterFunction

@Bean
public RouterFunction<ServerResponse> functionalRoute(
  FunctionalHandler handler) {
    return RouterFunctions.route(
      RequestPredicates.POST("/functional-endpoint"),
      handler::handleRequest);
}

这个路由使用了如下 Handler 类提供的处理函数:

@Component
public class FunctionalHandler {

    public Mono<ServerResponse> handleRequest(ServerRequest request) {
        Mono<String> responseBody = request
          .bodyToMono(CustomRequestEntity.class)
          .map(cre -> String.format(
            "Hi, %s [%s]!", cre.getName(), cre.getCode()));
 
        return ServerResponse.ok()
          .contentType(MediaType.APPLICATION_JSON)
          .body(responseBody, String.class);
    }
}

可以看到,该接口只是简单地格式化并返回请求体中的 CustomRequestEntity 对象:

public class CustomRequestEntity {
    
    private String name;
    private String code;

    // ... Constructors, Getters and Setters ...
    
}

目前一切正常,但如果我们需要对输入做些约束,比如字段不能为空,code 长度不少于 6 位,那该怎么办?

我们需要一种与业务逻辑解耦、又能高效校验输入的方式。

2.1. 实现一个 Validator

根据 Spring 官方文档,我们可以使用 Spring 的 Validator 接口来校验资源:

public class CustomRequestEntityValidator 
  implements Validator {

    @Override
    public boolean supports(Class<?> clazz) {
        return CustomRequestEntity.class.isAssignableFrom(clazz);
    }

    @Override
    public void validate(Object target, Errors errors) {
        ValidationUtils.rejectIfEmptyOrWhitespace(
          errors, "name", "field.required");
        ValidationUtils.rejectIfEmptyOrWhitespace(
          errors, "code", "field.required");
        CustomRequestEntity request = (CustomRequestEntity) target;
        if (request.getCode() != null && request.getCode().trim().length() < 6) {
            errors.rejectValue(
              "code",
              "field.min.length",
              new Object[] { Integer.valueOf(6) },
              "The code must be at least [6] characters in length.");
        }
    }
}

这里不展开讲解 Validator 的具体实现机制,只需知道:当校验失败时,错误会被收集到 Errors 对象中;如果 Errors 为空,说明校验通过。

有了 Validator 后,我们必须在执行业务逻辑前手动调用 validate 方法。

2.2. 执行校验逻辑

你可能会想到使用 [HandlerFilterFunction](/spring-webflux-filters) 来统一处理校验,但 ❌ 在 WebFlux 的异步模型中,我们只能拿到 MonoFlux,拿不到实际数据,所以无法在 Filter 中校验请求体。

✅ 最佳实践是:在 Handler 中处理请求体时再进行校验。

我们来修改 Handler 方法,加入校验逻辑:

public Mono<ServerResponse> handleRequest(ServerRequest request) {
    Validator validator = new CustomRequestEntityValidator();
    Mono<String> responseBody = request
      .bodyToMono(CustomRequestEntity.class)
      .map(body -> {
        Errors errors = new BeanPropertyBindingResult(
          body,
          CustomRequestEntity.class.getName());
        validator.validate(body, errors);

        if (errors == null || errors.getAllErrors().isEmpty()) {
            return String.format("Hi, %s [%s]!", body.getName(), body.getCode());
        } else {
            throw new ResponseStatusException(
              HttpStatus.BAD_REQUEST,
              errors.getAllErrors().toString());
        }
    });
    return ServerResponse.ok()
      .contentType(MediaType.APPLICATION_JSON)
      .body(responseBody, String.class);
}

这样,如果请求体不合法,就会返回 400 Bad Request

虽然功能实现了,但 ❌ 校验逻辑与业务逻辑混在一起,而且每处都需要重复这段代码,不优雅。

我们来优化一下。

3. 构建 DRY 的校验机制

我们可以封装一个抽象类,把校验流程统一处理,遵循 DRY 原则。

使用泛型支持不同的实体和校验器:

public abstract class AbstractValidationHandler<T, U extends Validator> {

    private final Class<T> validationClass;

    private final U validator;

    protected AbstractValidationHandler(Class<T> clazz, U validator) {
        this.validationClass = clazz;
        this.validator = validator;
    }

    public final Mono<ServerResponse> handleRequest(final ServerRequest request) {
        // ...here we will validate and process the request...
    }
}

实现 handleRequest 方法:

public Mono<ServerResponse> handleRequest(final ServerRequest request) {
    return request.bodyToMono(this.validationClass)
      .flatMap(body -> {
        Errors errors = new BeanPropertyBindingResult(
          body,
          this.validationClass.getName());
        this.validator.validate(body, errors);

        if (errors == null || errors.getAllErrors().isEmpty()) {
            return processBody(body, request);
        } else {
            return onValidationErrors(errors, body, request);
        }
    });
}

定义校验失败时的默认处理:

protected Mono<ServerResponse> onValidationErrors(
  Errors errors,
  T invalidBody,
  ServerRequest request) {
    throw new ResponseStatusException(
      HttpStatus.BAD_REQUEST,
      errors.getAllErrors().toString());
}

定义抽象方法,由子类实现业务逻辑:

abstract protected Mono<ServerResponse> processBody(
  T validBody,
  ServerRequest originalRequest);

完整代码可参考 GitHub 示例

3.1. 改造 Handler

现在我们的 Handler 只需继承这个抽象类,并实现 processBody 方法即可:

@Component
public class FunctionalHandler
  extends AbstractValidationHandler<CustomRequestEntity, CustomRequestEntityValidator> {

    private CustomRequestEntityValidationHandler() {
        super(CustomRequestEntity.class, new CustomRequestEntityValidator());
    }

    @Override
    protected Mono<ServerResponse> processBody(
      CustomRequestEntity validBody,
      ServerRequest originalRequest) {
        String responseBody = String.format(
          "Hi, %s [%s]!",
          validBody.getName(),
          validBody.getCode());
        return ServerResponse.ok()
          .contentType(MediaType.APPLICATION_JSON)
          .body(Mono.just(responseBody), String.class);
    }
}

这样改造后,Handler 只关注业务逻辑,不再关心校验细节,代码更清晰。

4. 支持 Bean Validation 注解

我们还可以利用 javax.validation 提供的注解来简化校验逻辑。

例如定义一个带注解的实体类:

public class AnnotatedRequestEntity {
 
    @NotNull
    private String user;

    @NotNull
    @Size(min = 4, max = 7)
    private String password;

    // ... Constructors, Getters and Setters ...
}

然后创建 Handler,注入 Spring 提供的默认 Validator(由 LocalValidatorFactoryBean 提供):

public class AnnotatedRequestEntityValidationHandler
  extends AbstractValidationHandler<AnnotatedRequestEntity, Validator> {

    private AnnotatedRequestEntityValidationHandler(@Autowired Validator validator) {
        super(AnnotatedRequestEntity.class, validator);
    }

    @Override
    protected Mono<ServerResponse> processBody(
      AnnotatedRequestEntity validBody,
      ServerRequest originalRequest) {

        // ...

    }
}

⚠️ 如果项目中存在多个 Validator Bean,记得加上 @Primary

@Bean
@Primary
public Validator springValidator() {
    return new LocalValidatorFactoryBean();
}

5. 总结

本文介绍了如何在 Spring 5 的函数式接口中实现输入校验。

我们通过封装抽象类,将校验逻辑与业务逻辑分离,避免重复代码。

✅ 虽然这套方案不是万能的,但它提供了一个清晰、可复用的结构,你可以根据自己的业务需求进行调整。

完整示例代码请参考 GitHub 仓库


原始标题:Validation for Functional Endpoints in Spring 5