Spring 5 的新功能:函數式 Web 框架

工程 | Arjen Poutsma | 2016 年 9 月 22 日 | ...

如同昨天在 Juergen 的部落格文章 中所提及,Spring Framework 5.0 的第二個里程碑引入了一個新的函數式 Web 框架。在這篇文章中,我將提供更多關於此框架的資訊。

請記住,函數式 Web 框架是建立在我們在 M1 中提供的相同反應式基礎之上,並且我們也在此基礎上支援基於註解的(即 @Controller@RequestMapping)請求處理,請參閱 M1 部落格文章 以瞭解更多資訊。

範例

我們先從我們的 範例應用程式 中摘錄一些片段。以下是一個反應式儲存庫,公開了 Person 物件。它與傳統的、非反應式儲存庫非常相似,只是它返回 Flux<Person>,在傳統上您會返回 List<Person>,以及 Mono<Person>,在傳統上您會返回 PersonMono<Void> 用作完成訊號:指示何時完成儲存。有關這些 Reactor 類型的更多資訊,請參閱 Dave 的部落格文章

public interface PersonRepository {
  Mono<Person> getPerson(int id);
  Flux<Person> allPeople();
  Mono<Void> savePerson(Mono<Person> person);
}

以下是如何使用新的函數式 Web 框架公開該儲存庫

RouterFunction<?> route = route(GET("/person/{id}"),
  request -> {
    Mono<Person> person = Mono.justOrEmpty(request.pathVariable("id"))
      .map(Integer::valueOf)
      .then(repository::getPerson);
    return Response.ok().body(fromPublisher(person, Person.class));
  })
  .and(route(GET("/person"),
    request -> {
      Flux<Person> people = repository.allPeople();
      return Response.ok().body(fromPublisher(people, Person.class));
    }))
  .and(route(POST("/person"),
    request -> {
      Mono<Person> person = request.body(toMono(Person.class));
      return Response.ok().build(repository.savePerson(person));
    }));

以下是如何執行它,例如在 Reactor Netty 中

HttpHandler httpHandler = RouterFunctions.toHttpHandler(route);
ReactorHttpHandlerAdapter adapter =
  new ReactorHttpHandlerAdapter(httpHandler);
HttpServer server = HttpServer.create("localhost", 8080);
server.startAndAwait(adapter);

最後要做的是嘗試一下

$ curl 'https://127.0.0.1:8080/person/1'
{"name":"John Doe","age":42}

這裡有很多內容要涵蓋,所以讓我們深入探討!

主要組件

我將通過介紹其主要組件來解釋這個框架:HandlerFunctionRouterFunctionFilterFunction。這三個介面以及本文中描述的所有其他類型都可以在 org.springframework.web.reactive.function 套件中找到。

HandlerFunction

這個新框架的起點是 HandlerFunction<T>,它本質上是一個 Function<Request, Response<T>>,其中 RequestResponse 是新定義的、不可變的介面,它們為底層 HTTP 訊息提供了一個 JDK-8 友好的 DSL。有一個方便的建構器用於建構 Response 實例,與 ResponseEntity 中的建構器非常相似。 HandlerFunction 的註解對應物將是一個帶有 @RequestMapping 的方法。

以下是一個簡單的「Hello World」處理器函數的範例,它返回一個具有 200 狀態和基於字串的主體的響應

HandlerFunction<String> helloWorld =
  request -> Response.ok().body(fromObject("Hello World"));

正如我們在上面的範例中看到的,處理器函數通過建立在 Reactor 之上而完全是反應式的:它們接受 FluxMono 或任何其他 Reactive Streams Publisher 作為響應類型。

重要的是要注意 HandlerFunction 本身是無副作用的,因為它返回響應,而不是將其作為參數(參見 Servlet.service(ServletRequest,ServletResponse),它本質上是一個 BiConsumer<ServletRequest,ServletResponse>)。無副作用的函數有很多好處:它們更容易 測試、組合和最佳化

RouterFunction

傳入的請求通過 RouterFunction<T>(即 Function<Request, Optional<HandlerFunction<T>>>)路由到處理器函數。如果路由器函數匹配,則評估為處理器函數;否則,它返回一個空結果。 RouterFunction 的目的與 @RequestMapping 註解類似。但是,有一個重要的區別:使用註解,您的路由 僅限於可以通過註解值表達的內容,並且這些註解值的處理不容易覆蓋;使用路由器函數,處理代碼就在您眼前:您可以輕鬆地覆蓋或替換它。

以下是一個帶有內聯處理器函數的路由器函數範例。它看起來有點冗長,但不要擔心:我們將在下面找到使其更簡短的方法。

RouterFunction<String> helloWorldRoute = 
  request -> {
    if (request.path().equals("/hello-world")) {
      return Optional.of(r -> Response.ok().body(fromObject("Hello World")));
    } else {
      return Optional.empty();
    }
  };

通常,您不會編寫完整的路由器函數,而是(靜態地)匯入 RouterFunctions.route(),這允許您使用 RequestPredicate(即 Predicate<Request>)和 HandlerFunction 建立 RouterFunction。如果謂詞適用,則返回處理器函數;否則返回空結果。使用 route,我們可以將上面重寫為以下內容

RouterFunction<String> helloWorldRoute =
  RouterFunctions.route(request -> request.path().equals("/hello-world"),
    request -> Response.ok().body(fromObject("Hello World")));

您可以(靜態地)匯入 RequestPredicates.* 以存取常用的謂詞,例如基於路徑、HTTP 方法、內容類型等進行匹配的謂詞。有了它,我們可以使我們的 helloWorldRoute 更加簡單

RouterFunction<String> helloWorldRoute =
  RouterFunctions.route(RequestPredicates.path("/hello-world"),
    request -> Response.ok().body(fromObject("Hello World")));

組合函數

兩個路由器函數可以組合成一個新的路由器函數,該函數可以路由到任一處理器函數:如果第一個函數不匹配,則評估第二個函數。您可以通過調用 RouterFunction.and() 來組合兩個路由器函數,如下所示

RouterFunction<?> route =
  route(path("/hello-world"),
    request -> Response.ok().body(fromObject("Hello World")))
  .and(route(path("/the-answer"),
    request -> Response.ok().body(fromObject("42"))));

如果路徑匹配 /hello-world,則以上程式碼將響應「Hello World」,如果路徑匹配 /the-answer,則響應「42」。如果都不匹配,則返回一個空的 Optional。請注意,組合的路由器函數按順序評估,因此將特定函數放在通用函數之前是有意義的。

您還可以通過調用 andor 來組合請求謂詞。這些工作方式如預期:對於 and,如果兩個給定的謂詞都匹配,則生成的謂詞匹配,如果任一謂詞匹配,則 or 匹配。例如

RouterFunction<?> route =
  route(method(HttpMethod.GET).and(path("/hello-world")), 
    request -> Response.ok().body(fromObject("Hello World")))
  .and(route(method(HttpMethod.GET).and(path("/the-answer")), 
    request -> Response.ok().body(fromObject("42"))));

實際上,RequestPredicates 中找到的大多數謂詞都是組合!例如,RequestPredicates.GET(String)RequestPredicates.method(HttpMethod)RequestPredicates.path(String) 的組合。因此,我們可以將上面重寫為

RouterFunction<?> route =
  route(GET("/hello-world"),
    request -> Response.ok().body(fromObject("Hello World")))
  .and(route(GET("/the-answer"),
    request -> Response.ok().body(fromObject(42))));

方法參考

順便一提:到目前為止,我們已將所有處理器函數編寫為內聯 lambda。雖然這對於演示和簡短的範例來說很好,但它確實有變得「混亂」的趨勢,因為您混合了兩個關注點:請求路由和請求處理。因此,讓我們看看是否可以使事情更清晰。首先,我們建立一個包含處理代碼的類別

class DemoHandler {
  public Response<String> helloWorld(Request request) {
    return Response.ok().body(fromObject("Hello World"));
  }
  public Response<String> theAnswer(Request request) {
    return Response.ok().body(fromObject("42"));
  }
}

請注意,這兩個方法都具有與處理器函數相容的簽章。這使我們可以使用 方法參考

DemoHandler handler = new DemoHandler(); // or obtain via DI
RouterFunction<?> route =
  route(GET("/hello-world"), handler::helloWorld)
  .and(route(GET("/the-answer"), handler::theAnswer));

FilterFunction

通過調用 RouterFunction.filter(FilterFunction<T, R>) 可以過濾路由器函數映射的路由,其中 FilterFunction<T,R> 本質上是一個 BiFunction<Request, HandlerFunction<T>, Response<R>>。處理器函數參數表示鏈中的下一個項目:這通常是一個 HandlerFunction,但如果應用了多個過濾器,則也可以是另一個 FilterFunction。讓我們將日誌記錄過濾器添加到我們的路由中

RouterFunction<?> route =
  route(GET("/hello-world"), handler::helloWorld)
  .and(route(GET("/the-answer"), handler::theAnswer))
  .filter((request, next) -> {
    System.out.println("Before handler invocation: " + request.path());
    Response<?> response = next.handle(request);
    Object body = response.body();
    System.out.println("After handler invocation: " + body);
    return response;
  });

請注意,調用下一個處理器是可選的。這在安全性或快取場景中很有用(例如,僅當用戶具有足夠的權限時才調用 next)。

由於 route 是一個無界路由器函數,因此我們不知道下一個處理器將返回什麼類型的響應。這就是為什麼我們最終在我們的過濾器中得到一個 Response<?>,以及一個 Object 響應主體。在我們的處理器類別中,這兩個方法都返回 Response<String>,因此應該可以有一個 String 響應主體。我們可以通過使用 RouterFunction.andSame() 而不是 and() 來完成此操作。此組合方法要求參數路由器函數具有相同的類型。例如,我們可以使所有響應都大寫

RouterFunction<String> route =
  route(GET("/hello-world"), handler::helloWorld)
  .andSame(route(GET("/the-answer"), handler::theAnswer))
  .filter((request, next) -> {
    Response<String> response = next.handle(request);
    String newBody = response.body().toUpperCase();
    return Response.from(response).body(fromObject(newBody));
  });

使用註解,可以使用 @ControllerAdvice 和/或 ServletFilter 來實現類似的功能。

執行伺服器

所有這些都很好,但還缺少一個部分:我們實際上如何在 HTTP 伺服器中執行這些函數?答案不出所料,是通過調用另一個函數來實現的。您可以使用 RouterFunctions.toHttpHandler() 將路由器函數轉換為 HttpHandlerHttpHandler 是 Spring 5.0 M1 中引入的反應式抽象:它允許您在各種反應式運行時環境中運行:Reactor Netty、RxNetty、Servlet 3.1+ 和 Undertow。在範例中,我們已經展示了在 Reactor Netty 中運行 route 的樣子。對於 Tomcat,它看起來像這樣

HttpHandler httpHandler = RouterFunctions.toHttpHandler(route);
HttpServlet servlet = new ServletHttpHandlerAdapter(httpHandler);
Tomcat server = new Tomcat();
Context rootContext = server.addContext("",
  System.getProperty("java.io.tmpdir"));
Tomcat.addServlet(rootContext, "servlet", servlet);
rootContext.addServletMapping("/", "servlet");
tomcatServer.start();

需要注意的一點是,以上內容不依賴於 Spring 應用程式上下文。就像 JdbcTemplate 和其他 Spring 實用程式類別一樣,使用應用程式上下文是可選的:您可以在上下文中連接您的處理器和路由器函數,但這不是必需的。另請注意,您還可以將路由器函數轉換為 HandlerMapping,以便它可以與反應式 @Controllers 並排在 DispatcherHandler 中運行。

結論

這結束了對 Spring 新的函數式風格 Web 框架的介紹。讓我通過給出一個簡短的摘要來總結一下

  • 處理器函數通過返回響應來處理請求,
  • 路由器函數路由到處理器函數,並且可以與其他路由器函數組合,
  • 路由器函數可以通過過濾器函數進行過濾,
  • 路由器函數可以在反應式 Web 運行時環境中運行。

為了給您更完整的了解,我建立了一個使用函數式 Web 框架的簡單範例專案。您可以在 GitHub 上找到該專案。

請告訴我們您的想法!

取得 Spring 電子報

保持與 Spring 電子報的聯繫

訂閱

領先一步

VMware 提供培訓和認證,以加速您的進展。

瞭解更多

取得支援

Tanzu Spring 在一個簡單的訂閱中為 OpenJDK™、Spring 和 Apache Tomcat® 提供支援和二進位檔案。

瞭解更多

即將到來的活動

查看 Spring 社群中所有即將到來的活動。

檢視全部