Spring 5 的时候,Web MVC 栈引入了 Functional Endpoints。这个东西东西是可以用于 Spring WebFlux 的,但是我对 WebFlux 栈不熟,所以这里说说我在 Web MVC 中使用的一些体验。
开始
首先,创建一个 Spring Boot 应用程序,这样我们就开始了,不需要任何多余的配置。
定义一个 Handler
一个 handler 是一个 HandlerFunction 实例。定义 handler 的方式十分简单,只要实现这个接口就好了,比如:
public class SimpleHandler implements HandlerFunction<ServerResponse> {@Overridepublic ServerResponse handle(ServerRequest request) throws Exception {return ServerResponse.ok().header("Content-Type", "application/json").body(Map.of("hello", "world"));}}
上面的代码定义了一个 handler,这个 handler 处理请求的方式是,什么都不做,然后以通过 http 返回一个 json 数据。
使用的时候,只要 new 一个 SimpleHandler 对象,或这个干脆把它声明为一个 Spring Bean,然后配置到路由中就好了(路由后面说)。
ServerResponse 有很多其他的 api,没有啥好说的,跟着 IDE 的智能提示看一看就好了。
请求数据
Spring MVC 中请求数据分为几类:path variables、query params、request body、http header。MVC 函数式 Endpoints 肯定是都支持的,只不过没有基于 Controller 的 handler 那么智能。
- Path variable 通过 ServerRequest 的
pathVariable方法来获取,返回一个String - Query param 通过
param方法获取,返回一个Optional<String> - Request body 通过
body方法获取,通过传入一个Class对象,body方法可以把 request body 转成一个指定的对象(和@RequestBody注解很像) - Http header 要通过
headers方法获取
数据校验
在基于 Controller 的 handler 实现中,只要给 @RequestBody 参数加一个 @Valid 或者 @Validated 注解,就可以自动进行校验了,然后再通过 BindingResult 获取校验结果。这里行不通了,要自己校验。
开启校验
- 在 Spring Boot 应用程序中引入 spring-boot-starter-validation
- 定义一个
Validatorbean,方式如下 ```java import org.springframework.boot.autoconfigure.validation.ValidatorAdapter; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.validation.Validator; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration public class WebMvcConfig implements WebMvcConfigurer {
@Beanpublic Validator validator(ApplicationContext ctx) {return ValidatorAdapter.get(ctx, getValidator());}
}
<a name="FbwL9"></a>### 使用校验主要把这个 `Validator` 注入到 Handler 中,就可以使用啦,像下面这样:```javapublic class SimpleHandler implements HandlerFunction<ServerResponse> {@Overridepublic ServerResponse handle(ServerRequest request) throws Exception {var body = request.body(Person.class);Errors errors = new BeanPropertyBindingResult(body, "whatever name");validator.validate(body, errors);// do something to errors, omitted// response handling omitted}@Autowiredprivate Validator validator;class Person {@NotNull(message = "name cannot be null")String name;void setName(String name) {this.name = name;}}}
路由
注册路由的方式非常简单,只要定义一个 RouterFunction bean,然后把 handler 配置进去就好了:
@Bean
public RouterFunction<ServerResponse> router() {
return RouterHelper.routerBuilder()
.GET("/simple", new SimpleHandler())
.POST("/login", loginHandler)
.build();
}
这样,就可以通过 http://[host]:[port][context-path]/simple 来访问 api 了。
可以定义多个 RouterFunction bean,要记得给它们取不同的 bean name。
路由嵌套
路由嵌套有两种方式
使用 nest 方法
nest 方法的用法如下:
RouterFunction<ServerResponse> nestedRoute = RouterFunctions.route()
.nest(RequestPredicates.path("/user"), () -> RouterFunctions.route()
.GET("/info", user::info)
// add more handlers
.build()
).build();
使用 path 方法
path 方法的用法如下:
RouterHelper.routerBuilder()
.path("/user", builder -> builder
.GET("/info", user::info)
// add more handlers
).build();
Filter
Filter 可以用来做一些比如权限控制啦、身份认证啦、参数预处理啦之类的东西。
函数式 endpoint 支持 filter,而且这个 api 非常好用。
定义 Filter
这里 filter 是指一个 HandlerFilterFunction 对象,定义 filter 只要实现这个接口就好了。比如:
HandlerFilterFunction<ServerResponse, ServerResponse> filter
= (request, next) -> {
if (request.param("pass").isEmpty())
throw BizException.badRequest("not pass");
return next.handle(request);
};
使用 Filter
HandlerFilterFunction 有一个 apply 方法,它接收一个 HandlerFunction,返回一个新的 HandlerFuntion。通过 apply 方法,我们就可以把 filter 应用到 handler 上了。比如:
RouterHelper.routerBuilder()
.GET("/filter", filter.apply(filterHandler))
.build();
HandlerFilterFunction 还有一个 andThen 方法可以把两个 filter 连成一串,这样就可以为 handler 应用多个 filter 了。
错误处理
基于 Controller 的 handler 在进行全局错误处理时,使用 @ExceptionHandler 注解来配置异常处理器。但是在这里这种方式就不适用了。
不过我们可以在 build router 时通过 onError 方法来注册错误处理器,例如:
public class RouterHelper {
public static RouterFunctions.Builder routerBuilder() {
return RouterFunctions.route()
// add more error handlers
.onError(Throwable.class,
RouterHelper::unhandled)
.onError(ServerWebInputException.class,
RouterHelper::webInputError);
}
static ServerResponse unhandled(Throwable ex, ServerRequest request) {
log.error("未经处理的错误", ex);
var json = new JsonObject();
json.put("msg", ex.getMessage());
if (ex.getCause() != null) {
json.put("cause", ex.getCause().getMessage());
}
return ServerResponse.status(INTERNAL_SERVER_ERROR.value())
.contentType(APPLICATION_JSON)
.body(json);
}
static ServerResponse webInputError(Throwable ex, ServerRequest request) {
log.error("请求参数错误", ex);
var inputError = (ServerWebInputException) ex;
var json = new JsonObject();
json.put("msg", inputError.getReason());
if (ex.getCause() != null) {
json.put("cause", inputError.getCause().getMessage());
}
return ServerResponse.status(BAD_REQUEST.value())
.contentType(APPLICATION_JSON)
.body(json);
}
private static final Logger log = LoggerFactory.getLogger(RouterHelper.class);
}
在效果上,和 @ExceptionHandler 是一样的。
RequestPredicate
可以对请求做断言(predicate)。如果不满足,则返回 404。
一个请求断言是一个 RequestPredicate 对象,使用方式见下例:
RequestPredicate predicate = request
-> request.param("access").isPresent();
HandlerFunction<ServerResponse> predicateHandler = request
-> ServerResponse.ok().body(Map.of());
RouterHelper.routerBuilder()
.GET("/predicate", predicate, predicateHandler)
.build();
Kotlin
如果你使用 kotlin,你还可以使用 spring framework 提供的 router DSL 来让 router 定义看起来更清爽。戳这里查看 DSL 文档。
