實戰Spring Boot 2.0 Reactive編程系列 - WebFlux初體驗

前言

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

正文

Spring 5 WebFlux介紹

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

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

Servlet 3.1+ API介紹

WebFlux 模塊須要運行在實現了 Servlet 3.1+ 規範 的容器之上。Servlet 3.1 規範中新增了對 異步處理 的支持,在新的 Servlet 規範中,Servlet 線程不須要一直 阻塞等待 到業務處理完成。spring

Servlet 3.1 中,其請求處理的線程模型大體以下:編程

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

  2. Servlet 線程將委託完成以後變返回到容器中去接收新的請求。緩存

Servlet 3.1 規範特別適用於那種 業務處理很是耗時 的場景之下,能夠減小 服務器資源 的佔用,而且提升 併發處理速度 ,而對於那些能 快速響應 的場景收益並不大。安全

因此 WebFlux 支持的容器有 TomcatJetty同步容器 ,也能夠是 NettyUndertow 這類 異步容器。在容器中 Spring WebFlux 會將 輸入流 適配成 MonoFlux 格式進行統一處理。bash

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 的工做流程圖:

關於 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 的區別:

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 用於 異步處理

如上圖所示,從 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,返回結果以下:

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,返回結果以下:

能夠看出,不管是使用 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 基於 全局功能路由控制器 的配置和使用案例。


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

零壹技術棧

本賬號將持續分享後端技術乾貨,包括虛擬機基礎,多線程編程,高性能框架,異步、緩存和消息中間件,分佈式和微服務,架構學習和進階等學習資料和文章。

相關文章
相關標籤/搜索