Spring WebFlux 教程:如何構建反應式 Web 應用程序
- 2021 年 6 月 24 日
- 筆記
- reactor-stream
Spring WebFlux 教程:如何構建反應式 Web 應用程序
反應式系統提供了我們在高數據流世界中所需的無與倫比的響應能力和可擴展性。然而,反應式系統需要經過專門培訓的工具和開發人員來實現這些獨特的程序架構。Spring WebFlux with Project Reactor 是一個專門為滿足現代公司的響應式需求而構建的框架。
今天,我們將通過解釋 WebFlux 如何與其他反應式堆棧工具配合、有何不同以及如何製作您的第一個應用程序來幫助您開始使用 WebFlux。
什麼是反應式系統?
反應式系統是採用反應式架構模式設計的系統,該模式優先使用松耦合、靈活和可擴展的組件。它們的設計還考慮了故障解決方案,以確保即使出現故障,大部分系統仍能運行。
反應式系統專註於:
-
反應性:最重要的是,反應性系統應該對任何用戶輸入做出快速響應。反應式系統倡導者認為,反應式有助於優化系統的所有其他部分,從數據收集到用戶體驗。
-
彈性:反應式系統的設計應該能夠預測系統故障。反應式系統期望組件最終會失效,並設計鬆散耦合的系統,即使幾個單獨的部件停止工作也能保持活動狀態。
-
彈性:反應式系統應該通過擴大或縮小以滿足需求來適應工作負載的大小。許多反應式系統還將使用預測性擴展來預測和準備突然變化。實現彈性的關鍵是消除任何瓶頸並構建可以根據需要分片或複製組件的系統。
-
消息驅動的通信:反應式系統的所有組件都是鬆散耦合的,每個組件之間都有硬邊界。您的系統應該通過顯式消息傳遞跨越這些邊界進行通信。這些消息讓不同的組件了解故障,並幫助他們將工作流委派給可以處理它的組件。
反應式和其他 Web 模式之間最顯着的區別是反應式系統可以一次執行多個未阻塞的調用,而不是讓一些調用等待其他調用。因此,響應式系統可以提高性能和響應速度,因為 Web 應用程序的每個部分都可以比必須等待另一部分更快地完成自己的部分。
什麼是反應堆項目?
Project Reactor 是一個由 Pivotal 構建並由 Spring 提供支持的框架。它實現了反應式 API 模式,最著名的是反應式流規範。
如果您熟悉Java 8 Streams,您會很快發現 Stream 和 Flux(或其單元素版本 Mono)之間的許多相似之處。它們之間的主要區別在於 Fluxes 和 Monos 遵循一種publisher-subscriber模式並實現背壓,而 Stream API 則沒有。
背壓是數據端點向數據生產者發出信號,表明它接收了太多數據的一種方式。這允許更好的流量管理和分配,因為它可以防止單個組件過度工作。
使用 Reactor 的主要優點是您可以完全控制數據流。您可以依靠訂閱者在準備好處理信息時詢問更多信息的能力,或者在發佈者端緩衝一些結果,甚至使用沒有背壓的全推送方法。
在我們的反應式堆棧中,它位於 Spring Boot 2.0 和 WebFlux 之上:
示例反應式堆棧
堆棧:技術堆棧是用於創建 Web 或移動應用程序的軟件產品和編程語言的組合。反應式堆棧是相同的,但用於創建反應式應用程序。
什麼是 Spring WebFlux?
Spring WebFlux 是一個完全非阻塞、基於註解的 Web 框架,它構建在 Project Reactor 之上,它使得在 HTTP 層上構建響應式應用程序成為可能。WebFlux 使用新的路由器功能特性將函數式編程應用於 Web 層並繞過聲明性控制器和請求映射。WebFlux 要求您將 Reactor 作為核心依賴項導入。
WebFlux 作為Spring MVC的響應式替代品在 Spring 5 中添加,並增加了對以下內容的支持:
- 非阻塞線程:無需等待先前任務完成即可完成指定任務的並發線程。
- Reactive Stream API:一種標準化工具,包括用於非阻塞背壓的異步流處理選項。
- 異步數據處理:當數據在後台處理並且用戶可以不間斷地繼續使用正常的應用程序功能時。
最終WebFlux摒棄了SpringMVC的多請求線程模型,而是使用多EventLoop非阻塞模型來啟用反應式、可擴展的應用程序。由於支持Netty、Undertow 和Servlet 3.1+ 容器等流行服務器,WebFlux 已成為反應式堆棧的關鍵部分。
Router功能
RouterFunction是標準springmvc中使用的@RequestMapping和@Controller注釋樣式的一種功能替代。
我們可以使用它將請求路由到處理程序函數:
- 傳統的路由定義
@RestController
public class ProductController {
@RequestMapping("/product")
public List<Product> productListing() {
return ps.findAll();
}
}
- 函數式定義
@Bean
public RouterFunction<ServerResponse> productListing(ProductService ps) {
return route().GET("/product", req -> ok().body(ps.findAll()))
.build();
}
你可以使用RouterFunctions.route()來創建路由,而不是編寫完整的路由器函數。路由註冊為spring的bean,因此可以在任何配置類中創建。 路由器功能避免了由請求映射的多步驟過程引起的潛在副作用,而是將其簡化為直接的路由器/處理程序鏈。這允許函數式編程實現反應式編程。
RequestMapping和Controller注釋樣式在WebFlux中仍然有效如果您對舊樣式更熟悉,RouterFunctions只是解決方案的一個新選項。
WebClient 詳解
項目中經常用到發送Http請求的客戶端,如果你使用webflux那非常簡單去創建一個Http請求。WebClient是WebFlux的反應式web客戶端,它是從著名的rest模板構建的。它是一個接口,表示web請求的主要入口點,並支持同步和異步操作。WebClient主要用於反應式後端到後端通信。
您可以通過使用Maven導入標準WebFlux依賴項來構建和創建WebClient實例:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
創建實例
WebClient webClient = WebClient.create();
// 如果是調用特定服務的API,可以在初始化webclient 時使用,baseUrl
WebClient webClient = WebClient.create("//github.com/1ssqq1lxr");
或者構造器方式初始化
WebClient webClient1 = WebClient.builder()
.baseUrl("//github.com/1ssqq1lxr")
.defaultHeader(HttpHeaders.CONTENT_TYPE, "application/vnd.github.v3+json")
.defaultHeader(HttpHeaders.USER_AGENT, "Spring 5 WebClient")
.build();
- Get請求
Mono<String> resp = WebClient.create()
.method(HttpMethod.GET)
.uri("//github.com/1ssqq1lxr")
.cookie("token","xxxx")
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.retrieve().bodyToMono(String.class);
- Post請求(表單)
MultiValueMap<String, String> formData = new LinkedMultiValueMap();
formData.add("name1","value1");
formData.add("name2","value2");
Mono<String> resp = WebClient.create().post()
.uri("//www.w3school.com.cn/test/demo_form.asp")
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
.body(BodyInserters.fromFormData(formData))
.retrieve().bodyToMono(String.class);
- Post請求(Body)
Book book = new Book();
book.setName("name");
book.setTitle("this is title");
Mono<String> resp = WebClient.create().post()
.uri("//github.com/1ssqq1lxr")
.contentType(MediaType.APPLICATION_JSON_UTF8)
.body(Mono.just(book),Book.class)
.retrieve().bodyToMono(String.class);
- 文件上傳
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.IMAGE_PNG);
HttpEntity<ClassPathResource> entity = new HttpEntity<>(new ClassPathResource("parallel.png"), headers);
MultiValueMap<String, Object> parts = new LinkedMultiValueMap<>();
arts.add("file", entity);
Mono<String> resp = WebClient.create().post()
.uri("//localhost:8080/upload")
.contentType(MediaType.MULTIPART_FORM_DATA)
.body(BodyInserters.fromMultipartData(parts))
.retrieve().bodyToMono(String.class);
Reactive Steam API
下篇文章給大家詳細講下Reactor3的API
Reactive Stream API是一個的函數集合,允許更智能的流數據流。它內置了對背壓和異步處理的支持,確保應用程序最有效地利用計算機和組件資源。
反應流API有四個主要接口:
-
Publisher:根據鏈接訂閱者的需求向他們發佈事件。充當訂戶可以監視事件的中心鏈接點。
-
Subscriber:接收和處理發佈服務器發出的事件。多個訂閱服務器可以鏈接到單個發佈服務器,並對同一事件做出不同的響應。訂戶可以設置為反應:
-
onNext,當它接收到下一個事件時。
-
onSubscribe,添加新訂閱時
-
onError,當另一個訂閱服務器發生錯誤時
-
onComplete,當一個訂閱完成時
-
Server容器
WebFlux在Tomcat、Jetty、servlet3.1+容器以及Netty和Undertow等非Servlet運行時上都受支持。Netty最常用於異步和非阻塞設計,因此WebFlux將默認使用它。只需對Maven或Gradle構建軟件進行簡單的更改,就可以輕鬆地在這些服務器選項之間切換。
這使得WebFlux在它可以使用的技術方面具有高度的通用性,並允許您使用現有的基礎設施輕鬆地實現它。
並發模型
WebFlux是以無阻塞的思想構建的,因此使用了與springmvc不同的並發編程模型。
springmvc假設線程將被阻塞,並在阻塞實例期間使用一個大的線程池來保持移動。這個更大的線程池使得MVC資源更密集,因為計算機硬件必須同時保持更多的線
WebFlux使用了一個小的線程池,因為它假設您永遠不需要通過工作來避免阻塞。這些線程稱為事件循環工作線程,數量固定,在傳入請求中的循環速度比MVC線程快。這意味着WebFlux更有效地使用計算機資源,因為活動線程總是在工作。
Spring WebFlux Security
WebFlux使用Spring安全性來實現身份驗證和授權協議。springsecurity使用WebFilter根據經過身份驗證的用戶列表認證請求。
@EnableWebFluxSecurity
public class HelloWebFluxSecurityConfig {
@Bean
public MapReactiveUserDetailsService userDetailsService() {
UserDetails user = User.withDefaultPasswordEncoder()
.username("user")
.password("user")
.roles("USER")
.build();
return new MapReactiveUserDetailsService(user);
}
}
在這裡,我們可以看到用戶有一個用戶名、一個密碼和一個或多個roles標籤,這些標籤允許自定義定訪問。類似於SpringBoot Security的 UserDetailsService接口
開始使用 Spring WebFlux
生成項目
參考配置
生成後的pom如下
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="//maven.apache.org/POM/4.0.0" xmlns:xsi="//www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="//maven.apache.org/POM/4.0.0 //maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.1</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.github.webflux.learn</groupId>
<artifactId>demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>demo</name>
<description>Demo project for Spring Boot</description>
<properties>
<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>
<optional>true</optional>
</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>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
開發接口
自定義一個函數路由:將請求path中的佔位參數獲取作為返回值
/**
* @author coding途中
*/
@Configuration
public class TestRouter {
@Bean
public RouterFunction<ServerResponse> routeExample() {
return RouterFunctions
.route(RequestPredicates.GET("/hello/{path}").and(RequestPredicates.accept(MediaType.TEXT_PLAIN)), serverRequest -> {
String str = serverRequest.pathVariable("path");
return ServerResponse.ok().contentType(MediaType.TEXT_PLAIN).bodyValue(str)
.switchIfEmpty(ServerResponse.notFound().build());
});
}
}
瀏覽器請求 //localhost:4990/hello/haha
haha
添加認證
/**
* @author coding途中
*/
@Configuration
@EnableWebFluxSecurity
public class HelloWebfluxSecurityConfig {
@Bean
public MapReactiveUserDetailsService userDetailsService() {
UserDetails user = User.withDefaultPasswordEncoder()
.username("user")
.password("user")
.roles("USER")
.build();
return new MapReactiveUserDetailsService(user);
}
@Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
// @formatter:off
return http.authorizeExchange()
.pathMatchers( "/hello/**").authenticated()
.pathMatchers("/hello/login").permitAll()
.anyExchange().authenticated()
.and()
.formLogin().and()
.logout().and()
.httpBasic().and()
.csrf().disable()
.build();
}
}
- 再次請求接口 瀏覽器請求 //localhost:4990/hello/haha 此時瀏覽重定向到 //localhost:4990/login
登陸頁面
輸入user/user 用戶名密碼後完成登陸。
再次瀏覽器請求 //localhost:4990/hello/authenticate
authenticate
結束
識別下方二維碼!回復:
入群
,掃碼加入我們交流群!
點贊是認可,在看是支持