實戰Spring Boot 2.0 Reactive程式設計系列 - WebFlux初體驗

零壹技術棧發表於2018-07-02

前言

上文引入了 反應式程式設計模型 相關概念,對 Spring Reactor 的核心 API 進行了簡單歸納。本文會對 Spring 5 WebFlux 進行相關介紹,包括引入 Servlet 3.1 +,各個功能元件 Router FunctionsWebFluxReactive Streams 等,以及如何在 Spring Boot 2.0 中分別以 全域性功能路由MVC 控制器 的方式配置 HTTP 請求處理。

實戰Spring Boot 2.0 Reactive程式設計系列 - WebFlux初體驗

正文

Spring 5 WebFlux介紹

關於 Spring 5WebFlux 響應式程式設計,下圖是傳統 Spring Web MVC 結構以及Spring 5 中新增加的基於 Reactive StreamsSpring WebFlux 框架。可以使用 webFlux 模組來構建 非同步的非堵塞的事件驅動 的服務,其在 伸縮性方面 表現非常好。

實戰Spring Boot 2.0 Reactive程式設計系列 - WebFlux初體驗

如圖所示,WebFlux 模組從上到下依次是 Router FunctionsWebFluxReactive Streams 三個新元件。

Servlet 3.1+ API介紹

WebFlux 模組需要執行在實現了 Servlet 3.1+ 規範 的容器之上。Servlet 3.1 規範中新增了對 非同步處理 的支援,在新的 Servlet 規範中,Servlet 執行緒不需要一直 阻塞等待 到業務處理完成。

Servlet 3.1 中,其請求處理的執行緒模型大致如下:

  1. Servlet 執行緒接收到新的請求後,不需要等待業務處理完成再進行結果輸出,而是將這個請求委託給另外一個執行緒(業務執行緒)來完成。

  2. Servlet 執行緒將委託完成之後變返回到容器中去接收新的請求。

Servlet 3.1 規範特別適用於那種 業務處理非常耗時 的場景之下,可以減少 伺服器資源 的佔用,並且提高 併發處理速度 ,而對於那些能 快速響應 的場景收益並不大。

所以 WebFlux 支援的容器有 TomcatJetty同步容器 ,也可以是 NettyUndertow 這類 非同步容器。在容器中 Spring WebFlux 會將 輸入流 適配成 MonoFlux 格式進行統一處理。

Spring WebFlux的功能模組

下面介紹上圖中 WebFlux 各個模組:

1. Router Functions

對標準的 @Controller@RequestMapping等的 Spring MVC 註解,提供一套 函式式風格API,用於建立 RouterHandlerFilter

2. WebFlux

核心元件,協調上下游各個元件提供 響應式程式設計 支援。

3. Reactive Streams

一種支援 背壓 (Backpressure)非同步資料流處理標準,主流實現有 RxJavaReactorSpring WebFlux 整合的是Reactor

Flux

FluxMono 屬於 事件釋出者,類似於 生產者,對消費者 提供訂閱介面。當有事件發生的時候,FluxMono 會回撥 消費者相應的方法來通知 消費者 相應的事件。

下面這張圖是 Flux 的工作流程圖:

實戰Spring Boot 2.0 Reactive程式設計系列 - WebFlux初體驗

關於 Flux 的工作模式,可以看出 Flux 可以 觸發 (emit) 很多 item,而這些 item 可以經過若干 Operators 然後才被 subscribe,下面是使用 Flux 的一個例子:

Flux.fromIterable(getSomeLongList())
    .mergeWith(Flux.interval(100))
    .doOnNext(serviceA::someObserver)
    .map(d -> d * 2)
    .take(3)
    .onErrorResumeWith(errorHandler::fallback)
    .doAfterTerminate(serviceM::incrementTerminate)
    .subscribe(System.out::println);
複製程式碼

Mono

下面的圖片是 Mono 的處理流程,可以很直觀的看出來 MonoFlux 的區別:

實戰Spring Boot 2.0 Reactive程式設計系列 - WebFlux初體驗

Mono 只能觸發 (emit) 一個 item,下面是使用 Mono 的一個例子:

Mono.fromCallable(System::currentTimeMillis)
    .flatMap(time -> Mono.first(serviceA.findRecent(time), serviceB.findRecent(time)))
    .timeout(Duration.ofSeconds(3), errorHandler::fallback)
    .doOnSuccess(r -> serviceM.incrementSuccess())
    .subscribe(System.out::println);
複製程式碼

Spring Boot 2.0 Reactive Stack

Spring Boot Webflux 就是基於 Reactor 實現的。Spring Boot 2.0 包括一個新的 spring-webflux 模組。該模組包含對 響應式 HTTPWebSocket 客戶端的支援,以及對 RESTHTMLWebSocket 互動等程式 的支援。一般來說,Spring MVC 用於 同步處理Spring Webflux 用於 非同步處理

實戰Spring Boot 2.0 Reactive程式設計系列 - WebFlux初體驗

如上圖所示,從 Web 表現層到資料訪問,再到容器,Spring Boot 2.0 同時提供了 同步阻塞式非同步非阻塞式 兩套完整的 API Stack

從上而下對比以下兩者的區別:

API Stack Sevlet Stack Reactive Stack
Web控制層 Spring MVC Spring WebFlux
安全認證層 Spring Security Spring Security
資料訪問層 Spring Data Repositories Spring Data Reactive Repositories
容器API Servlet API Reactive Streams Adapters
內嵌容器 Servlet Containers Netty, Servlet 3.1+ Containers

適用場景

控制層一旦使用 Spring WebFlux,它下面的安全認證層、資料訪問層都必須使用 Reactive API。其次,Spring Data Reactive Repositories 目前只支援 MongoDBRedisCouchbase 等幾種不支援事務管理的 NOSQL。技術選型時一定要權衡這些弊端和風險,比如:

  1. Spring MVC 能滿足場景的,就不需要更改為 Spring WebFlux

  2. 要注意容器的支援,可以看看底層 內嵌容器 的支援。

  3. 微服務 體系結構,Spring WebFluxSpring MVC 可以混合使用。尤其開發 IO 密集型 服務的時候,可以選擇 Spring WebFlux 去實現。

程式設計模型

Spring 5 Web 模組包含了 Spring WebFluxHTTP 抽象。類似 Servlet APIWebFlux 提供了 WebHandler API 去定義 非阻塞 API 抽象介面。可以選擇以下兩種程式設計模型實現:

  1. 註解控制層:MVC 保持一致,WebFlux 也支援 響應性 @RequestBody 註解。

  2. 功能性端點: 基於 lambda 輕量級程式設計模型,用來 建立路由處理請求 的工具。和上面最大的區別就是,這種模型,全程 控制了 請求 - 響應 的生命流程

內嵌容器

Spring Boot 大框架一樣啟動應用,但 Spring WebFlux 預設是通過 Netty 啟動,並且自動設定了 預設埠8080。另外還提供了對 JettyUndertow 等容器的支援。開發者自行在新增對應的容器 Starter 元件依賴,即可配置並使用對應 內嵌容器例項

注意: 必須是 Servlet 3.1+ 容器,如 Tomcat、Jetty;或者非 Servlet 容器,如 Netty 和 Undertow。

Starter 元件

Spring Boot 大框架一樣,Spring Boot Webflux 提供了很多 開箱即用Starter 元件。新增 spring-boot-starter-webflux 依賴,就可用於構建 響應式 API 服務,其包含了 WebFluxTomcat 內嵌容器 等。

快速開始

Spring Initializr構建專案骨架

利用 Spring Initializer 快速生成 Spring Boot 應用,配置專案資訊並設定依賴。

配置Maven依賴

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.0.0.RELEASE</version>
    <relativePath/> <!-- lookup parent from repository -->
</parent>

<properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    <java.version>1.8</java.version>
</properties>

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

    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>io.projectreactor</groupId>
        <artifactId>reactor-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
        </plugin>
    </plugins>
</build>
複製程式碼

Spring Boot啟動類

@SpringBootApplication
public class DemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}
複製程式碼

配置實體類

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class Message {
    String body;
}
複製程式碼

1. MVC控制器方式

1.1. 編寫控制器

@RestController
@RequestMapping
public class MessageController {
    @GetMapping
    public Flux<Message> allMessages(){
        return Flux.just(
            Message.builder().body("hello Spring 5").build(),
            Message.builder().body("hello Spring Boot 2").build()
        );
    }  
}
複製程式碼

1.2. 編寫測試類

@RunWith(SpringRunner.class)
@WebFluxTest(controllers = MessageController.class)
public class DemoApplicationTests {
    @Autowired
    WebTestClient client;

    @Test
    public void getAllMessagesShouldBeOk() {
        client.get().uri("/").exchange().expectStatus().isOk();
    }
}
複製程式碼

1.3. 檢視啟動日誌

2018-05-27 17:37:23.550  INFO 67749 --- [           main] s.w.r.r.m.a.RequestMappingHandlerMapping : Mapped "{[],methods=[GET]}" onto reactor.core.publisher.Flux<com.example.demo.Message> com.example.demo.MessageController.allMessages()
2018-05-27 17:37:23.998  INFO 67749 --- [ctor-http-nio-1] r.ipc.netty.tcp.BlockingNettyContext     : Started HttpServer on /0:0:0:0:0:0:0:0:8080
2018-05-27 17:37:23.999  INFO 67749 --- [           main] o.s.b.web.embedded.netty.NettyWebServer  : Netty started on port(s): 8080
2018-05-27 17:37:24.003  INFO 67749 --- [           main] com.example.demo.DemoApplication         : Started DemoApplication in 1.6 seconds (JVM running for 2.274)
複製程式碼

從日誌裡可以看出:

  1. 啟動時 WebFlux 利用 MVC 原生的 RequestMappingHandlerMapping 將控制器裡的 請求路徑MVC 中的 處理器 進行繫結。
  2. Spring WebFlux 預設採用 Netty 作為 內嵌容器,且啟動埠預設為 8080

訪問 http://localhost:8080,返回結果如下:

實戰Spring Boot 2.0 Reactive程式設計系列 - WebFlux初體驗

2. 全域性Router API方式

2.1. 配置全域性Router Bean

@Configuration
public class DemoRouterConfig {
    @Bean
    public RouterFunction<ServerResponse> routes() {
        return route(GET("/"), (ServerRequest req)-> ok()
                .body(
                    BodyInserters.fromObject(
                        Arrays.asList(
                            Message.builder().body("hello Spring 5").build(),
                            Message.builder().body("hello Spring Boot 2").build()
                        )
                    )
                )
        );
    }
}
複製程式碼

2.2. 編寫測試類

@RunWith(SpringRunner.class)
@WebFluxTest
public class DemoApplicationTests {    
    @Autowired
    WebTestClient client;
    
    @Test
    public void getAllMessagesShouldBeOk() {
        client.get().uri("/").exchange().expectStatus().isOk();
    }
}
複製程式碼

2.3. 檢視啟動日誌

執行 Spring Boot 啟動入口類,啟動日誌如下(不重要的省略):

2018-05-27 17:20:28.870  INFO 67696 --- [           main] o.s.w.r.f.s.s.RouterFunctionMapping      : Mapped (GET && /) -> com.example.demo.DemoRouterConfig$$Lambda$213/1561745898@3381b4fc
2018-05-27 17:20:28.931  INFO 67696 --- [           main] o.s.w.r.r.m.a.ControllerMethodResolver   : Looking for @ControllerAdvice: org.springframework.boot.web.reactive.context.AnnotationConfigReactiveWebServerApplicationContext@1460a8c0: startup date [Sun May 27 17:20:27 CST 2018]; root of context hierarchy
2018-05-27 17:20:29.311  INFO 67696 --- [ctor-http-nio-1] r.ipc.netty.tcp.BlockingNettyContext     : Started HttpServer on /0:0:0:0:0:0:0:0:8080
2018-05-27 17:20:29.312  INFO 67696 --- [           main] o.s.b.web.embedded.netty.NettyWebServer  : Netty started on port(s): 8080
2018-05-27 17:20:29.316  INFO 67696 --- [           main] com.example.demo.DemoApplication         : Started DemoApplication in 2.137 seconds (JVM running for 3.169)
複製程式碼

從日誌裡可以看出:啟動時 WebFlux 利用 RouterFunctionMappingRouterFunction 裡的 全域性路徑請求處理 進行了對映和繫結。

訪問 http://localhost:8080,返回結果如下:

實戰Spring Boot 2.0 Reactive程式設計系列 - WebFlux初體驗

可以看出,無論是使用 Fucntional Router 還是 MVC Controller,都可以產生相同的效果!

開發執行環境

  • JDK 1.8 + : Spring Boot 2.x 要求 JDK 1.8 環境及以上版本。另外,Spring Boot 2.x 只相容 Spring Framework 5.0 及以上版本。

  • Maven 3.2 + : 為 Spring Boot 2.x 提供了相關依賴構建工具是 Maven,版本需要 3.2 及以上版本。使用 Gradle 則需要 1.12 及以上版本。MavenGradle 大家各自挑選下喜歡的就好。

小結

本文首先對 Spring 5 WebFlux 進行了單獨介紹,包括引入 Servlet 3.1 +,各個功能元件 Router FunctionsWebFluxReactive Streams 等。然後在 Spring Boot 2.0 詳細地介紹了 Reactive StackServlet Stack 的組成區別,並分別給出了 WebFlux 基於 全域性功能路由控制器 的配置和使用案例。


歡迎關注技術公眾號: 零壹技術棧

零壹技術棧

本帳號將持續分享後端技術乾貨,包括虛擬機器基礎,多執行緒程式設計,高效能框架,非同步、快取和訊息中介軟體,分散式和微服務,架構學習和進階等學習資料和文章。

相關文章