前言
上文引入了 反應式程式設計模型 相關概念,對 Spring Reactor
的核心 API
進行了簡單歸納。本文會對 Spring 5 WebFlux
進行相關介紹,包括引入 Servlet 3.1 +
,各個功能元件 Router Functions
、WebFlux
和 Reactive Streams
等,以及如何在 Spring Boot 2.0
中分別以 全域性功能路由 和 MVC
控制器 的方式配置 HTTP
請求處理。
正文
Spring 5 WebFlux介紹
關於 Spring 5
的 WebFlux
響應式程式設計,下圖是傳統 Spring Web MVC
結構以及Spring 5
中新增加的基於 Reactive Streams
的 Spring WebFlux
框架。可以使用 webFlux
模組來構建 非同步的、非堵塞的、事件驅動 的服務,其在 伸縮性方面 表現非常好。
如圖所示,WebFlux
模組從上到下依次是 Router Functions
、WebFlux
、Reactive Streams
三個新元件。
Servlet 3.1+ API介紹
WebFlux
模組需要執行在實現了 Servlet 3.1+
規範 的容器之上。Servlet 3.1
規範中新增了對 非同步處理 的支援,在新的 Servlet
規範中,Servlet
執行緒不需要一直 阻塞等待 到業務處理完成。
在 Servlet 3.1
中,其請求處理的執行緒模型大致如下:
-
Servlet
執行緒接收到新的請求後,不需要等待業務處理完成再進行結果輸出,而是將這個請求委託給另外一個執行緒(業務執行緒)來完成。 -
Servlet
執行緒將委託完成之後變返回到容器中去接收新的請求。
Servlet 3.1
規範特別適用於那種 業務處理非常耗時 的場景之下,可以減少 伺服器資源 的佔用,並且提高 併發處理速度 ,而對於那些能 快速響應 的場景收益並不大。
所以 WebFlux
支援的容器有 Tomcat
、Jetty
等 同步容器 ,也可以是 Netty
和 Undertow
這類 非同步容器。在容器中 Spring WebFlux
會將 輸入流 適配成 Mono
或 Flux
格式進行統一處理。
Spring WebFlux的功能模組
下面介紹上圖中 WebFlux
各個模組:
1. Router Functions
對標準的 @Controller
,@RequestMapping
等的 Spring MVC
註解,提供一套 函式式風格 的 API
,用於建立 Router
、Handler
和Filter
。
2. WebFlux
核心元件,協調上下游各個元件提供 響應式程式設計 支援。
3. Reactive Streams
一種支援 背壓 (Backpressure)
的 非同步資料流處理標準,主流實現有 RxJava
和 Reactor
,Spring WebFlux
整合的是Reactor
。
Flux
Flux
和 Mono
屬於 事件釋出者,類似於 生產者,對消費者 提供訂閱介面。當有事件發生的時候,Flux
或 Mono
會回撥 消費者相應的方法來通知 消費者 相應的事件。
下面這張圖是 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
的處理流程,可以很直觀的看出來 Mono
和 Flux
的區別:
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
模組。該模組包含對 響應式 HTTP
和 WebSocket
客戶端的支援,以及對 REST
、HTML
和 WebSocket
互動等程式 的支援。一般來說,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
目前只支援 MongoDB
、Redis
和 Couchbase
等幾種不支援事務管理的 NOSQL
。技術選型時一定要權衡這些弊端和風險,比如:
-
Spring MVC
能滿足場景的,就不需要更改為Spring WebFlux
。 -
要注意容器的支援,可以看看底層 內嵌容器 的支援。
-
微服務 體系結構,
Spring WebFlux
和Spring MVC
可以混合使用。尤其開發IO
密集型 服務的時候,可以選擇Spring WebFlux
去實現。
程式設計模型
Spring 5 Web
模組包含了 Spring WebFlux
的 HTTP
抽象。類似 Servlet API
, WebFlux
提供了 WebHandler API
去定義 非阻塞 API
抽象介面。可以選擇以下兩種程式設計模型實現:
-
註解控制層: 和
MVC
保持一致,WebFlux
也支援 響應性@RequestBody
註解。 -
功能性端點: 基於
lambda
輕量級程式設計模型,用來 建立路由 和 處理請求 的工具。和上面最大的區別就是,這種模型,全程 控制了 請求 - 響應 的生命流程
內嵌容器
跟 Spring Boot
大框架一樣啟動應用,但 Spring WebFlux
預設是通過 Netty
啟動,並且自動設定了 預設埠 為 8080
。另外還提供了對 Jetty
、Undertow
等容器的支援。開發者自行在新增對應的容器 Starter
元件依賴,即可配置並使用對應 內嵌容器例項。
注意: 必須是 Servlet 3.1+ 容器,如 Tomcat、Jetty;或者非 Servlet 容器,如 Netty 和 Undertow。
Starter 元件
跟 Spring Boot
大框架一樣,Spring Boot Webflux
提供了很多 開箱即用 的 Starter
元件。新增 spring-boot-starter-webflux
依賴,就可用於構建 響應式 API
服務,其包含了 WebFlux
和 Tomcat
內嵌容器 等。
快速開始
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)
複製程式碼
從日誌裡可以看出:
- 啟動時
WebFlux
利用MVC
原生的RequestMappingHandlerMapping
將控制器裡的 請求路徑 和MVC
中的 處理器 進行繫結。 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
利用 RouterFunctionMapping
將 RouterFunction
裡的 全域性路徑 和 請求處理 進行了對映和繫結。
訪問 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
及以上版本。Maven
和Gradle
大家各自挑選下喜歡的就好。
小結
本文首先對 Spring 5 WebFlux
進行了單獨介紹,包括引入 Servlet 3.1 +
,各個功能元件 Router Functions
、WebFlux
和 Reactive Streams
等。然後在 Spring Boot 2.0
詳細地介紹了 Reactive Stack
和 Servlet Stack
的組成區別,並分別給出了 WebFlux
基於 全域性功能路由 和 控制器 的配置和使用案例。
歡迎關注技術公眾號: 零壹技術棧
本帳號將持續分享後端技術乾貨,包括虛擬機器基礎,多執行緒程式設計,高效能框架,非同步、快取和訊息中介軟體,分散式和微服務,架構學習和進階等學習資料和文章。