新一代Spring Web框架WebFlux!
反應(yīng)式系統(tǒng)提供了我們?cè)诟邤?shù)據(jù)流世界中所需的無(wú)與倫比的響應(yīng)能力和可擴(kuò)展性。然而,反應(yīng)式系統(tǒng)需要經(jīng)過(guò)專(zhuān)門(mén)培訓(xùn)的工具和開(kāi)發(fā)人員來(lái)實(shí)現(xiàn)這些獨(dú)特的程序架構(gòu)。Spring WebFlux with Project Reactor 是一個(gè)專(zhuān)門(mén)為滿(mǎn)足現(xiàn)代公司的響應(yīng)式需求而構(gòu)建的框架。
今天,我們將通過(guò)解釋 WebFlux 如何與其他反應(yīng)式堆棧工具配合、有何不同以及如何制作您的第一個(gè)應(yīng)用程序來(lái)幫助您開(kāi)始使用 WebFlux。
什么是反應(yīng)式系統(tǒng)?
反應(yīng)式系統(tǒng)是采用反應(yīng)式架構(gòu)模式設(shè)計(jì)的系統(tǒng),該模式優(yōu)先使用松耦合、靈活和可擴(kuò)展的組件。它們的設(shè)計(jì)還考慮了故障解決方案,以確保即使出現(xiàn)故障,大部分系統(tǒng)仍能運(yùn)行。
反應(yīng)式系統(tǒng)專(zhuān)注于:
- 反應(yīng)性:最重要的是,反應(yīng)性系統(tǒng)應(yīng)該對(duì)任何用戶(hù)輸入做出快速響應(yīng)。反應(yīng)式系統(tǒng)倡導(dǎo)者認(rèn)為,反應(yīng)式有助于優(yōu)化系統(tǒng)的所有其他部分,從數(shù)據(jù)收集到用戶(hù)體驗(yàn)。
- 彈性:反應(yīng)式系統(tǒng)的設(shè)計(jì)應(yīng)該能夠預(yù)測(cè)系統(tǒng)故障。反應(yīng)式系統(tǒng)期望組件最終會(huì)失效,并設(shè)計(jì)松散耦合的系統(tǒng),即使幾個(gè)單獨(dú)的部件停止工作也能保持活動(dòng)狀態(tài)。
- 彈性:反應(yīng)式系統(tǒng)應(yīng)該通過(guò)擴(kuò)大或縮小以滿(mǎn)足需求來(lái)適應(yīng)工作負(fù)載的大小。許多反應(yīng)式系統(tǒng)還將使用預(yù)測(cè)性擴(kuò)展來(lái)預(yù)測(cè)和準(zhǔn)備突然變化。實(shí)現(xiàn)彈性的關(guān)鍵是消除任何瓶頸并構(gòu)建可以根據(jù)需要分片或復(fù)制組件的系統(tǒng)。
- 消息驅(qū)動(dòng)的通信:反應(yīng)式系統(tǒng)的所有組件都是松散耦合的,每個(gè)組件之間都有硬邊界。您的系統(tǒng)應(yīng)該通過(guò)顯式消息傳遞跨越這些邊界進(jìn)行通信。這些消息讓不同的組件了解故障,并幫助他們將工作流委派給可以處理它的組件。
反應(yīng)式和其他 Web 模式之間最顯著的區(qū)別是反應(yīng)式系統(tǒng)可以一次執(zhí)行多個(gè)未阻塞的調(diào)用,而不是讓一些調(diào)用等待其他調(diào)用。因此,響應(yīng)式系統(tǒng)可以提高性能和響應(yīng)速度,因?yàn)?Web 應(yīng)用程序的每個(gè)部分都可以比必須等待另一部分更快地完成自己的部分。
什么是反應(yīng)堆項(xiàng)目?
Project Reactor 是一個(gè)由 Pivotal 構(gòu)建并由 Spring 提供支持的框架。它實(shí)現(xiàn)了反應(yīng)式 API 模式,最著名的是反應(yīng)式流規(guī)范。
如果您熟悉Java 8 Streams,您會(huì)很快發(fā)現(xiàn) Stream 和 Flux(或其單元素版本 Mono)之間的許多相似之處。它們之間的主要區(qū)別在于 Fluxes 和 Monos 遵循一種publisher-subscriber模式并實(shí)現(xiàn)背壓,而 Stream API 則沒(méi)有。
背壓是數(shù)據(jù)端點(diǎn)向數(shù)據(jù)生產(chǎn)者發(fā)出信號(hào),表明它接收了太多數(shù)據(jù)的一種方式。這允許更好的流量管理和分配,因?yàn)樗梢苑乐箚蝹€(gè)組件過(guò)度工作。
使用 Reactor 的主要優(yōu)點(diǎn)是您可以完全控制數(shù)據(jù)流。您可以依靠訂閱者在準(zhǔn)備好處理信息時(shí)詢(xún)問(wèn)更多信息的能力,或者在發(fā)布者端緩沖一些結(jié)果,甚至使用沒(méi)有背壓的全推送方法。
在我們的反應(yīng)式堆棧中,它位于 Spring Boot 2.0 和 WebFlux 之上:
示例反應(yīng)式堆棧
堆棧:技術(shù)堆棧是用于創(chuàng)建 Web 或移動(dòng)應(yīng)用程序的軟件產(chǎn)品和編程語(yǔ)言的組合。反應(yīng)式堆棧是相同的,但用于創(chuàng)建反應(yīng)式應(yīng)用程序。
什么是 Spring WebFlux?
Spring WebFlux 是一個(gè)完全非阻塞、基于注解的 Web 框架,它構(gòu)建在 Project Reactor 之上,它使得在 HTTP 層上構(gòu)建響應(yīng)式應(yīng)用程序成為可能。WebFlux 使用新的路由器功能特性將函數(shù)式編程應(yīng)用于 Web 層并繞過(guò)聲明性控制器和請(qǐng)求映射。WebFlux 要求您將 Reactor 作為核心依賴(lài)項(xiàng)導(dǎo)入。
WebFlux 作為Spring MVC的響應(yīng)式替代品在 Spring 5 中添加,并增加了對(duì)以下內(nèi)容的支持:
- 非阻塞線(xiàn)程:無(wú)需等待先前任務(wù)完成即可完成指定任務(wù)的并發(fā)線(xiàn)程。
- Reactive Stream API:一種標(biāo)準(zhǔn)化工具,包括用于非阻塞背壓的異步流處理選項(xiàng)。
- 異步數(shù)據(jù)處理:當(dāng)數(shù)據(jù)在后臺(tái)處理并且用戶(hù)可以不間斷地繼續(xù)使用正常的應(yīng)用程序功能時(shí)。
最終WebFlux摒棄了SpringMVC的多請(qǐng)求線(xiàn)程模型,而是使用多EventLoop非阻塞模型來(lái)啟用反應(yīng)式、可擴(kuò)展的應(yīng)用程序。由于支持Netty、Undertow 和Servlet 3.1+ 容器等流行服務(wù)器,WebFlux 已成為反應(yīng)式堆棧的關(guān)鍵部分。
Router功能
RouterFunction是標(biāo)準(zhǔn)springmvc中使用的@RequestMapping和@Controller注釋樣式的一種功能替代。
我們可以使用它將請(qǐng)求路由到處理程序函數(shù):
- 傳統(tǒng)的路由定義
- @RestController
- public class ProductController {
- @RequestMapping("/product")
- public List<Product> productListing() {
- return ps.findAll();
- }
- }
- 函數(shù)式定義
- @Bean
- public RouterFunction<ServerResponse> productListing(ProductService ps) {
- return route().GET("/product", req -> ok().body(ps.findAll()))
- .build();
- }
你可以使用RouterFunctions.route()來(lái)創(chuàng)建路由,而不是編寫(xiě)完整的路由器函數(shù)。路由注冊(cè)為spring的bean,因此可以在任何配置類(lèi)中創(chuàng)建。路由器功能避免了由請(qǐng)求映射的多步驟過(guò)程引起的潛在副作用,而是將其簡(jiǎn)化為直接的路由器/處理程序鏈。這允許函數(shù)式編程實(shí)現(xiàn)反應(yīng)式編程。
RequestMapping和Controller注釋樣式在WebFlux中仍然有效如果您對(duì)舊樣式更熟悉,RouterFunctions只是解決方案的一個(gè)新選項(xiàng)。
WebClient 詳解
項(xiàng)目中經(jīng)常用到發(fā)送Http請(qǐng)求的客戶(hù)端,如果你使用webflux那非常簡(jiǎn)單去創(chuàng)建一個(gè)Http請(qǐng)求。WebClient是WebFlux的反應(yīng)式web客戶(hù)端,它是從著名的rest模板構(gòu)建的。它是一個(gè)接口,表示web請(qǐng)求的主要入口點(diǎn),并支持同步和異步操作。WebClient主要用于反應(yīng)式后端到后端通信。
您可以通過(guò)使用Maven導(dǎo)入標(biāo)準(zhǔn)WebFlux依賴(lài)項(xiàng)來(lái)構(gòu)建和創(chuàng)建WebClient實(shí)例:
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-webflux</artifactId>
- </dependency>
創(chuàng)建實(shí)例
- WebClient webClient = WebClient.create();
- // 如果是調(diào)用特定服務(wù)的API,可以在初始化webclient 時(shí)使用,baseUrl
- WebClient webClient = WebClient.create("https://github.com/1ssqq1lxr");
或者構(gòu)造器方式初始化
- WebClient webClient1 = WebClient.builder()
- .baseUrl("https://github.com/1ssqq1lxr")
- .defaultHeader(HttpHeaders.CONTENT_TYPE, "application/vnd.github.v3+json")
- .defaultHeader(HttpHeaders.USER_AGENT, "Spring 5 WebClient")
- .build();
- Get請(qǐng)求
- Mono<String> resp = WebClient.create()
- .method(HttpMethod.GET)
- .uri("https://github.com/1ssqq1lxr")
- .cookie("token","xxxx")
- .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
- .retrieve().bodyToMono(String.class);
- Post請(qǐng)求(表單)
- MultiValueMap<String, String> formData = new LinkedMultiValueMap();
- formData.add("name1","value1");
- formData.add("name2","value2");
- Mono<String> resp = WebClient.create().post()
- .uri("http://www.w3school.com.cn/test/demo_form.asp")
- .contentType(MediaType.APPLICATION_FORM_URLENCODED)
- .body(BodyInserters.fromFormData(formData))
- .retrieve().bodyToMono(String.class);
- Post請(qǐng)求(Body)
- Book book = new Book();
- book.setName("name");
- book.setTitle("this is title");
- Mono<String> resp = WebClient.create().post()
- .uri("https://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("http://localhost:8080/upload")
- .contentType(MediaType.MULTIPART_FORM_DATA)
- .body(BodyInserters.fromMultipartData(parts))
- .retrieve().bodyToMono(String.class);
Reactive Steam API
下篇文章給大家詳細(xì)講下Reactor3的API
Reactive Stream API是一個(gè)的函數(shù)集合,允許更智能的流數(shù)據(jù)流。它內(nèi)置了對(duì)背壓和異步處理的支持,確保應(yīng)用程序最有效地利用計(jì)算機(jī)和組件資源。
反應(yīng)流API有四個(gè)主要接口:
- Publisher:根據(jù)鏈接訂閱者的需求向他們發(fā)布事件。充當(dāng)訂戶(hù)可以監(jiān)視事件的中心鏈接點(diǎn)。
- Subscriber:接收和處理發(fā)布服務(wù)器發(fā)出的事件。多個(gè)訂閱服務(wù)器可以鏈接到單個(gè)發(fā)布服務(wù)器,并對(duì)同一事件做出不同的響應(yīng)。訂戶(hù)可以設(shè)置為反應(yīng):
- onNext,當(dāng)它接收到下一個(gè)事件時(shí)。
- onSubscribe,添加新訂閱時(shí)
- onError,當(dāng)另一個(gè)訂閱服務(wù)器發(fā)生錯(cuò)誤時(shí)
- onComplete,當(dāng)一個(gè)訂閱完成時(shí)
Server容器
WebFlux在Tomcat、Jetty、servlet3.1+容器以及Netty和Undertow等非Servlet運(yùn)行時(shí)上都受支持。Netty最常用于異步和非阻塞設(shè)計(jì),因此WebFlux將默認(rèn)使用它。只需對(duì)Maven或Gradle構(gòu)建軟件進(jìn)行簡(jiǎn)單的更改,就可以輕松地在這些服務(wù)器選項(xiàng)之間切換。
這使得WebFlux在它可以使用的技術(shù)方面具有高度的通用性,并允許您使用現(xiàn)有的基礎(chǔ)設(shè)施輕松地實(shí)現(xiàn)它。
并發(fā)模型
WebFlux是以無(wú)阻塞的思想構(gòu)建的,因此使用了與springmvc不同的并發(fā)編程模型。
springmvc假設(shè)線(xiàn)程將被阻塞,并在阻塞實(shí)例期間使用一個(gè)大的線(xiàn)程池來(lái)保持移動(dòng)。這個(gè)更大的線(xiàn)程池使得MVC資源更密集,因?yàn)橛?jì)算機(jī)硬件必須同時(shí)保持更多的線(xiàn)
WebFlux使用了一個(gè)小的線(xiàn)程池,因?yàn)樗僭O(shè)您永遠(yuǎn)不需要通過(guò)工作來(lái)避免阻塞。這些線(xiàn)程稱(chēng)為事件循環(huán)工作線(xiàn)程,數(shù)量固定,在傳入請(qǐng)求中的循環(huán)速度比MVC線(xiàn)程快。這意味著WebFlux更有效地使用計(jì)算機(jī)資源,因?yàn)榛顒?dòng)線(xiàn)程總是在工作。
Spring WebFlux Security
WebFlux使用Spring安全性來(lái)實(shí)現(xiàn)身份驗(yàn)證和授權(quán)協(xié)議。springsecurity使用WebFilter根據(jù)經(jīng)過(guò)身份驗(yàn)證的用戶(hù)列表認(rèn)證請(qǐng)求。
- @EnableWebFluxSecurity
- public class HelloWebFluxSecurityConfig {
- @Bean
- public MapReactiveUserDetailsService userDetailsService() {
- UserDetails user = User.withDefaultPasswordEncoder()
- .username("user")
- .password("user")
- .roles("USER")
- .build();
- return new MapReactiveUserDetailsService(user);
- }
- }
在這里,我們可以看到用戶(hù)有一個(gè)用戶(hù)名、一個(gè)密碼和一個(gè)或多個(gè)roles標(biāo)簽,這些標(biāo)簽允許自定義定訪問(wèn)。類(lèi)似于SpringBoot Security的 UserDetailsService接口
開(kāi)始使用 Spring WebFlux
生成項(xiàng)目
spring代碼生成器
參考配置
生成后的pom如下
- <?xml version="1.0" encoding="UTF-8"?>
- <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
- xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://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>
開(kāi)發(fā)接口
自定義一個(gè)函數(shù)路由:將請(qǐng)求path中的占位參數(shù)獲取作為返回值
- /**
- * @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());
- });
- }
- }
瀏覽器請(qǐng)求 http://localhost:4990/hello/haha
- haha
添加認(rèn)證
- /**
- * @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();
- }
- }
- 再次請(qǐng)求接口 瀏覽器請(qǐng)求 http://localhost:4990/hello/haha 此時(shí)瀏覽重定向到 http://localhost:4990/login
登陸頁(yè)面
輸入user/user 用戶(hù)名密碼后完成登陸。
再次瀏覽器請(qǐng)求 http://localhost:4990/hello/authenticate
- authenticate