Spring MVC 函數語言程式設計進階

碼農小胖哥發表於2020-05-18

1. 前言

上一篇對 Spring MVC 的函式式介面程式設計進行了簡單入門,讓很多不知道的同學見識了這種新操作。也有反應這種看起來沒有傳統寫法順眼,其實大家都一樣。但是我們還是要敢於嘗試新事物。Java Lambada 剛出來也是被人各種吐槽,現在我在很多專案都見到了它的身影。好了轉回正題,本文是對上一篇的延伸,我們繼續對 Functional Endpoint 進行一些瞭解和運用。正規化轉換其實上一篇已經介紹差不多了,但是一旦你初次接觸這種方式往往會面臨新的問題。

2. 新的問題

在使用這種風格時我們也會遇到一些新的問題。接下來我們將通過舉例來一步步解決這些問題。

2.1 如何異常處理

介面異常處理是必須的。改成函式式風格後異常可以這樣處理:

    /**
     * 介面附帶異常處理邏輯.
     *
     * @param userService the user service
     * @return the user by name with error handle
     */
    public RouterFunction<ServerResponse> withErrorHandle() {
        return RouterFunctions.route()
                .GET("/userwitherrorhandle/{username}",
                        request -> ServerResponse.ok()
                          .body(userService.getByName(request.pathVariable("username"))))
                // 異常處理
                .onError(RuntimeException.class,
                        (e, request) -> EntityResponse.fromObject(e.getMessage())
                                .status(HttpStatus.INTERNAL_SERVER_ERROR)
                                .build())
                .build();
    }

你可以使用上面的 onError 方法及其過載方法進行介面的異常處理。但是傳統方法有統一異常處理啊!不要捉急,後面我們也會進行統一異常的處理。

2.2 如何使用過濾器

我還有不少 Spring MVC 在使用過濾器呢,使用這種風格如何編寫過濾器,上一篇漏掉了一個處理過濾器的函式式介面HandlerFilterFunction 。我們通過該介面來對請求進行過濾:

    /**
     * 對特定介面指定過濾器.
     *
     * @param userService the user service
     * @return the router function
     */
    public RouterFunction<ServerResponse> withFilter() {
        return RouterFunctions.route().POST("/save",
                request -> ServerResponse.ok()
                        .body(userService.saveUser(request.body(UserInfo.class))))
                // 執行了一個過濾器邏輯 引數攜帶了 save 放行 否則返回 bad request 並附帶訊息
                .filter((request, next) -> request.param("save").isPresent() ?
                        next.handle(request) :
                        ServerResponse.status(HttpStatus.BAD_REQUEST).body("no save"))
                .build();
    }

通過 filter 方法我們可以實現日誌、安全策略、跨域等功能。

2.3 如何使用攔截器

使用函數語言程式設計風格時並沒有提供 Spring MVC 的攔截器 API,但是提供了類似過濾器前置/後置處理機制以達到同樣的效果。

    public RouterFunction<ServerResponse> getUserByName() {
        return RouterFunctions.route()
                .GET("/user/{username}",
                        request -> ServerResponse.ok()
                         .body(userService.getByName(request.pathVariable("username"))))
                // 前置處理 列印 path
                .before(serverRequest -> {
                    log.info(serverRequest.path());
                    return serverRequest;
                })
                // 後置處理 如果響應狀態為200 則列印 response ok
                .after(((serverRequest, serverResponse) -> {
                    if (serverResponse.statusCode() == HttpStatus.OK) {
                        log.info("response ok");
                    }
                    return serverResponse;
                })).build();
    }

當你請求/user/{username}時, beforeafter 方法將會分別進行前置和後置處理。

2.4 如何進行統一處理

傳統方式我們每個Controller 處理的都是特定單一領域的業務,UserController 處理 User相關業務,我們會給它新增一個統一的字首標識 /v1/user;OrderController處理 Order 相關業務,給它新增一個統一的字首標識 /v1/order。對相同得業務介面進行聚合更加有利於維護使用函數語言程式設計我們可以通過以下方式實現:

@Bean
RouterFunction<ServerResponse> userEndpoints(UserController userController) {
    return RouterFunctions.route()
            .path("/v2/user", builder -> builder
                    //  /get/{username} -> /v2/user//get/{username}
                    .add(userController.getUserByName()
                            //  /del/{username} -> /v2/user//del/{username}
                            .and(userController.delUser()
                                    // /save -> /v2/user/save
                                    .and(userController.saveUser()
                                            // /update -> /v2/user/update
                                            .and(userController.updateUser())))))
            .build();
}

你也可以使用 RouterFunctions.route().nest相關的方法進行實現。而且對這些路由進行分組聚合之後就可以統一過濾器、攔截器、異常處理。例如上一篇提到的統一異常問題:

@Bean
RouterFunction<ServerResponse> nestEndpoints(UserController userController) {
    return RouterFunctions.route().nest(RequestPredicates.path("/v1/user"),
            builder -> builder
                    .add(userController.getUserByName())
                    .add(userController.delUser())
                    .add(userController.saveUser())
                    .add(userController.updateUser()))
            // 對上述路由進行統一的異常處理
            .onError(RuntimeException.class,
                    (throwable, serverRequest) -> ServerResponse
                            .status(HttpStatus.BAD_REQUEST)
                            .body("bad req"))
            .build();
}

3. 總結

本文主要對 Spring MVC 函式式開發和傳統開發中等效的特性(過濾器、攔截器、分組聚合等)進行了簡單的說明,更加貼合於實際運用。函式式風格開發更加靈活,但是同樣讓習慣指令式程式設計的開發者有點不適應,但是目前越來越被普遍的應用。所以-/如果有志於長期從事程式設計開發的同學來說,還是需要掌握的。本文的 demo 可通過關注公眾號:Felordcn回覆 mvcfun 獲取。

關注公眾號:Felordcn 獲取更多資訊

個人部落格:https://felord.cn

相關文章