Spring 5 的时候,Web MVC 栈引入了 Functional Endpoints。这个东西东西是可以用于 Spring WebFlux 的,但是我对 WebFlux 栈不熟,所以这里说说我在 Web MVC 中使用的一些体验。

开始

首先,创建一个 Spring Boot 应用程序,这样我们就开始了,不需要任何多余的配置。

定义一个 Handler

一个 handler 是一个 HandlerFunction 实例。定义 handler 的方式十分简单,只要实现这个接口就好了,比如:

  1. public class SimpleHandler implements HandlerFunction<ServerResponse> {
  2. @Override
  3. public ServerResponse handle(ServerRequest request) throws Exception {
  4. return ServerResponse.ok()
  5. .header("Content-Type", "application/json")
  6. .body(Map.of("hello", "world"));
  7. }
  8. }

上面的代码定义了一个 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 获取校验结果。这里行不通了,要自己校验。

首先要

开启校验

  1. 在 Spring Boot 应用程序中引入 spring-boot-starter-validation
  2. 定义一个 Validator bean,方式如下 ```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 {

  1. @Bean
  2. public Validator validator(ApplicationContext ctx) {
  3. return ValidatorAdapter.get(ctx, getValidator());
  4. }

}

  1. <a name="FbwL9"></a>
  2. ### 使用校验
  3. 主要把这个 `Validator` 注入到 Handler 中,就可以使用啦,像下面这样:
  4. ```java
  5. public class SimpleHandler implements HandlerFunction<ServerResponse> {
  6. @Override
  7. public ServerResponse handle(ServerRequest request) throws Exception {
  8. var body = request.body(Person.class);
  9. Errors errors = new BeanPropertyBindingResult(body, "whatever name");
  10. validator.validate(body, errors);
  11. // do something to errors, omitted
  12. // response handling omitted
  13. }
  14. @Autowired
  15. private Validator validator;
  16. class Person {
  17. @NotNull(message = "name cannot be null")
  18. String name;
  19. void setName(String name) {
  20. this.name = name;
  21. }
  22. }
  23. }

路由

注册路由的方式非常简单,只要定义一个 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 文档