Spring 5 新特性:函式式 Web 框架

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

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

請記住,函式式 Web 框架是建立在我們 M1 版本提供的相同響應式基礎之上的,在該基礎上我們也支援基於註解(即 @Controller@RequestMapping)的請求處理,更多相關資訊請參閱M1 部落格文章

示例

我們先從我們的示例應用程式中摘錄一些片段。下面是一個響應式倉庫,它暴露了 Person 物件。它與傳統的、非響應式倉庫非常相似,不同之處在於,傳統倉庫返回 List<Person> 的地方,它返回 Flux<Person>;傳統倉庫返回 Person 的地方,它返回 Mono<Person>Mono<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://: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 且基於 String 的響應體

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,以便它可以在 DispatcherHandler 中執行(可能與響應式 @Controllers 並行執行)。

結論

至此,Spring 新函式式 Web 框架的介紹結束。讓我總結一下要點:

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

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

請告訴我們你的想法!

訂閱 Spring 資訊

保持與 Spring 資訊的連線

訂閱

先行一步

VMware 提供培訓和認證,助你加速進步。

瞭解更多

獲得支援

Tanzu Spring 透過一項簡單的訂閱即可為 OpenJDK™、Spring 和 Apache Tomcat® 提供支援和二進位制檔案。

瞭解更多

即將舉行的活動

檢視 Spring 社群中所有即將舉行的活動。

檢視全部