知識庫 / Spring / Spring MVC RSS 訂閱

基於Spring MVC的函數控制器

Spring MVC
HongKong
6
01:20 PM · Dec 06 ,2025

1. 引言

Spring 5 引入了 WebFlux,這是一個新的框架,允許我們使用響應式編程模型構建 Web 應用程序。

在本教程中,我們將學習如何將這種編程模型應用於 Spring MVC 中的函數式控制器。

2. Maven 設置

我們將使用 Spring Boot 來演示新的 API。

該框架支持基於註解的定義控制器的方法。但它還添加了一種領域特定語言,提供了一種函數式定義控制器的途徑。

從 Spring 5.2 版本開始,函數式方法也將在 Spring Web MVC 框架中可用。 類似於 WebFlux 模塊,RouterFunctionsRouterFunction 是該 API 的主要抽象。

因此,我們首先導入 spring-boot-starter-web 依賴項

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

3. 路由器函數 vs. @控制器

在函數領域,Web 服務被稱作路由,而傳統的 @Controller 和 @RequestMapping 概念被 路由器函數 所取代。

為了創建我們的第一個服務,讓我們採用基於註解的服務,並看看它如何轉化為其功能等效版本。

我們將使用一個返回產品目錄中所有產品的服務的例子:

@RestController
public class ProductController {

    @RequestMapping("/product")
    public List<Product> productListing() {
        return ps.findAll();
    }
}

現在,讓我們來看一下它的功能等價物:

@Bean
public RouterFunction<ServerResponse> productListing(ProductService ps) {
    return route().GET("/product", req -> ok().body(ps.findAll()))
      .build();
}

3.1. 路由定義

需要注意的是,在功能式方法中,productListing() 方法返回一個 RouterFunction,而不是響應體。 這定義了路由,而不是請求的執行。

RouterFunction 包含路徑、請求頭、一個處理函數,該函數將用於生成響應體和響應頭。它可以包含單個或一組 Web 服務。

我們將在研究嵌套路由時,更詳細地介紹 Web 服務組。

在示例中,我們使用靜態的 route() 方法在 RouterFunction 中創建了一個 RouterFunction。 可以使用此方法提供路由的所有請求和響應屬性。

3.2. 請求謂詞

在我們的示例中,我們使用 GET() 方法在 route() 上指定這是一個 GET 請求,路徑以 String 形式提供。

我們還可以使用 RequestPredicate 來指定請求的更多細節。

例如,前一個示例中的路徑也可以使用 RequestPredicate 指定如下:

RequestPredicates.path("/product")

在這裏,我們使用了靜態實用工具 RequestPredicates 來創建一個 RequestPredicate 對象。

3.3. 響應

類似於 ServerResponse,它包含靜態實用方法,用於創建響應對象。

在我們的示例中,我們使用 ok() 將 HTTP 狀態碼 200 添加到響應頭中,然後使用 body() 來指定響應體。

此外,ServerResponse 還支持使用 EntityResponse 從自定義數據類型構建響應,還可以通過 RenderingResponse 使用 Spring MVC 的 ModelAndView

3.4. 註冊路由

接下來,我們使用 @Bean 註解將此路由註冊到應用程序上下文中:

@SpringBootApplication
public class SpringBootMvcFnApplication {

    @Bean
    RouterFunction<ServerResponse> productListing(ProductController pc, ProductService ps) {
        return pc.productListing(ps);
    }
}

現在,讓我們使用函數式方法來實施一些在開發 Web 服務時經常遇到的常見用例。

4. 嵌套路由

在應用程序中,通常會有大量的 Web 服務,並且這些服務會根據功能或實體進行邏輯分組。例如,我們可能希望所有與產品相關的服務從 product</em/> 開始。

讓我們為現有的 product</em/> 路徑添加另一個路徑,以根據名稱查找產品:

public RouterFunction<ServerResponse> productSearch(ProductService ps) {
    return route().nest(RequestPredicates.path("/product"), builder -> {
        builder.GET("/name/{name}", req -> ok().body(ps.findByName(req.pathVariable("name"))));
    }).build();
}

在傳統方法中,我們通過傳遞一個指向 @Controller 的路徑來實現。但是,組建 Web 服務的功能等效方法是 route() 方法上的 nest() 方法。

在這裏,我們首先提供想要組建新路由的路徑,即 /product。然後,我們使用構建器對象,如同在之前的示例中一樣,添加路由。

nest() 方法負責將添加到構建器對象中的路由與主 RouterFunction 合併。

5.   錯誤處理

另一個常見用例是使用自定義錯誤處理機制。我們可以使用 onError() 方法在 route() 上定義自定義異常處理器。

這相當於在註解式方法中使用的 @ExceptionHandler。但由於它可以為每個路由組定義單獨的異常處理器,因此它更具靈活性。

讓我們為我們之前創建的產品搜索路由添加一個異常處理器,以處理當產品未找到時拋出的自定義異常:

public RouterFunction<ServerResponse> productSearch(ProductService ps) {
    return route()...
      .onError(ProductService.ItemNotFoundException.class,
         (e, req) -> EntityResponse.fromObject(new Error(e.getMessage()))
           .status(HttpStatus.NOT_FOUND)
           .build())
      .build();
}

onError() 方法接受 Exception 類對象,並期望從功能實現中獲取一個 ServerResponse

我們使用了 EntityResponse,它是 ServerResponse 的子類型,用於在此處構建響應對象,該對象來自自定義數據類型 Error。然後我們添加狀態,並使用 EntityResponse.build(),它返回一個 ServerResponse 對象。

6. 過濾器

使用過濾器是一種常見的實現身份驗證以及管理諸如日誌記錄和審計等橫切關注點的途徑。 過濾器用於決定是否繼續或中止請求的處理。

例如,我們想要創建一個新的路由,將產品添加到目錄中:

public RouterFunction<ServerResponse> adminFunctions(ProductService ps) {
    return route().POST("/product", req -> ok().body(ps.save(req.body(Product.class))))
      .onError(IllegalArgumentException.class, 
         (e, req) -> EntityResponse.fromObject(new Error(e.getMessage()))
           .status(HttpStatus.BAD_REQUEST)
           .build())
        .build();
}

由於這是一個管理功能,因此我們也需要驗證調用服務的用户。

我們可以通過在 route() 中添加 filter() 方法來實現這一點。

public RouterFunction<ServerResponse> adminFunctions(ProductService ps) {
   return route().POST("/product", req -> ok().body(ps.save(req.body(Product.class))))
     .filter((req, next) -> authenticate(req) ? next.handle(req) : 
       status(HttpStatus.UNAUTHORIZED).build())
     ....;
}

在這裏,filter() 方法提供請求以及下一個處理程序,我們使用它進行簡單的身份驗證,如果驗證成功,則允許產品保存;如果驗證失敗,則返回 UNAUTHORIZED 錯誤給客户端。

7. 跨界關注點

有時,我們可能需要在請求之前、之後或周圍執行一些操作。例如,我們可能希望記錄傳入請求和傳出響應的某些屬性。

讓我們在應用程序找到匹配傳入請求時,每次都記錄一條語句。 我們將使用 before() 方法在 route() 上進行操作:

@Bean
RouterFunction<ServerResponse> allApplicationRoutes(ProductController pc, ProductService ps) {
    return route()...
      .before(req -> {
          LOG.info("Found a route which matches " + req.uri()
            .getPath());
          return req;
      })
      .build();
}

同樣,我們也可以在請求處理完成後,使用 route() 方法中的 after() 方法添加一個簡單的日誌語句:

@Bean
RouterFunction<ServerResponse> allApplicationRoutes(ProductController pc, ProductService ps) {
    return route()...
      .after((req, res) -> {
          if (res.statusCode() == HttpStatus.OK) {
              LOG.info("Finished processing request " + req.uri()
                  .getPath());
          } else {
              LOG.info("There was an error while processing request" + req.uri());
          }
          return res;
      })          
      .build();
    }

8. 結論

在本教程中,我們首先對使用函數式方法定義控制器進行了簡要介紹。然後,我們比較了 Spring MVC 註解與其函數式等效項。

接下來,我們實現了使用函數式控制器的簡單 Web 服務,該服務返回產品列表。

然後,我們繼續實現 Web 服務控制器的常見用例,包括嵌套路由、錯誤處理、添加訪問控制過濾器以及管理諸如日誌記錄等橫切關注點。

user avatar
0 位用戶收藏了這個故事!
收藏

發佈 評論

Some HTML is okay.