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 的异步模型中,我们只能拿到 Mono
或 Flux
,拿不到实际数据,所以无法在 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 仓库。