日韩av黄I国产麻豆传媒I国产91av视频在线观看I日韩一区二区三区在线看I美女国产在线I麻豆视频国产在线观看I成人黄色短片

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 >

艿艿连肝了几个周末,写了一篇贼长的 Spring 响应式 Web 框架 WebFlux!市面第二完整~

發布時間:2025/3/21 66 豆豆
生活随笔 收集整理的這篇文章主要介紹了 艿艿连肝了几个周末,写了一篇贼长的 Spring 响应式 Web 框架 WebFlux!市面第二完整~ 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

本文在提供完整代碼示例,可見 https://github.com/YunaiV/SpringBoot-Labs 的 lab-27 目錄。

原創不易,給點個 Star 嘿,一起沖鴨!

1. 概述

友情提示:Reactive Programming ,翻譯為反應式編程,又稱為響應式編程。本文,我們統一使用響應式。不過,比較正確的叫法還是反應式。

Spring Framework?5?在 2017 年 9 月份,發布了 GA 通用版本。既然是一個新的大版本,必然帶來了非常多的改進,其中比較重要的一點,就是將響應式編程帶入了 Spring 生態。又或者說,將響應式編程“真正”帶入了 Java 生態之中。

在此之前,相信絕大多數 Java 開發者,對響應式編程的概念是非常模糊的。甚至說,截止到目前 2019 年 11 月份,對于國內的 Java 開發者,也是知之甚少。

對于我們來說,最早看到的就是 Spring5?提供了一個新的 Web 框架,基于響應式編程的 Spring WebFlux 。至此,SpringMVC 在“干掉” Struts 之后,難道要開始進入 Spring 自己的兩個 Web 框架的雙雄爭霸?

實際上,WebFlux 在出來的兩年時間里,據艿艿所了解到的情況,鮮有項目從采用 SpringMVC 遷移到 WebFlux ,又或者新項目直接采用 WebFlux 。這又是為什么呢?

艿艿:V2EX 上還有這樣一個討論 《現在有公司在使用 Spring Boot 2.0 的 WebFlux 嗎?》 。

響應式編程,對我們現有的編程方式,是一場顛覆,對于框架也是。

  • 在 Spring 提供的框架中,實際并沒有全部實現好對響應式編程的支持。例如說,Spring Transaction 事務組件,在 Spring 5.2 M2 版本,才提供了支持響應式編程的 ReactiveTransactionManager 事務管理器。

  • 更不要說,Java 生態常用的框架,例如說 MyBatis、Jedis 等等,都暫未提供響應式編程的支持。

所以,WebFlux 想要能夠真正普及到我們的項目中,不僅僅需要 Spring 自己體系中的框架提供對響應式編程的很好的支持,也需要 Java 生態中的框架也要做到如此。例如說:

艿艿:😈 Java 框架存在大量基于 ThreadLocal 線程變量,實現參數的透傳,改造的成本,實際是不小的。

當然,即使如此,這也并不妨礙我們來對 WebFlux 進行一個小小的入門。畢竟,響應式編程這把火,終將熊熊燃起,燒死那些異性戀。哈哈哈~

艿艿:下面的會涉及比較多的概念,不想看的胖友,直接跳到 「2. 快速入門」 小節,直接開始 WebFlux 的入門。

1.1 響應式編程

我們先簡單來了解下響應式編程的相關姿勢,以保證能夠看懂 WebFlux 入門的代碼示例,哈哈哈~

維基百科對響應式編程定義如下:

FROM https://en.wikipedia.org/wiki/Reactive_programming

Reactive programming is an asynchronous programming paradigm concerned with data streams and the propagation of change. This means that it becomes possible to express static (e.g. arrays) or dynamic (e.g. event emitters) data streams with ease via the employed programming language(s).

反應式編程是一種異步編程范式,它關注數據流和變化的傳播。這意味著可以通過使用編程語言輕松地表示靜態(如數組)或動態(如事件發射器)數據流。

Spring 官方文檔對響應式編程定義如下:

FROM https://docs.spring.io/spring-framework/docs/5.0.0.BUILD-SNAPSHOT/spring-framework-reference/html/web-reactive.html#web-reactive-programming

In plain terms reactive programming is about non-blocking applications that are asynchronous and event-driven and require a small number of threads to scale vertically (i.e. within the JVM) rather than horizontally (i.e. through clustering).

簡單地說,響應式編程是關于非阻塞應用程序的,這些應用程序是異步的、事件驅動的,并且需要少量的線程來垂直伸縮(即在 JVM 中),而不是水平伸縮(即通過集群)。

😈 兩個看起來都不很易懂。不過如果胖友看過 Netty 框架的介紹,會發現跟 Spring 的描述非常相像。定義如下:

FROM https://www.oschina.net/p/netty

Netty 是一個 Java 開源框架。Netty 提供異步的、事件驅動的網絡應用程序框架和工具,用以快速開發高性能、高可靠性的網絡服務器和客戶端程序。

是不是都看到了異步 + 事件驅動。本質上,Netty 也是有基于響應式編程的思想。所以在下文中,我們會看到,可以使用 Netty 作為 WebFlux 的服務器。

嗶嗶了這么多,艿艿來用簡單但不完全精準的語言嘗試下。以后端 API 請求的處理來舉例子。

  • 在現在主流的編程模型中,請求是被同步阻塞處理完成,返回結果給前端。

  • 在響應式的編程模型中,請求是被作為一個事件丟到線程池中執行,等到執行完畢,異步回調結果給主線程,最后返回給前端。

通過這樣的方式,主線程(實際是多個,這里只是方便描述哈)不斷接收請求,不負責直接同步阻塞處理,從而避免自身被阻塞。

1.2 Reactor 框架

在 Java 生態中,提供響應式編程的框架主要有 Reactor、RxJava、JDK9 Flow API 。

那么,Spring 會選擇哪個框架作為其響應式編程的基礎呢?

  • 首先,可以排除 JDK9 Flow API ,因為 Spring5 要支持 JDK8 版本開始。

  • 其次,Reactor 是 Spring?公司 Pivotal(咳咳咳,2019 年竟然被 VMWare 收購了)是開源的框架,所以必然是“強強聯合”,嘿嘿。

如果胖友想要了解 Reactor 和 RxJava 的對比,可以看看 《八個層面比較 Java 8, RxJava, Reactor》 文章,挺詳細的。

讓我們一起來看看 Reactor 官方對自己的介紹:

FROM https://projectreactor.io/

Reactor is a fourth-generation Reactive library for building non-blocking applications on the JVM based on the Reactive Streams Specification

Reactor 是一個第四代響應式編程框架,用于構建非阻塞 JVM 應用程序,基于 Reactive Streams Specification 來實現。

Reactor Operators and Schedulers can sustain high throughput rates on the order of 10's of millions of messages per second.

Reactor 的操作和調度可以提供每秒千萬條消息的高吞吐量。

Plus its low memory footprint should go under most of the radars.

再加上它的低內存占用,應該在大多數雷達(radars)之下。咳咳咳,這個 radars 怎么翻譯。

簡單來說,Reactor 說是一個響應式編程框架,又快又不占用內存的那種。😈

關于 Reactor 的使用,這里艿艿就不過多介紹,感興趣的胖友,可以看看 《使用 Reactor 進行反應式編程》 文章。如下是對其中的一段內容的節選并修改:

Reactor 有兩個非常重要的基本概念:

  • Flux ,表示的是包含 0 到 N 個元素的異步序列。當消息通知產生時,訂閱者(Subscriber)中對應的方法?#onNext(t),?#onComplete(t)?和?#onError(t)?會被調用。

  • Mono 表示的是包含 0 或者 1 個元素的異步序列。該序列中同樣可以包含與 Flux 相同的三種類型的消息通知。

  • 同時,Flux 和 Mono 之間可以進行轉換。例如:

    • 對一個 Flux 序列進行計數操作,得到的結果是一個?Mono<Long>?對象。

    • 把兩個 Mono 序列合并在一起,得到的是一個 Flux 對象。

😈 其實,可以先暫時簡單把 Mono 理解成 Object ,Flux 理解成 List 。嘿嘿~

1.3 Spring WebFlux

Spring 官方文檔對 Spring WebFlux 介紹如下:

FROM https://docs.spring.io/spring-framework/docs/5.0.0.BUILD-SNAPSHOT/spring-framework-reference/html/web-reactive.html

Spring Framework 5 includes a new spring-webflux module. The module contains support for reactive HTTP and WebSocket clients as well as for reactive server web applications including REST, HTML browser, and WebSocket style interactions.

Spring Framework 5 提供了一個新的?spring-webflux?模塊。該模塊包含了:

  • 對響應式支持的 HTTP 和 WebSocket 客戶端。

  • 對響應式支持的 Web 服務器,包括 Rest API、HTML 瀏覽器、WebSocket 等交互方式。

On the server-side WebFlux supports 2 distinct programming models:

  • Annotation-based with @Controller and the other > annotations supported also with Spring MVC

  • Functional, Java 8 lambda style routing and handling

在服務端方面,WebFlux 提供了 2 種編程模型(翻譯成使用方式,可能更易懂):

  • 方式一,基于 Annotated Controller 方式實現:基于?@Controller?和 SpringMVC 使用的其它注解。😈 也就是說,我們大體上可以像使用 SpringMVC 的方式,使用 WebFlux 。

  • 方式二,基于函數式編程方式:函數式,Java 8 lambda 表達式風格的路由和處理。😈 可能有點晦澀,晚點我們看了示例就會明白。

Both programming models are executed on the same reactive foundation that adapts non-blocking HTTP runtimes to the Reactive Streams API.

這兩個編程模型,都是在同一個響應式基礎(foundation)上執行的,該基礎將非阻塞 HTTP 運行時(runtime)適配成響應式 API 。😈 簡單來說,就是將原有的 API ,使用 Reactor 封裝成響應式 API ,讓我們開發者使用更加便捷。

The diagram below shows the server-side stack including traditional, Servlet-based Spring MVC on the left from the spring-webmvc module and also the reactive stack on the right from the spring-webflux module.

下圖顯示了服務端的技術棧,左側是?spring-webmvc?模塊中傳統的、基于 Servlet 的 Spring MVC ,右側是?spring-webflux?模塊中的響應式技術棧。

webflux-overview

  • 😈 仔細看第一層的兩個框框,分別是上面提到的 WebFlux 的兩種編程模型。表達的是 SpringMVC 不支持 Router Functions 方式,而 WebFlux 支持。

WebFlux can run on Servlet containers with support for the Servlet 3.1 Non-Blocking IO API as well as on other async runtimes such as Netty and Undertow.

WebFlux 可以運行在:

  • 支持 Servlet 3.1 非阻塞 IO API 的 Servlet 容器上

  • 也可以運行在支持異步運行時的,例如說 Netty 或者 Undertow 上

Each runtime is adapted to a reactive ServerHttpRequest and ServerHttpResponse exposing the body of the request and response as Flux, rather than InputStream and OutputStream, with reactive backpressure.

每一個運行時(runtime)適用于將響應式的 ServerHttpRequest 和 ServerHttpResponse 中 request 和 response 的 body 暴露成?Flux<DataBuffer>?對象,而不是 InputStream ?和 InputStream ?對象,可用于響應式中的背壓(backpressure)。😈 這段有點晦澀,簡單來說:

  • 對于 Servlet 來說,?ServletRequest#getInputStream()?方法,獲得請求的主體內容返回的是 InputStream 對象。

  • 對于 WebFlux 來說,ServerHttpRequest#getBody()?方法,獲得請求的主體內容返回的是?Flux<DataBuffer>?對象。

REST-style JSON and XML serialization and deserialization is supported on top as a?Flux<Object>, and so is HTML view rendering and Server-Sent Events.

REST 風格 API 使用到的 JSON 和 XML 序列化和反序列化,需要提供對?Flux<Object>?的支持。對于 HTML 渲染,和 SSE 也要提供對?Flux<Object>?的支持。

😈 咳咳咳,看完了這一大段,是不是突然有點想捶死艿艿,說的什么 XX 玩樣啊!其實,在我們初學 SpringMVC 的時候,也是一臉懵逼的學完。隨著我們對 SpringMVC 的日趨熟練,逐步對其提供的組件、原理、源碼慢慢熟悉。所以,對于我們來說,WebFlux 乃至響應式編程來說,都是足夠新穎的知識,我們要抱著空杯心態,「Stay Hungry, Stay Foolish」 。

如果胖友的時間比較充分,可以選擇把 《Spring 文檔 —— Web on Reactive Stack》 仔細看看,詳盡的介紹了 Spring 在 Web 方面,響應式相關的技術棧。

雖然說上面我們在介紹 WebFlux ,把它搞的很復雜,實際在快速入門使用它,還是非常簡單的。下面,開始讓我們開始愉快的快速入門下~

艿艿:考慮到艿艿之前已經寫了 《芋道 Spring Boot SpringMVC 入門》 文章,所以本文我們提供的示例,盡量覆蓋到在 SpringMVC 提到的內容。

當然,很多相似的概念,艿艿也不重復介紹,不然顯得我老啰嗦了。

2. 快速入門

示例代碼對應倉庫:lab-27-webflux-01 。

本小節,我們會使用?spring-boot-starter-webflux?實現 WebFlux 的自動化配置。然后實現用戶的增刪改查接口。接口列表如下:

請求方法URL功能
GET/users/list查詢用戶列表
GET/users/get獲得指定用戶編號的用戶
POST/users/add添加用戶
POST/users/update更新指定用戶編號的用戶
POST/users/delete刪除指定用戶編號的用戶

下面,開始遨游~

2.1 引入依賴

在?pom.xml?文件中,引入相關依賴。

<?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?http://maven.apache.org/xsd/maven-4.0.0.xsd"><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.2.1.RELEASE</version><relativePath/>?<!--?lookup?parent?from?repository?--></parent><modelVersion>4.0.0</modelVersion><artifactId>lab-27-webflux-01</artifactId><dependencies><!--?實現對?Spring?WebFlux?的自動化配置?--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-webflux</artifactId></dependency><!--?方便等會寫單元測試?--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency></dependencies></project>
  • 具體每個依賴的作用,胖友自己認真看下艿艿添加的所有注釋噢。

我們使用 IDEA Maven 插件 ,查看下?spring-boot-starter-webflux?依賴中,所引入的依賴。如下圖所示:

  • 引入?reactor-core?依賴,使用 Reactor 作為 WebFlux 的響應式框架的基礎。

  • 引入?spring-boot-starter-reactor-netty?依賴,使用 Netty 構建 WebFlux 的 Web 服務器。其中 RxNetty 庫,是基于 Reactor 的響應式框架的基礎之上,提供出 Netty 的響應式 API 。

當然,我們除了使用可以使用其它作為 WebFlux 的 Web 服務器,如下表格:

Server nameServer API usedReactive Streams support
NettyNetty APIReactor Netty
UndertowUndertow APIspring-web: Undertow to Reactive Streams bridge
TomcatServlet 3.1 non-blocking I/O; Tomcat API to read and write ByteBuffers vs byte[]spring-web: Servlet 3.1 non-blocking I/O to Reactive Streams bridge
JettyServlet 3.1 non-blocking I/O; Jetty API to write ByteBuffers vs byte[]spring-web: Servlet 3.1 non-blocking I/O to Reactive Streams bridge
Servlet 3.1 containerServlet 3.1 non-blocking I/Ospring-web: Servlet 3.1 non-blocking I/O to Reactive Streams bridge
  • 當然,也需要基于 Reactor 的響應式框架的基礎之上,封裝相應的響應式 API 。

可能胖友會有疑惑,為什么 WebFlux 運行在 Servlet 容器上時,需要?Servlet 3.1+?以上的容器呢?在 Servlet 3.1 規范發布時,它定義了非常重要的特性,Non-blocking I/O 非阻塞 IO ,提供了異步處理請求的支持。我們來詳細展開下:

  • 在 Servlet 3.1 規范之前的版本,請求是只能被 Servlet 同步阻塞處理完成,返回結果給前端。

  • 在 Servlet 3.1 規范開始的版本,請求是允許被 Servlet 丟到線程池中處執行,等到執行完畢,異步回調結果給 Servlet ,最后返回給前端。

艿艿:推薦胖友在閱讀完本文之后,可以看看 《Servlet 3.0/3.1 中的異步處理》 文章,可以對 WebFlux 有更好的理解。

2.2 Application

創建?Application.java?類,配置?@SpringBootApplication?注解即可。代碼如下:

//?Application.java@SpringBootApplication public?class?Application?{public?static?void?main(String[]?args)?{SpringApplication.run(Application.class,?args);}}
  • 先暫時不啟動項目。等我們添加好 API 接口。

在 「1.3 Spring WebFlux」 小節中,我們提到了 WebFlux 有兩種編程模型,分別是:

  • 方式一,基于 Annotated Controller 方式實現

  • 方式二,基于函數式編程方式

我們分別在下面兩個小節來看看。

2.3 基于 Annotated Controller 方式實現

在?cn.iocoder.springboot.lab27.springwebflux.controller?包路徑下,創建 UserController 類。代碼如下:

//?UserController.java@RestController @RequestMapping("/users") public?class?UserController?{/***?查詢用戶列表**?@return?用戶列表*/@GetMapping("/list")public?Flux<UserVO>?list()?{//?查詢列表List<UserVO>?result?=?new?ArrayList<>();result.add(new?UserVO().setId(1).setUsername("yudaoyuanma"));result.add(new?UserVO().setId(2).setUsername("woshiyutou"));result.add(new?UserVO().setId(3).setUsername("chifanshuijiao"));//?返回列表return?Flux.fromIterable(result);}/***?獲得指定用戶編號的用戶**?@param?id?用戶編號*?@return?用戶*/@GetMapping("/get")public?Mono<UserVO>?get(@RequestParam("id")?Integer?id)?{//?查詢用戶UserVO?user?=?new?UserVO().setId(id).setUsername("username:"?+?id);//?返回return?Mono.just(user);}/***?添加用戶**?@param?addDTO?添加用戶信息?DTO*?@return?添加成功的用戶編號*/@PostMapping("add")public?Mono<Integer>?add(@RequestBody?Publisher<UserAddDTO>?addDTO)?{//?插入用戶記錄,返回編號Integer?returnId?=?1;//?返回用戶編號return?Mono.just(returnId);}/***?更新指定用戶編號的用戶**?@param?updateDTO?更新用戶信息?DTO*?@return?是否修改成功*/@PostMapping("/update")public?Mono<Boolean>?update(@RequestBody?Publisher<UserUpdateDTO>?updateDTO)?{//?更新用戶記錄Boolean?success?=?true;//?返回更新是否成功return?Mono.just(success);}/***?刪除指定用戶編號的用戶**?@param?id?用戶編號*?@return?是否刪除成功*/@PostMapping("/delete")?//?URL?修改成?/delete?,RequestMethod?改成?DELETEpublic?Mono<Boolean>?delete(@RequestParam("id")?Integer?id)?{//?刪除用戶記錄Boolean?success?=?false;//?返回是否更新成功return?Mono.just(success);}}
  • 在類和方法上,我們添加了?@Controller?和 SpringMVC 在使用的?@GetMapping?和?PostMapping?等注解,提供 API 接口,這個和我們在使用 SpringMVC 是一模一樣的。

  • 在?dto?和?vo?包下,有 API 使用到的 DTO 和 VO 類。

  • 😈 因為是入門示例,我們會發現代碼十分簡單,保持淡定。在后文中,我們會提供和 Spring Data JPA、Spring Data MongoDB、Spring Data Redis 等等整合的示例。

  • #list()?方法,我們最終調用?Flux#fromIterable(Iterable<? extends T> it)?方法,將 List 包裝成 Flux 對象返回。

  • #get(Integer id)?方法,我們最終調用?Mono#just(T data)?方法,將 UserVO 包裝成 Mono 對象返回。

  • #add(Publisher<UserAddDTO> addDTO)?方法,參數為 Publisher 類型,泛型為 UserAddDTO 類型,并且添加了?@RequestBody?注解,從 request 的 Body 中讀取參數。注意,此時提交參數需要使用?"application/json"?等 Content-Type 內容類型。

  • #add(...)?方法,也可以使用?application/x-www-form-urlencoded?或?multipart/form-data?這兩個 Content-Type 內容類型,通過 request 的 Form Data 或 Multipart Data 傳遞參數。代碼如下: //?UserController.java/***?添加用戶**?@param?addDTO?添加用戶信息?DTO*?@return?添加成功的用戶編號*/ @PostMapping("add2") public?Mono<Integer>?add(Mono<UserAddDTO>?addDTO)?{//?插入用戶記錄,返回編號Integer?returnId?=?UUID.randomUUID().hashCode();//?返回用戶編號return?Mono.just(returnId); }
    • 此時,參數為 Mono 類型,泛型為 UserAddDTO 類型。

    • 當然,我們也可以直接使用參數為 UserAddDTO 類型。如果后續需要使用到 Reactor API ,則我們自己主動調用?Mono#just(T data)?方法,封裝出 Publisher 對象。😈 注意,Flux 和 Mono 都實現了 Publisher 接口。

    • 可能有胖友不了解 request Form Data、Multipart Data 和 request Body 的差異,可以看看 《HTTP 請求中 request payload 和 formData 區別?》 文章。

    • WebFlux 對于 Form Data ,在 《Web on Reactive Stack —— Spring WebFlux —— Form Data》 有簡短說明。

    • WebFlux 對于 Multipart Data ?,在 《Web on Reactive Stack —— Spring WebFlux —— Multipart Data 》 有簡短說明。

  • #update(Publisher<UserUpdateDTO> updateDTO)?方法,和?#add(Publisher<UserAddDTO> addDTO)?方法一致,就不重復贅述。

  • #delete(Integer id)?方法,和?#get(Integer id)?方法一致,就不重復贅述。

2.4 基于函數式編程方式

在?cn.iocoder.springboot.lab27.springwebflux.controller?包路徑下,創建 UserRouter 類。代碼如下:

//?UserRouter.java@Configuration public?class?UserRouter?{@Beanpublic?RouterFunction<ServerResponse>?userListRouterFunction()?{return?RouterFunctions.route(RequestPredicates.GET("/users2/list"),new?HandlerFunction<ServerResponse>()?{@Overridepublic?Mono<ServerResponse>?handle(ServerRequest?request)?{//?查詢列表List<UserVO>?result?=?new?ArrayList<>();result.add(new?UserVO().setId(1).setUsername("yudaoyuanma"));result.add(new?UserVO().setId(2).setUsername("woshiyutou"));result.add(new?UserVO().setId(3).setUsername("chifanshuijiao"));//?返回列表return?ServerResponse.ok().bodyValue(result);}});}@Beanpublic?RouterFunction<ServerResponse>?userGetRouterFunction()?{return?RouterFunctions.route(RequestPredicates.GET("/users2/get"),new?HandlerFunction<ServerResponse>()?{@Overridepublic?Mono<ServerResponse>?handle(ServerRequest?request)?{//?獲得編號Integer?id?=?request.queryParam("id").map(s?->?StringUtils.isEmpty(s)???null?:?Integer.valueOf(s)).get();//?查詢用戶UserVO?user?=?new?UserVO().setId(id).setUsername(UUID.randomUUID().toString());//?返回列表return?ServerResponse.ok().bodyValue(user);}});}@Beanpublic?RouterFunction<ServerResponse>?demoRouterFunction()?{return?route(GET("/users2/demo"),?request?->?ok().bodyValue("demo"));}}
  • 在類上,添加?@Configuration?注解,保證該類中的 Bean 們,都被掃描到。

  • 在每個方法中,我們都通弄?RouterFunctions#route(RequestPredicate predicate, HandlerFunction<T> handlerFunction)?方法,定義了一條路由。

    • 第一個參數?predicate?參數,是 RequestPredicate 類型,請求謂語,用于匹配請求。可以通過 RequestPredicates 來構建各種條件。

    • 第二個參數?handlerFunction?參數,是 RouterFunction 類型,處理器函數。

  • 每個方法定義的路由,胖友自己看下代碼,一眼能看的明白。一般來說,采用第三個方法的寫法,更加簡潔。注意,需要使用?static import?靜態引入,代碼如下:

    import?static?org.springframework.web.reactive.function.server.RequestPredicates.*; import?static?org.springframework.web.reactive.function.server.RouterFunctions.*; import?static?org.springframework.web.reactive.function.server.ServerResponse.*;

一般來說,艿艿更加推薦基于 Annotated Controller 方式實現的編程方式,更符合我們現在的開發習慣,學習成本也相對低一些。同時,和 API 接口文檔工具 Swagger 也更容易集成。

😈 有沒覺得每個 HandlerFunction 函數,和每個 Servlet 有點像。

更多基于函數式編程方式的示例,可以看看如下兩篇文章:

  • 《Introduction to the Functional Web Framework in Spring 5》

  • 《Spring Boot RouterFunction tutorial》

3. 測試接口

示例代碼對應倉庫:lab-27-webflux-01 。

在開發完接口,我們會進行接口的自測。一般情況下,我們先啟動項目,然后使用 Postman、curl、瀏覽器,手工模擬請求后端 API 接口。

實際上,WebFlux 提供了 Web 測試客戶端 WebTestClient 類,方便我們快速測試接口。下面,我們對 UserController 提供的接口,進行下單元測試。也就是說,本小節,我們會繼續在 lab-27-webflux-01 示例的基礎上修改。

MockMvc 提供了集成測試和單元測試的能力,我們分成 「3.1 集成測試」 和 「3.2 單元測試」 來看。如果胖友對測試這塊不太了解,可以看看如下兩篇文章:

  • 《小談 Java 單元測試》

  • 《談談單元測試》

3.1 集成測試

創建 UserControllerTest 測試類,我們來測試一下簡單的 UserController 的每個操作。核心代碼如下:

//?UserControllerTest.java@RunWith(SpringRunner.class) @SpringBootTest(classes?=?Application.class) @AutoConfigureWebFlux @AutoConfigureWebTestClient public?class?UserControllerTest?{@Autowiredprivate?WebTestClient?webClient;@Testpublic?void?testList()?{webClient.get().uri("/users/list").exchange()?//?執行請求.expectStatus().isOk()?//?響應狀態碼?200.expectBody().json("[\n"?+"????{\n"?+"????????\"id\":?1,\n"?+"????????\"username\":?\"yudaoyuanma\"\n"?+"????},\n"?+"????{\n"?+"????????\"id\":?2,\n"?+"????????\"username\":?\"woshiyutou\"\n"?+"????},\n"?+"????{\n"?+"????????\"id\":?3,\n"?+"????????\"username\":?\"chifanshuijiao\"\n"?+"????}\n"?+"]");?//?響應結果}@Testpublic?void?testGet()?{//?獲得指定用戶編號的用戶webClient.get().uri("/users/get?id=1").exchange()?//?執行請求.expectStatus().isOk()?//?響應狀態碼?200.expectBody().json("{\n"?+"????\"id\":?1,\n"?+"????\"username\":?\"username:1\"\n"?+"}");?//?響應結果}@Testpublic?void?testGet2()?{//?獲得指定用戶編號的用戶webClient.get().uri("/users/v2/get?id=1").exchange()?//?執行請求.expectStatus().isOk()?//?響應狀態碼?200.expectBody().json("{\n"?+"????\"id\":?1,\n"?+"????\"username\":?\"test\"\n"?+"}");?//?響應結果}@Testpublic?void?testAdd()?{Map<String,?Object>?params?=?new?HashMap<>();params.put("username",?"yudaoyuanma");params.put("password",?"nicai");//?添加用戶webClient.post().uri("/users/add").bodyValue(params).exchange()?//?執行請求.expectStatus().isOk()?//?響應狀態碼?200.expectBody().json("1");?//?響應結果。因為沒有提供 content 的比較,所以只好使用 json 來比較。竟然能通過}@Testpublic?void?testAdd2()?{?//?發送文件的測試,可以參考?https://dev.to/shavz/sending-multipart-form-data-using-spring-webtestclient-2gb7?文章BodyInserters.FormInserter<String>?formData?=?//?Form?Data?數據,需要這么拼湊BodyInserters.fromFormData("username",?"yudaoyuanma").with("password",?"nicai");//?添加用戶webClient.post().uri("/users/add2").body(formData).exchange()?//?執行請求.expectStatus().isOk()?//?響應狀態碼?200.expectBody().json("1");?//?響應結果。因為沒有提供 content 的比較,所以只好使用 json 來比較。竟然能通過}@Testpublic?void?testUpdate()?{Map<String,?Object>?params?=?new?HashMap<>();params.put("id",?1);params.put("username",?"yudaoyuanma");//?修改用戶webClient.post().uri("/users/update").bodyValue(params).exchange()?//?執行請求.expectStatus().isOk()?//?響應狀態碼?200.expectBody(Boolean.class)?//?期望返回值類型是?Boolean.consumeWith((Consumer<EntityExchangeResult<Boolean>>)?result?->?//?通過消費結果,判斷符合是 true 。Assert.assertTrue("返回結果需要為?true",?result.getResponseBody()));}@Testpublic?void?testDelete()?{//?刪除用戶webClient.post().uri("/users/delete?id=1").exchange()?//?執行請求.expectStatus().isOk()?//?響應狀態碼?200.expectBody(Boolean.class)?//?期望返回值類型是?Boolean.isEqualTo(true);?//?這樣更加簡潔一些 //??????????????? .consumeWith((Consumer<EntityExchangeResult<Boolean>>) result ->?//?通過消費結果,判斷符合是 true 。 //????????????????????????Assert.assertTrue("返回結果需要為?true",?result.getResponseBody()));}}
  • 在類上,我們添加了?@AutoConfigureWebTestClient?注解,用于自動化配置我們稍后注入的 WebTestClient Bean 對象?webClient?。在后續的測試中,我們會看到都是通過?webClient?調用后端 API 接口。而每一次調用后端 API 接口,都會執行真正的后端邏輯。因此,整個邏輯,走的是集成測試,會啟動一個真實的 Spring 環境。

  • 每次 API 接口的請求,都通過 RequestHeadersSpec 來構建。構建完成后,通過?RequestHeadersSpec#exchange()?方法來執行請求,返回 ResponseSpec 結果。

    • WebTestClient 的?#get()、#head()、#delete()、#options()?方法,返回的是 RequestHeadersUriSpec 對象。

    • WebTestClient 的?#post()、#put()、#delete()、#patch()?方法,返回的是 RequestBodyUriSpec 對象。

    • RequestHeadersUriSpec 和 RequestBodyUriSpec 都繼承了 RequestHeadersSpec 接口。

  • 執行完請求后,通過調用 RequestBodyUriSpec 的各種斷言方法,添加對結果的預期,相當于做斷言。如果不符合預期,則會拋出異常,測試不通過。

3.2 單元測試

為了更好的展示 WebFlux 單元測試的示例,我們需要改寫 UserController 的代碼,讓其會依賴 UserService 。修改點如下:

  • 在?cn.iocoder.springboot.lab27.springwebflux.service?包路徑下,創建 UserService 類。代碼如下:

    //?UserService.java@Service public?class?UserService?{public?UserVO?get(Integer?id)?{return?new?UserVO().setId(id).setUsername("test");}}
  • 在 UserController 類中,增加?GET /users/v2/get?接口,獲得指定用戶編號的用戶。代碼如下:

    //?UserController.java@Autowired private?UserService?userService;/***?獲得指定用戶編號的用戶**?@param?id?用戶編號*?@return?用戶*/ @GetMapping("/v2/get") public?Mono<UserVO>?get2(@RequestParam("id")?Integer?id)?{//?查詢用戶UserVO?user?=?userService.get(id);//?返回return?Mono.just(user); }
    • 在代碼中,我們注入了 UserService Bean 對象?userService?,然后在新增的接口方法中,會調用?UserService#get(Integer id)?方法,獲得指定用戶編號的用戶。

創建 UserControllerTest2 測試類,我們來測試一下簡單的 UserController 的新增的這個 API 操作。代碼如下:

//?UserControllerTest2.java@RunWith(SpringRunner.class) @WebFluxTest(UserController.class) public?class?UserControllerTest2?{@Autowiredprivate?WebTestClient?webClient;@MockBeanprivate?UserService?userService;@Testpublic?void?testGet2()?throws?Exception?{//?Mock?UserService?的?get?方法System.out.println("before?mock:"?+?userService.get(1));?//?<1.1>Mockito.when(userService.get(1)).thenReturn(new?UserVO().setId(1).setUsername("username:1"));?//?<1.2>System.out.println("after?mock:"?+?userService.get(1));?//?<1.3>//?查詢用戶列表webClient.get().uri("/users/v2/get?id=1").exchange()?//?執行請求.expectStatus().isOk()?//?響應狀態碼?200.expectBody().json("{\n"?+"????\"id\":?1,\n"?+"????\"username\":?\"username:1\"\n"?+"}");?//?響應結果}}
  • 在類上添加?@WebFluxTest?注解,并且傳入的是 UserController 類,表示我們要對 UserController 進行單元測試。

  • 同時,@WebFluxTest?注解,是包含了?@UserController?的組合注解,所以它會自動化配置我們稍后注入的 WebTestClient Bean 對象?mvc?。在后續的測試中,我們會看到都是通過?webClient?調用后端 API 接口。但是!每一次調用后端 API 接口,并不會執行真正的后端邏輯,而是走的 Mock 邏輯。也就是說,整個邏輯,走的是單元測試會啟動一個?Mock?的 Spring 環境。

艿艿:注意上面每個加粗的地方!

  • userService?屬性,我們添加了?@MockBean?注解,實際這里注入的是一個使用 Mockito 創建的 UserService Mock 代理對象。如下圖所示:

    • 打印的就是我們 Mock 返回的 UserVO 對象。

    • 結果竟然返回的是?null?空。理論來說,此時應該返回一個?id = 1?的 UserVO 對象。實際上,因為此時的?userService?是通過 Mockito 來 Mock 出來的對象,其所有調用它的方法,返回的都是空。

    • UserController 中,也會注入一個 UserService 屬性,此時注入的就是該 Mock 出來的 UserService Bean 對象。

    • 默認情況下,

    • <1.1>?處,我們調用?UserService#get(Integer id)?方法,然后打印返回結果。執行結果如下:

      before?mock:null
    • <1.2>?處,通過 Mockito 進行 Mock?userService?的?#get(Integer id)?方法,當傳入的?id = 1?方法參數時,返回?id = 1?并且?username = "username:1"?的 UserVO 對象。

    • <1.3>?處,再次調用?UserService#get(Integer id)?方法,然后打印返回結果。執行結果如下:

      after?cn.iocoder.springboot.lab27.springwebflux.vo.UserVO@23202c31
  • 后續,使用?webClient?完成一次后端 API 調用,并進行斷言結果是否正確。執行成功,單元測試通過。

可能胖友對單元測試不是很了解,這里在額外推薦一本書 《有效的單元測試》 。很薄,周末抽幾個小時就能讀完。

如果覺得本小節還不夠,可以看看 《SpringBoot WebFlux Test – @WebFluxTest》 文章,寫的還是不錯的。

4. 全局統一返回

示例代碼對應倉庫:lab-27-webflux-02 。

在我們提供后端 API 給前端時,我們需要告前端,這個 API 調用結果是否成功:

  • 如果成功,成功的數據是什么。后續,前端會取數據渲染到頁面上。

  • 如果失敗,失敗的原因是什么。一般,前端會將原因彈出提示給用戶。

這樣,我們就需要有統一的返回結果,而不能是每個接口自己定義自己的風格。一般來說,統一的全局返回信息如下:

  • 成功時,返回成功的狀態碼?+?數據

  • 失敗時,返回失敗的狀態碼?+?錯誤提示

在標準的 RESTful API 的定義,是推薦使用 HTTP 響應狀態碼 返回狀態碼。一般來說,我們實踐很少這么去做,主要有如下原因:

  • 業務返回的錯誤狀態碼很多,HTTP 響應狀態碼無法很好的映射。例如說,活動還未開始、訂單已取消等等。

  • 國內開發者對 HTTP 響應狀態碼不是很了解,可能只知道 200、403、404、500 幾種常見的。這樣,反倒增加學習成本。

所以,實際項目在實踐時,我們會將狀態碼放在 Response Body?響應內容中返回。

在全局統一返回里,我們至少需要定義三個字段:

  • code:狀態碼。無論是否成功,必須返回。

    關于這一塊,也有團隊實踐時,增加了?success?字段,通過?true?和?false?表示成功還是失敗。這個看每個團隊的習慣吧。艿艿的話,還是偏好基于約定,返回 0 時表示成功。

    • 成功時,狀態碼為 0 。

    • 失敗時,對應業務的錯誤碼。

  • data:數據。成功時,返回該字段。

  • message:錯誤提示。失敗時,返回該字段。

那么,讓我們來看兩個示例:

//?成功響應 {code:?0,data:?{id:?1,username:?"yudaoyuanma"} }//?失敗響應 {code:?233666,message:?"徐媽太丑了" }

下面,我們來看一個示例。

艿艿:考慮到不破壞 「2. 快速入門」 和 「3. 測試接口」 提供的示例,我們需要重新弄搭建一個。

4.1 引入依賴

在 「2.2 引入依賴」 一致。

4.2 Application

在 「2.3 Application」 一致。

4.3 CommonResult

在?cn.iocoder.springboot.lab27.springwebflux.core.vo?包路徑,創建 CommonResult 類,用于全局統一返回。代碼如下:

//?CommonResult.javapublic?class?CommonResult<T>?implements?Serializable?{public?static?Integer?CODE_SUCCESS?=?0;/***?錯誤碼*/private?Integer?code;/***?錯誤提示*/private?String?message;/***?返回數據*/private?T?data;/***?將傳入的?result?對象,轉換成另外一個泛型結果的對象**?因為 A 方法返回的 CommonResult 對象,不滿足調用其的 B 方法的返回,所以需要進行轉換。**?@param?result?傳入的?result?對象*?@param?<T>?返回的泛型*?@return?新的?CommonResult?對象*/public?static?<T>?CommonResult<T>?error(CommonResult<?>?result)?{return?error(result.getCode(),?result.getMessage());}public?static?<T>?CommonResult<T>?error(Integer?code,?String?message)?{Assert.isTrue(!CODE_SUCCESS.equals(code),?"code 必須是錯誤的!");CommonResult<T>?result?=?new?CommonResult<>();result.code?=?code;result.message?=?message;return?result;}public?static?<T>?CommonResult<T>?success(T?data)?{CommonResult<T>?result?=?new?CommonResult<>();result.code?=?CODE_SUCCESS;result.data?=?data;result.message?=?"";return?result;}@JsonIgnore?//?忽略,避免?jackson?序列化給前端public?boolean?isSuccess()?{?//?方便判斷是否成功return?CODE_SUCCESS.equals(code);}@JsonIgnore?//?忽略,避免?jackson?序列化給前端public?boolean?isError()?{?//?方便判斷是否失敗return?!isSuccess();}//?...?省略?setting/getting/toString?方法}
  • 每個字段,胖友自己看相應的注釋。

4.4 GlobalResponseBodyHandler

在?cn.iocoder.springboot.lab27.springwebflux.core.web?包路徑,創建 GlobalResponseBodyHandler 類,全局統一返回的處理器。代碼如下:

//?GlobalResponseBodyHandler.javapublic?class?GlobalResponseBodyHandler?extends?ResponseBodyResultHandler?{private?static?Logger?LOGGER?=?LoggerFactory.getLogger(GlobalResponseBodyHandler.class);private?static?MethodParameter?METHOD_PARAMETER_MONO_COMMON_RESULT;private?static?final?CommonResult?COMMON_RESULT_SUCCESS?=?CommonResult.success(null);static?{try?{//?<1>?獲得 METHOD_PARAMETER_MONO_COMMON_RESULT 。其中?-1 表示?`#methodForParams()`?方法的返回值METHOD_PARAMETER_MONO_COMMON_RESULT?=?new?MethodParameter(GlobalResponseBodyHandler.class.getDeclaredMethod("methodForParams"),?-1);}?catch?(NoSuchMethodException?e)?{LOGGER.error("[static][獲取?METHOD_PARAMETER_MONO_COMMON_RESULT?時,找不都方法");throw?new?RuntimeException(e);}}public?GlobalResponseBodyHandler(List<HttpMessageWriter<?>>?writers,?RequestedContentTypeResolver?resolver)?{super(writers,?resolver);}public?GlobalResponseBodyHandler(List<HttpMessageWriter<?>>?writers,?RequestedContentTypeResolver?resolver,?ReactiveAdapterRegistry?registry)?{super(writers,?resolver,?registry);}@Override@SuppressWarnings("unchecked")public?Mono<Void>?handleResult(ServerWebExchange?exchange,?HandlerResult?result)?{Object?returnValue?=?result.getReturnValue();Object?body;//?<1.1>?處理返回結果為?Mono?的情況if?(returnValue?instanceof?Mono)?{body?=?((Mono<Object>)?result.getReturnValue()).map((Function<Object,?Object>)?GlobalResponseBodyHandler::wrapCommonResult).defaultIfEmpty(COMMON_RESULT_SUCCESS);//?<1.2>?處理返回結果為?Flux?的情況}?else?if?(returnValue?instanceof?Flux)?{body?=?((Flux<Object>)?result.getReturnValue()).collectList().map((Function<Object,?Object>)?GlobalResponseBodyHandler::wrapCommonResult).defaultIfEmpty(COMMON_RESULT_SUCCESS);//?<1.3>?處理結果為其它類型}?else?{body?=?wrapCommonResult(returnValue);}//?<2>return?writeBody(body,?METHOD_PARAMETER_MONO_COMMON_RESULT,?exchange);}private?static?Mono<CommonResult>?methodForParams()?{return?null;}private?static?CommonResult<?>?wrapCommonResult(Object?body)?{//?如果已經是?CommonResult?類型,則直接返回if?(body?instanceof?CommonResult)?{return?(CommonResult<?>)?body;}//?如果不是,則包裝成?CommonResult?類型return?CommonResult.success(body);}}
  • 繼承 WebFlux 的 ResponseBodyResultHandler 類,因為該類將 Response 的 body 寫回給前端。所以,我們通過重寫該類的?#handleResult(ServerWebExchange exchange, HandlerResult result)?方法,將返回結果進行使用 CommonResult 包裝。

  • <1>?處,獲得?METHOD_PARAMETER_MONO_COMMON_RESULT?。其中?-1?表示?#methodForParams()?方法的返回值類型?Mono<CommonResult>?。后續我們在#handleResult(ServerWebExchange exchange, HandlerResult result)?方法中,會使用到?METHOD_PARAMETER_MONO_COMMON_RESULT?。

  • 重寫?#handleResult(ServerWebExchange exchange, HandlerResult result)?方法,將返回結果進行使用 CommonResult 包裝。

    • <1.1>?處,處理返回結果為 Mono 的情況。通過調用?Mono#map(Function<? super T, ? extends R> mapper)?方法,將原返回結果,進行包裝成?CommonResult<?>?。

    • <1.2>?處,處理返回結果為 Flux 的情況。先通過調用?Flux#collectList()?方法,將其轉換成?Mono<List<T>>?對象,后續就是和?<1.1>?相同的邏輯。

    • <1.3>?處,處理結果為其它類型的情況,直接進行包裝成?CommonResult<?>?。

  • <2>?處,調用父類方法?#writeBody(Object body, MethodParameter bodyParameter, ServerWebExchange exchange)?方法,實現將結果寫回給前端。

在思路上,和 SpringMVC 使用 ResponseBodyAdvice +?@ControllerAdvice?注解,是一致的。只是說,WebFlux 暫時沒有提供這樣的方式,所以咱只好通過繼承 ResponseBodyResultHandler 類,重寫其?#handleResult(ServerWebExchange exchange, HandlerResult result)?方法,將返回結果進行使用 CommonResult 包裝。

4.5 WebFluxConfiguration

在?cn.iocoder.springboot.lab27.springwebflux.config?包路徑下,創建 WebFluxConfiguration 配置類。代碼如下:

//?WebFluxConfiguration.java@Configuration public?class?WebFluxConfiguration?{@Beanpublic?GlobalResponseBodyHandler?responseWrapper(ServerCodecConfigurer?serverCodecConfigurer,RequestedContentTypeResolver?requestedContentTypeResolver)?{return?new?GlobalResponseBodyHandler(serverCodecConfigurer.getWriters(),?requestedContentTypeResolver);}}
  • 在?#responseWrapper(serverCodecConfigurer, requestedContentTypeResolver)?方法中,我們創建了 4.4 GlobalResponseBodyHandler Bean 對象,實現對返回結果的包裝。

4.6 UserController

在?cn.iocoder.springboot.lab27.springwebflux.controller?包路徑下,創建 UserController 類。代碼如下:

//?UserController.java@RestController @RequestMapping("/users") public?class?UserController?{/***?查詢用戶列表**?@return?用戶列表*/@GetMapping("/list")public?Flux<UserVO>?list()?{//?查詢列表List<UserVO>?result?=?new?ArrayList<>();result.add(new?UserVO().setId(1).setUsername("yudaoyuanma"));result.add(new?UserVO().setId(2).setUsername("woshiyutou"));result.add(new?UserVO().setId(3).setUsername("chifanshuijiao"));//?返回列表return?Flux.fromIterable(result);}/***?獲得指定用戶編號的用戶**?@param?id?用戶編號*?@return?用戶*/@GetMapping("/get")public?Mono<UserVO>?get(@RequestParam("id")?Integer?id)?{//?查詢用戶UserVO?user?=?new?UserVO().setId(id).setUsername("username:"?+?id);//?返回return?Mono.just(user);}/***?獲得指定用戶編號的用戶**?@param?id?用戶編號*?@return?用戶*/@GetMapping("/get2")public?Mono<CommonResult<UserVO>>?get2(@RequestParam("id")?Integer?id)?{//?查詢用戶UserVO?user?=?new?UserVO().setId(id).setUsername("username:"?+?id);//?返回return?Mono.just(CommonResult.success(user));}/***?獲得指定用戶編號的用戶**?@param?id?用戶編號*?@return?用戶*/@GetMapping("/get3")public?UserVO?get3(@RequestParam("id")?Integer?id)?{//?查詢用戶UserVO?user?=?new?UserVO().setId(id).setUsername("username:"?+?id);//?返回return?user;}/***?獲得指定用戶編號的用戶**?@param?id?用戶編號*?@return?用戶*/@GetMapping("/get4")public?CommonResult<UserVO>?get4(@RequestParam("id")?Integer?id)?{//?查詢用戶UserVO?user?=?new?UserVO().setId(id).setUsername("username:"?+?id);//?返回return?CommonResult.success(user);}}
  • API 接口雖然比較多,但是我們可以先根據返回結果的類型,分成 Flux 和 Mono 兩類。然后,艿艿這里又創建了 Mono 分類的四種情況的接口,就是?/users/get、/users/get2、/users/get3、/users/get4?四個。胖友看下這四個接口的返回結果的類型,很容易就明白了。

  • 在?#get(Integer id)?方法,返回的結果是 UserVO 類型。這樣,結果會被 GlobalResponseBodyHandler 攔截,包裝成 CommonResult 類型返回。請求結果如下:

    {"code":?0,"message":?"","data":?{"id":?10,"username":?"username:10"} }
    • 會有?"message": ""?的返回的原因是,我們使用 SpringMVC 提供的 Jackson 序列化,對于 CommonResult 此時的?message = null?的情況下,會序列化它成?"message": ""?返回。實際情況下,不會影響前端處理。

  • 在?# get2(Integer id)?方法,返回的結果是?Mono<Common<UserVO>>?類型。結果雖然也會被 GlobalResponseBodyHandler 處理,但是不會二次再重復包裝成 CommonResult 類型返回。

5. 全局異常處理

示例代碼對應倉庫:lab-27-webflux-02 。

在 「4. 全局統一返回」 中,我們已經定義了使用 CommonResult 全局統一返回,并且看到了成功返回的示例與代碼。這一小節,我們主要是來全局異常處理,最終能也是通過 CommonResult 返回。

那么,我們就不嗶嗶,直接看著示例代碼,遨游起來。

友情提示:該示例,基于 「4. 全局統一返回」 的 lab-27-webflux-02 的基礎上,繼續改造。

5.1 ServiceExceptionEnum

在?cn.iocoder.springboot.lab27.springwebflux.constants?包路徑,創建 ServiceExceptionEnum 枚舉類,枚舉項目中的錯誤碼。代碼如下:

//?ServiceExceptionEnum.javapublic?enum?ServiceExceptionEnum?{//?==========?系統級別?==========SUCCESS(0,?"成功"),SYS_ERROR(2001001000,?"服務端發生異常"),MISSING_REQUEST_PARAM_ERROR(2001001001,?"參數缺失"),//?==========?用戶模塊?==========USER_NOT_FOUND(1001002000,?"用戶不存在"),//?==========?訂單模塊?==========//?==========?商品模塊?==========;/***?錯誤碼*/private?int?code;/***?錯誤提示*/private?String?message;ServiceExceptionEnum(int?code,?String?message)?{this.code?=?code;this.message?=?message;}//?...?省略?getting?方法}
  • 因為錯誤碼是全局的,最好按照模塊來拆分。如下是艿艿在 onemall 項目的實踐:

    /***?服務異常**?參考?https://www.kancloud.cn/onebase/ob/484204?文章**?一共?10?位,分成四段**?第一段,1?位,類型*??????1?-?業務級別異常*??????2?-?系統級別異常*?第二段,3?位,系統類型*??????001?-?用戶系統*??????002?-?商品系統*??????003?-?訂單系統*??????004?-?支付系統*??????005?-?優惠劵系統*??????...?-?...*?第三段,3?位,模塊*??????不限制規則。*??????一般建議,每個系統里面,可能有多個模塊,可以再去做分段。以用戶系統為例子:*??????????001?-?OAuth2?模塊*??????????002?-?User?模塊*??????????003?-?MobileCode?模塊*?第四段,3?位,錯誤碼*???????不限制規則。*???????一般建議,每個模塊自增。*/

5.2 ServiceException

我們在一起討論下 Service 邏輯異常的時候,如何進行返回。這里的邏輯異常,我們指的是,例如說用戶名已經存在,商品庫存不足等。一般來說,常用的方案選擇,有兩種:

  • 封裝統一的業務異常類 ServiceException ,里面有錯誤碼和錯誤提示,然后進行?throws?拋出。

  • 封裝通用的返回類 CommonResult ,里面有錯誤碼和錯誤提示,然后進行?return?返回。

一開始,我們選擇了 CommonResult ,結果發現如下情況:

  • 因為 Spring?@Transactional?聲明式事務,是基于異常進行回滾的,如果使用 CommonResult 返回,則事務回滾會非常麻煩。

  • 當調用別的方法時,如果別人返回的是 CommonResult 對象,還需要不斷的進行判斷,寫起來挺麻煩的。

所以,后來我們采用了拋出業務異常 ServiceException 的方式。

在?cn.iocoder.springboot.lab27.springwebflux.core.exception?包路徑,創建 ServiceException 異常類,繼承 RuntimeException 異常類,用于定義業務異常。代碼如下:

//?ServiceException.javapublic?final?class?ServiceException?extends?RuntimeException?{/***?錯誤碼*/private?final?Integer?code;public?ServiceException(ServiceExceptionEnum?serviceExceptionEnum)?{//?使用父類的?message?字段super(serviceExceptionEnum.getMessage());//?設置錯誤碼this.code?=?serviceExceptionEnum.getCode();}//?...?省略?getting?方法}
  • 提供傳入?serviceExceptionEnum?參數的構造方法。具體的處理,看下代碼和注釋。

5.3 GlobalExceptionHandler

在?cn.iocoder.springboot.lab27.springwebflux.core.web?包路徑,創建 GlobalExceptionHandler 類,全局統一返回的處理器。代碼如下:

//?GlobalExceptionHandler.java@ControllerAdvice(basePackages?=?"cn.iocoder.springboot.lab27.springwebflux.controller") public?class?GlobalExceptionHandler?{private?Logger?logger?=?LoggerFactory.getLogger(getClass());/***?處理?ServiceException?異常*/@ResponseBody@ExceptionHandler(value?=?ServiceException.class)public?CommonResult?serviceExceptionHandler(ServiceException?ex)?{logger.debug("[serviceExceptionHandler]",?ex);//?包裝?CommonResult?結果return?CommonResult.error(ex.getCode(),?ex.getMessage());}/***?處理?ServerWebInputException?異常**?WebFlux?參數不正確*/@ResponseBody@ExceptionHandler(value?=?ServerWebInputException.class)public?CommonResult?serverWebInputExceptionHandler(ServerWebInputException?ex)?{logger.debug("[ServerWebInputExceptionHandler]",?ex);//?包裝?CommonResult?結果return?CommonResult.error(ServiceExceptionEnum.MISSING_REQUEST_PARAM_ERROR.getCode(),ServiceExceptionEnum.MISSING_REQUEST_PARAM_ERROR.getMessage());}/***?處理其它?Exception?異常*/@ResponseBody@ExceptionHandler(value?=?Exception.class)public?CommonResult?exceptionHandler(Exception?e)?{//?記錄異常日志logger.error("[exceptionHandler]",?e);//?返回?ERROR?CommonResultreturn?CommonResult.error(ServiceExceptionEnum.SYS_ERROR.getCode(),ServiceExceptionEnum.SYS_ERROR.getMessage());}}
  • 在 WebFlux 中,可以使用通過實現 ResponseBodyAdvice 接口,并添加?@ControllerAdvice?接口,攔截 Controller 的返回結果。注意,我們這里?@ControllerAdvice?注解,設置了?basePackages?屬性,只攔截?"cn.iocoder.springboot.lab27.springwebflux.controller"?包,也就是我們定義的 Controller 。為什么呢?因為在項目中,我們可能會引入 Swagger 等庫,也使用 Controller 提供 API 接口,那么我們顯然不應該讓 GlobalResponseBodyHandler 去攔截這些接口,畢竟它們并不需要我們去替它們做全局統一的返回

  • 我們定義了三個方法,通過添加?@ExceptionHandler?注解,定義每個方法對應處理的異常。并且,也添加了?@ResponseBody?注解,標記直接使用返回結果作為 API 的響應。

  • #serviceExceptionHandler(...)?方法,攔截處理 ServiceException 業務異常,直接使用該異常的?code?+?message?屬性,構建出 CommonResult 對象返回。

  • #serverWebInputExceptionHandler(...)?方法,攔截處理 ServerWebInputException 請求參數異常,構建出錯誤碼為?ServiceExceptionEnum.MISSING_REQUEST_PARAM_ERROR?的 CommonResult 對象返回。

  • #exceptionHandler(...)?方法,攔截處理 Exception 異常,構建出錯誤碼為?ServiceExceptionEnum.SYS_ERROR?的 CommonResult 對象返回。這是一個兜底的異常處理,避免有一些其它異常,我們沒有在 GlobalExceptionHandler 中,提供自定義的處理方式。

注意,在?#exceptionHandler(...)?方法中,我們還多使用?logger?打印了錯誤日志,方便我們接入 ELK 等日志服務,發起告警,通知我們去排查解決。如果胖友的系統里暫時沒有日志服務,可以記錄錯誤日志到數據庫中,也是不錯的選擇。而其它兩個方法,因為是更偏業務的,相對正常的異常,所以無需記錄錯誤日志。

5.4 UserController

在 UserController 類中,我們添加兩個 API 接口,拋出異常,方便我們測試全局異常處理的效果。代碼如下:

//?UserController.java/***?測試拋出?NullPointerException?異常*/ @GetMapping("/exception-01") public?UserVO?exception01()?{throw?new?NullPointerException("沒有粗面魚丸"); }/***?測試拋出?ServiceException?異常*/ @GetMapping("/exception-02") public?UserVO?exception02()?{throw?new?ServiceException(ServiceExceptionEnum.USER_NOT_FOUND); }
  • 在?#exception01()?方法,拋出 NullPointerException 異常。這樣,異常會被?GlobalExceptionHandler#exceptionHandler(...)?方法來攔截,包裝成 CommonResult 類型返回。請求結果如下:

    {"code":?2001001000,"message":?"服務端發生異常","data":?null }
  • 在?#exception02()?方法,拋出 ServiceException 異常。這樣,異常會被?GlobalExceptionHandler#serviceExceptionHandler(...)?方法來攔截,包裝成 CommonResult 類型返回。請求結果如下:

    {"code":?1001002000,"message":?"用戶不存在","data":?null }

5.5 簡單小結

采用?ControllerAdvice?+?@ExceptionHandler?注解的方式,可以很方便的實現 WebFlux 的全局異常處理。不過這種方案存在一個弊端,不支持 WebFlux 的基于函數式編程方式。不過考慮到,絕大多數情況下,我們并不會采用基于函數式編程方式,所以這種方案還是沒問題的。看了下 WebFlux 的官方文檔,也是推薦這種方案,詳細可見 《Web on Reactive Stack —— Spring WebFlux —— Managing Exceptions》 。

如果胖友真的需要支持 WebFlux 的基于函數式編程方式,可以看看 《Handling Errors in Spring WebFlux》 文章,通過繼承?org.springframework.boot.autoconfigure.web.reactive.error.AbstractErrorWebExceptionHandler?抽象類,實現自定義的全局異常處理器。

6. WebFilter 過濾器

示例代碼對應倉庫:lab-27-webflux-02 。

在 SpringMVC 中,我們可以通過實現 HandlerInterceptor 接口,攔截 SpringMVC 處理請求的過程,自定義前置和處理的邏輯。不了解這塊的胖友,可以看看 《芋道 Spring Boot SpringMVC 入門》 的 「6. HandlerInterceptor 攔截器」 小節。

在 WebFlux 中,我們可以通過實現 WebFilter 接口,過濾 WebFlux 處理請求的過程,自定義前置和處理的邏輯。該接口代碼如下:

//?DemoWebFilterWebFilter.java/***?Contract?for?interception-style,?chained?processing?of?Web?requests?that?may*?be?used?to?implement?cross-cutting,?application-agnostic?requirements?such*?as?security,?timeouts,?and?others.**?@author?Rossen?Stoyanchev*?@since?5.0*/ public?interface?WebFilter?{/***?Process?the?Web?request?and?(optionally)?delegate?to?the?next*?{@code?WebFilter}?through?the?given?{@link?WebFilterChain}.*?@param?exchange?the?current?server?exchange*?@param?chain?provides?a?way?to?delegate?to?the?next?filter*?@return?{@code?Mono<Void>}?to?indicate?when?request?processing?is?complete*/Mono<Void>?filter(ServerWebExchange?exchange,?WebFilterChain?chain);}
  • 因為 WebFilterChain 的?#filter(ServerWebExchange exchange)?方法,返回的是?Mono<Void>?對象,所以可以進行各種 Reactor 的操作。咳咳咳,當然需要胖友比較了解 Reactor 的使用,我們才能實現出的 WebFilter ,否則會覺得挺難用的。

  • 另外,WebFilterChain 是由多個 WebFilter 過濾器組成的鏈,其默認的實現為 DefaultWebFilterChain 。

  • 總體來說,從形態上和我們在 Servlet 看到的 FilterChain 和 Filter 是比較相似的,只是因為結合了 Reactor 響應式編程,所以編寫時,差異蠻大的。

6.1 DemoWebFilter

下面,讓我們來編寫一個簡單的 WebFilter 示例。

在?cn.iocoder.springboot.lab27.springwebflux.core.filter?包路徑,創建 DemoWebFilter 類,一個簡單的 WebFilter 示例。代碼如下:

//?DemoWebFilter.java@Component @Order(1) public?class?DemoWebFilter?implements?WebFilter?{private?Logger?logger?=?LoggerFactory.getLogger(getClass());@Overridepublic?Mono<Void>?filter(ServerWebExchange?serverWebExchange,?WebFilterChain?webFilterChain)?{//?<1>?繼續執行請求return?webFilterChain.filter(serverWebExchange).doOnSuccess(new?Consumer<Void>()?{?//?<2>?執行成功后回調@Overridepublic?void?accept(Void?aVoid)?{logger.info("[accept][執行成功]");}});}}
  • 在類上,添加?@Component?注解,創建 DemoWebFilter Bean 對象。這樣,該過濾器就已經加入了 WebFlux 的過濾器鏈中。目前,暫未內置支持根據請求路徑 uri 等條件來配置是否過濾,需要我們自己在實現?#filter(serverWebExchange, webFilterChain)?方法來完成。

  • 在類上,添加?@Order?注解,設置過濾器的順序。

  • 實現?#filter(serverWebExchange, webFilterChain)?方法,實現在請求執行完成后,打印一條執行成功的日志。

    • <1>?處,調用?WebFilterChain#filter(exchange)?方法,交給過濾器中的下一個過濾器,繼續進行過濾處理,并返回?Mono<Void>?對象。

    • <2>?處,調用?Mono#doOnSuccess(Consumer<? super T> onSuccess)?方法,實現在請求執行完成后,打印一條執行成功的日志。這里,我們可以參考 《Reactor 文檔 —— Mono》 ,實現各種其它操作。

😈 在后面的小節中,我們會看一個實現處理 Cors 跨域的 CorsWebFilter ,對理解 WebFilter 有一定的幫助。

6.2 Filtering Handler Functions

在基于函數式編程方式中,可以使用如下的方式,實現對每個路由的過濾處理。代碼如下:

//?UserRouter.java@Bean public?RouterFunction<ServerResponse>?demo2RouterFunction()?{return?route(GET("/users2/demo2"),?request?->?ok().bodyValue("demo")).filter(new?HandlerFilterFunction<ServerResponse,?ServerResponse>()?{@Overridepublic?Mono<ServerResponse>?filter(ServerRequest?request,?HandlerFunction<ServerResponse>?next)?{return?next.handle(request).doOnSuccess(new?Consumer<ServerResponse>()?{?//?執行成功后回調@Overridepublic?void?accept(ServerResponse?serverResponse)?{logger.info("[accept][執行成功]");}});}}); }

因為實際場景下,使用到基于函數式編程方式比較少,這里就不擴展開來講。感興趣的胖友,可以看看 《Web on Reactive Stack —— Spring WebFlux —— Filtering Handler Functions》 文檔。

7. Servlet、Filter、Listener

目前測試下來,java.servlet?提供的 Servlet、Filter、Listener 組件,無法在 WebFlux 中使用。測試的示例,可見 lab-27-webflux-03 。

艿艿翻了下 Spring Security 對 WebFlux 的支持,也是通過實現 WebFlux 接口的 WebFilterChainProxy 過濾器,即在 「6. WebFilter 過濾器」 中看到的內容。

8. Cors 跨域

超過微信文章長度限制,請訪問 http://www.iocoder.cn/Spring-Boot/WebFlux/

9. 集成響應式的 MongoDB

超過微信文章長度限制,請訪問 http://www.iocoder.cn/Spring-Boot/WebFlux/

10. 集成響應式的 Redis

超過微信文章長度限制,請訪問 http://www.iocoder.cn/Spring-Boot/WebFlux/

11. 集成響應式的 Elasticsearch

超過微信文章長度限制,請訪問 http://www.iocoder.cn/Spring-Boot/WebFlux/

12. 整合響應式的 JPA

超過微信文章長度限制,請訪問 http://www.iocoder.cn/Spring-Boot/WebFlux/

13. 整合響應式的 R2DBC 和事務

超過微信文章長度限制,請訪問 http://www.iocoder.cn/Spring-Boot/WebFlux/

14. 其他內容

超過微信文章長度限制,請訪問 http://www.iocoder.cn/Spring-Boot/WebFlux/

666. 彩蛋

至此,我們已經完成了 Spring WebFlux 的簡單入門。如果用一句簡單的話來概括 WebFlux 的話,那就是:

  • WebFlux 在 Spring Framework 5 推出的,以 Reactor 庫為基礎,基于異步和事件驅動,實現的響應式 Web 開發框架。

  • WebFlux 能夠充分利用多核 CPU 的硬件資源,處理大量的并發請求。因此,可以在不擴充硬件的資源的情況下,提升系統的吞吐性和伸縮性。

注意,這里我們提到的是吞吐性和伸縮性,而不是提升每個請求的性能。我們來回想下整個 WebFlux 的執行過程:請求是被作為一個事件丟到線程池中執行,等到執行完畢,異步回調結果給主線程,最后返回給前端。

那么整個過程,相比 SpringMVC 的執行過程來說,至少多了一次線程的上下文切換。我們都知道,線程的切換是有成本的。所以,單看一個請求的處理,SpringMVC 的性能是優于 WebFlux 的。

我們上文提到的主線程,一般來說就是 IO 線程。

但是,由于 WebFlux 的 IO 線程是非阻塞的,可以不斷解析請求,丟到線程池中執行。而 SpringMVC 的 IO 線程是阻塞的,需要等到請求被處理完畢,才能解析下一個請求并進行處理。這樣,隨著每個請求的被處理時間越長、并發請求的量級越大,WebFlux 相比 SpringMVC 的整體吞吐量高的越多,平均的請求響應時間越短。如下圖所示:

性能對比

從圖中,我們可以看到,隨著并發請求量的增大,WebFlux 的響應時間平穩在 100ms 左右,而 SpringMVC 的響應式時間從 3000 并發量開始,響應時間直線上升。😈 感興趣的胖友,可以參考如下文章,自己做一波性能的基準測試:

  • 《性能測試 —— SpringMVC、Webflux 基準測試》

  • 《性能測試 —— Spring Cloud Gateway、Zuul 基準測試》

  • 《WebFlux 性能測試》

  • 《WebFlux 性能問題和適用場景》

那么什么場景下的服務,適合使用 WebFlux 呢?我們可以把任務分成 IO 密集型和 CPU 密集型,而服務本質上,是執行一個又一個的任務,所以也可以這么分。😈 不了解 IO 密集型和 CPU 密集型的胖友,可以先看下 《計算密集型和 IO 密集型》 文章。

而我們業務中編寫的代碼,都無一幸免需要跟 MySQL、MongoDB、Elasticsearch 等數據庫打交道,又或者跟 Redis、Memcached 等緩存服務打交道,還或者需要跟 RocketMQ、RabbitMQ、Kafka 等消息隊列打交道。無論這些中間件做的多牛逼,性能多么掉渣天,我們都無法避免會經過網絡 IO 和磁盤 IO 。所以,我們提供的服務,大多數都是 IO 密集型。很少會存在,直接從內存讀取數據,直接返回的情況。

**因此,我們業務中編寫的代碼,絕大多多多數都是 IO 密集型,都是適合使用 WebFlux 的。**但是,響應式編程對開發人員的編碼能力要求會比較高,一旦腦子一抽,在 IO 線程中編寫了阻塞代碼,反倒出現性能下滑。具體可以看看艿艿在 《性能測試 —— SpringMVC、Webflux 基準測試》 提供的測試示例,明明白白的。

艿艿建議的話,如果考慮使用 WebFlux 的話,一定要把 Reactor 好好學習下,不然真的是做廝大發好。同時,每次上線之前,對使用 WebFlux 編寫的服務,做下性能測試,可以發現編寫不正確的地方,找到阻塞 IO 線程的邏輯。

目前,暫時找不到大規模使用 WebFlux 的業務開源項目,最大使用 WebFlux 構建的開源項目,就是 Spring Cloud 開源的網關 Spring Cloud Gateway 。😈 可能,WebFlux 或者響應式編程最好的歸宿,暫時是中間件。如果胖友有看過 Dubbo 的線程模型,就會發現和 WebFlux 是異曲同工之妙。

OK ,嗶嗶結束~如果胖友想要進一步了解 WebFlux 的話,不煩看看 Spring Cloud Gateway 的源碼,可以看看艿艿寫的 《芋道 Spring Cloud Gateway 源碼解析》 。

總結

以上是生活随笔為你收集整理的艿艿连肝了几个周末,写了一篇贼长的 Spring 响应式 Web 框架 WebFlux!市面第二完整~的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。

国产电影一区二区三区四区 | 国产精品美女999 | 日韩天堂在线观看 | 四虎影视www | bbb搡bbb爽爽爽 | 国产剧情在线一区 | 久久成人麻豆午夜电影 | 久久久国产一区二区 | 在线三级av | 中文字幕亚洲综合久久五月天色无吗'' | 韩国精品一区二区三区六区色诱 | 日韩国产精品久久久久久亚洲 | 久久免费在线观看视频 | a资源在线 | 亚洲色图激情文学 | 国产高清综合 | 久久久久国产精品厨房 | 久久久久久久久网站 | 天天骚夜夜操 | 日韩精品视频免费看 | 丁香六月婷婷 | 96看片| 中文字幕在线观看免费高清电影 | 国产无套精品久久久久久 | 中文字幕成人 | 久久美女精品 | 日韩综合一区二区 | 伊人久久国产 | 欧美日韩一级视频 | 深夜福利视频在线观看 | 亚洲国产日韩一区 | 久久国产一区 | 韩日三级在线 | 96超碰在线| 99中文字幕在线观看 | 香蕉网在线播放 | 91精品国产高清自在线观看 | 在线观看网站你懂的 | 草草草影院 | 黄视频色网站 | 日韩欧美国产激情在线播放 | 国产精品乱码在线 | 九九热1| 91精品视频一区二区三区 | 99精品视频在线观看视频 | 99热这里精品 | 国产色视频 | 欧美激情在线看 | 黄色的网站免费看 | 久久久久久久久久久国产精品 | 婷婷 中文字幕 | 男女精品久久 | 日日干激情五月 | 亚洲黄色一级电影 | 国产精品免费av | 国产一区免费视频 | 亚洲成人软件 | 国产在线观看免费 | 在线视频 一区二区 | 久久精品免费观看 | 色国产视频 | 久草视频网 | 成人国产精品一区二区 | 国产美女视频免费观看的网站 | 精品久久久久久国产 | 国产精品第二页 | 国产精品国产三级国产专区53 | 国产成人福利在线观看 | 天天操天天操天天操天天操 | 久久久免费看片 | 插婷婷| 91资源在线 | 欧美极品xxxxx| 国产精品综合久久久久久 | 日本黄色免费观看 | 日本老少交 | 久久综合免费视频 | 亚洲 中文 欧美 日韩vr 在线 | 亚洲视频在线观看网站 | 精品亚洲午夜久久久久91 | 久久久国产精华液 | 香蕉视频在线网站 | 日韩一级精品 | 欧美做受高潮 | 色激情五月| 福利视频| 狠狠色丁香久久婷婷综 | 天天色图 | 日韩欧美在线观看一区二区 | 成人毛片在线视频 | 色搞搞| 麻豆影视网站 | 狠狠干,狠狠操 | 免费网站观看www在线观看 | 四虎影视国产精品免费久久 | 天天艹天天干天天 | 国产精品毛片久久久久久 | 久久男人视频 | 久久一区国产 | 懂色av一区二区在线播放 | 国产精品淫片 | 久久久精品久久日韩一区综合 | 国产一级在线免费观看 | 国产一区二区久久久久 | 国产精品久久久久久久久久白浆 | 成人性生交视频 | 国产精品免费观看在线 | 二区三区av | 免费在线观看av网址 | 午夜视频在线观看一区二区三区 | 三级免费黄色 | 伊人五月婷 | 五月天婷亚洲天综合网鲁鲁鲁 | 中文字幕在线成人 | 国产一级二级在线 | 国产123av| 丁香婷婷激情国产高清秒播 | 久久久久免费精品视频 | 国产 一区二区三区 在线 | 国模视频一区二区三区 | 色伊人网 | 久久综合网色—综合色88 | 欧美日韩国产亚洲乱码字幕 | 国产在线中文 | 欧美激情第八页 | 欧美日韩国产二区 | 天天干天天色2020 | 欧美在线视频第一页 | 久草视频在线资源 | 一区二区精品在线视频 | 天堂av一区二区 | 亚洲男男gaygayxxxgv| 久久精品视频在线看 | 91mv.cool在线观看 | 中文字幕在线观看一区二区 | 黄色三级免费片 | 91热精品 | 国产成人精品一区一区一区 | 99精品一级欧美片免费播放 | 91豆麻精品91久久久久久 | 91大神在线观看视频 | 日韩一区二区三区高清在线观看 | 国产高清不卡在线 | 精品国产乱码一区二区三区在线 | av福利电影 | 国产高清视频在线播放一区 | 久久久久久久久久毛片 | 日韩视频一区二区在线 | 久久精品96 | 91麻豆精品国产91久久久更新时间 | 国产小视频免费在线网址 | 免费av影视 | 亚洲男男gaygay无套同网址 | 国产精品夜夜夜一区二区三区尤 | 麻豆传媒一区二区 | 在线观看成人小视频 | 欧美极品xxxx | 色资源网免费观看视频 | 久久久www免费电影网 | 久久精品网| 欧美韩日精品 | 国产91免费看 | 又黄又爽又湿又无遮挡的在线视频 | 日本在线观看一区二区三区 | 午夜精品一区二区国产 | 日本公乱妇视频 | 欧美一区二区三区在线 | 午夜精品久久久久久中宇69 | 91麻豆精品国产91 | 中文字幕在线成人 | 97人人添人澡人人爽超碰动图 | 久久国产亚洲精品 | 91精品免费在线视频 | 日韩激情小视频 | 国产高清视频在线播放一区 | 久久久一本精品99久久精品 | 国产成人久久av | 1024手机基地在线观看 | 9在线观看免费高清完整版在线观看明 | av最新资源 | 少妇性xxx | 免费在线观看成人av | 国产第一页在线播放 | 日韩在线视频精品 | 美女久久久久久久 | .国产精品成人自产拍在线观看6 | 久久视频国产精品免费视频在线 | 色婷婷亚洲婷婷 | 一本一道波多野毛片中文在线 | 欧美激情第八页 | www.玖玖玖| 福利一区二区 | 成人免费观看网站 | 亚洲精品欧洲精品 | 久久精品成人热国产成 | 69国产盗摄一区二区三区五区 | 中文在线a天堂 | 99视屏 | 最近免费中文字幕 | 亚洲综合精品视频 | 亚洲最新av在线 | 成 人 黄 色 视频播放1 | 久草在线最新 | 中文字幕专区高清在线观看 | 欧美激情xxxx | 天天干天天玩天天操 | 三级黄色理论片 | 91在线视频导航 | 在线看免费 | 免费看的黄色 | 久久理论片 | 色综合天天视频在线观看 | 超碰人在线 | 日韩精品免费一线在线观看 | 久久久久久国产精品 | 色综合天天射 | 久久精品免费观看 | www.久草视频 | 国产免费三级在线观看 | 91在线精品秘密一区二区 | 精品字幕在线 | 精品久久久久久综合日本 | 国产黄a三级三级三级三级三级 | 最新午夜 | 欧美性色综合网 | 99999精品视频 | 免费在线观看国产黄 | 久草网视频| 中文字幕免费成人 | 中文资源在线播放 | 日日添夜夜添 | 香蕉视频国产在线观看 | 亚州精品一二三区 | 久久人人添人人爽添人人88v | 国产精品久久久久久久久免费看 | 欧美成人视| 久久精品国产免费看久久精品 | 久久男女视频 | 久久视频二区 | 国产资源在线观看 | 国产啊v在线 | h动漫中文字幕 | 国产精品麻豆三级一区视频 | 日韩欧美一区二区三区在线 | 综合久久久 | 午夜国产在线观看 | 国产美女精品久久久 | 国产在线观看二区 | 中文字幕专区高清在线观看 | 国产 欧美 在线 | 日韩视频区| 国产精彩在线视频 | 亚洲国产剧情av | 天天做天天射 | 91香蕉视频黄色 | 在线国产小视频 | 日本在线中文 | 日韩精品黄 | 国产精品国产亚洲精品看不卡 | 国产精品乱码一区二区视频 | 国产免费一区二区三区最新 | 久久久久欧美精品999 | 中文网丁香综合网 | 日本久久影视 | 精品国产中文字幕 | 中文字幕av在线不卡 | 999久久国精品免费观看网站 | 国产成人精品久久二区二区 | 久久久香蕉视频 | 亚洲国产wwwccc36天堂 | 色婷婷视频在线 | 欧美日韩高清一区 | 亚洲精品美女在线观看 | 美女视频久久黄 | 欧美在线一二区 | 日韩高清在线一区 | www.com久久| av网址在线播放 | 最近免费在线观看 | 国产精品入口久久 | 日本三级国产 | 国产在线传媒 | 人人干网 | 国产精品毛片一区视频播不卡 | 国产系列在线观看 | 亚洲一区日韩在线 | 一本—道久久a久久精品蜜桃 | 亚洲国产欧美在线人成大黄瓜 | 三级黄色在线观看 | 波多野结衣一区 | 天天色图| 日本久久中文 | 国精产品999国精产品视频 | 国产99久久久国产精品免费看 | 日本在线观看黄色 | 91pony九色丨交换 | www.福利| 18国产精品福利片久久婷 | 国产精品麻豆免费版 | 色99在线 | 久久精品国产久精国产 | 日韩免费三级 | 欧美日韩精品在线免费观看 | 久草免费资源 | 国产中文 | 亚州精品国产 | 超碰国产在线 | 欧美日视频 | 激情婷婷在线观看 | 免费a现在观看 | 免费一级片在线观看 | 国产亚洲在线视频 | 日本福利视频在线 | 日本美女xx| 久久久久久草 | 一区二区 不卡 | 日本性视频 | 国产伦精品一区二区三区高清 | 911久久| av大全在线观看 | 91丨九色丨丝袜 | 在线免费观看涩涩 | 国产视频2021 | 最近最新最好看中文视频 | 69夜色精品国产69乱 | 亚洲黄在线观看 | 一区二区激情视频 | 久久精久久精 | 国产91精品久久久久 | 91成人久久| 在线 视频 一区二区 | 久久久久亚洲精品 | 免费日韩高清 | 国产99久久久精品视频 | 福利精品在线 | 99re国产视频| 久久99国产一区二区三区 | 婷婷色狠狠 | 91激情视频在线 | 高清视频一区二区三区 | 国产一区二区三区午夜 | 精品国产美女 | 成人avav| 国产一级精品在线观看 | 99久久综合狠狠综合久久 | 欧美精品乱码99久久影院 | 国产精品久久久久免费 | 黄色毛片在线观看 | 日韩在线免费观看视频 | 精品91| 日本中文字幕久久 | 色综合久久久久久久久五月 | 99久久这里有精品 | 丰满少妇在线 | 97视频在线观看视频免费视频 | 日韩国产高清在线 | 中文在线a∨在线 | 久av在线| 综合色久 | 蜜桃麻豆www久久囤产精品 | 丁香婷婷综合色啪 | 国产视频一区在线播放 | 婷婷六月综合亚洲 | 久久国产精品免费一区 | 国产精品大片免费观看 | 日韩高清精品一区二区 | 91人人人 | 欧美一级淫片videoshd | 久草精品网| 在线免费视频 你懂得 | 麻豆视频免费版 | 视频在线一区二区三区 | 亚洲成人av在线播放 | 91视频在线自拍 | 最新国产福利 | 99久久久国产精品免费99 | 亚州黄色一级 | 日韩有码在线观看视频 | a爱爱视频 | 久久久影院| 一区二区三区电影 | 国产三级久久久 | 欧美激情va永久在线播放 | 国产成人不卡 | 天天干 天天摸 天天操 | 日本大尺码专区mv | 99热精品久久 | 国产又粗又猛又黄又爽的视频 | 91精品网站在线观看 | 人人爽人人澡人人添人人人人 | 国产91精品一区二区绿帽 | 日p视频在线观看 | 国产综合精品一区二区三区 | 精品在线观看国产 | 国产在线观看地址 | 97国产精品久久 | 国产一区二区三区免费在线 | 最新三级在线 | 免费日韩| av中文字幕第一页 | 国产黄在线观看 | 免费三级影片 | 黄色国产大片 | 亚洲精品在线二区 | 成片视频免费观看 | 麻豆av一区二区三区在线观看 | 91av免费在线观看 | 97在线观看视频国产 | 成人小视频在线免费观看 | 免费观看国产视频 | 国产日韩精品欧美 | 视色网站 | 中文字幕日韩无 | 欧美污污网站 | 久久综合成人 | 国产精品久久久久久久毛片 | 中文字幕一区二区在线播放 | 国产精品九九视频 | 五月天免费网站 | 国产精品igao视频网网址 | 欧美日在线观看 | 久久免费播放 | 亚洲欧美在线综合 | 91久久久国产精品 | 综合久久久久久久久 | 99热9| 天天添夜夜操 | 综合激情 | 天天操人人干 | 99在线免费观看 | 国内精品久久久久久久影视麻豆 | 免费视频成人 | 日韩精品在线观看视频 | 91精品蜜桃 | 色视频网站在线观看一=区 a视频免费在线观看 | 欧美午夜寂寞影院 | 日本不卡123 | 91在线播放综合 | 中文综合在线 | 91成人短视频在线观看 | 欧美视屏一区二区 | 欧美日韩在线第一页 | 九九精品视频在线观看 | 久久爽久久爽久久av东京爽 | 国产一区免费视频 | 精品一区 在线 | 亚洲视频999 | 香蕉成人在线视频 | 国产亚洲精品综合一区91 | 丁香六月婷婷 | 国产成人精品一区二区在线 | 91精品国产网站 | 在线日韩亚洲 | 日韩欧美一区二区三区免费观看 | 亚洲国产精品视频 | 色综合色综合久久综合频道88 | 在线综合色 | 精品国产一区二区三区四区在线观看 | 日韩精品一区二区不卡 | 国产在线观看你懂得 | 国产高清视频在线观看 | 蜜桃视频在线视频 | 欧美午夜理伦三级在线观看 | 香蕉视频导航 | 日韩av专区 | 久久久久久久久久免费视频 | 久久综合久久综合这里只有精品 | 午夜精品久久久久久久99 | 欧美一进一出抽搐大尺度视频 | 在线成人短视频 | 日韩在线观看中文字幕 | 人人澡人人爱 | 制服丝袜天堂 | 亚洲精品乱码久久久久久久久久 | 日韩视 | 97国产精品视频 | 欧美日韩69 | 六月婷操 | 亚洲国产精品成人av | 国产麻豆精品在线观看 | 黄色毛片视频免费观看中文 | 日日操日日 | 亚洲干视频在线观看 | 久久大片 | 欧美一级免费 | 免费色视频网站 | 欧美精品久久久久久久久久丰满 | 国产精品激情偷乱一区二区∴ | 亚洲综合色丁香婷婷六月图片 | 国产精品99久久久 | 欧美精品国产精品 | 韩国精品福利一区二区三区 | 日韩精品中文字幕av | 91福利国产在线观看 | 国产精品国内免费一区二区三区 | 午夜国产在线观看 | 高清在线观看av | 在线看日韩| 国产黄网站在线观看 | 国产黄色a | 欧美在线观看视频一区二区三区 | 激情中文字幕 | 国产精品久久久久aaaa | 国产一级久久久 | 欧洲一区二区三区精品 | 国产精品一区二区三区视频免费 | 欧美精品久久人人躁人人爽 | 在线色资源 | 碰超人人 | 超碰99在线 | 久久成年人网站 | av天天色| 国产xx视频 | 91精品国产99久久久久久红楼 | 欧美激情奇米色 | www.久久99 | 欧美日韩高清一区二区 国产亚洲免费看 | 欧美日韩视频在线观看一区二区 | 草久视频在线 | 新版资源中文在线观看 | 国模精品在线 | 亚洲成人精品在线观看 | 免费在线激情电影 | 色噜噜日韩精品一区二区三区视频 | 波多野结衣理论片 | 日日噜噜噜噜夜夜爽亚洲精品 | 久久综合久久综合这里只有精品 | 久久综合久色欧美综合狠狠 | 午夜精品一区二区国产 | 九九精品久久久 | 成人av在线电影 | 黄色.com| 成年人在线免费看 | 玖玖爱免费视频 | 久久视频国产精品免费视频在线 | 国产成人一区二区啪在线观看 | www.狠狠操| 中文字幕视频网站 | 成人黄色av免费在线观看 | 国产色视频网站 | 国产网站在线免费观看 | 日日夜夜人人天天 | 久久久久久高潮国产精品视 | 中文字幕丝袜 | 狠狠操.com| 婷婷综合激情 | 成人a在线 | 精品国产伦一区二区三区免费 | 久久91久久久久麻豆精品 | 日本特黄一级片 | 久草视频资源 | 免费91在线观看 | 97免费视频在线 | 久久久99国产精品免费 | 国内亚洲精品 | 免费看黄视频 | 色av色av色av | 97视频在线观看免费 | 久久久久久久看片 | 国产香蕉视频在线观看 | 日本视频高清 | 日韩电影在线观看中文字幕 | 精品一二 | 天天爱天天色 | 久久久免费毛片 | 亚洲精品乱码久久久久久蜜桃不爽 | 最近高清中文字幕 | 国产日韩欧美在线影视 | 成人在线观看免费视频 | 色噜噜狠狠色综合中国 | 亚洲v精品 | 天天爽夜夜爽人人爽曰av | 久久精品高清 | 99久久精品免费看国产一区二区三区 | 日韩精品高清视频 | 亚洲天堂视频在线 | 日本成址在线观看 | 久久久久看片 | 亚洲成色777777在线观看影院 | 91麻豆精品一区二区三区 | 在线观看视频在线 | 欧美激情综合网 | 韩国一区视频 | 午夜国产成人 | 精品久久久久久国产 | 天堂av在线 | 蜜臀av一区| 最新精品视频在线 | 亚洲国产资源 | 五月天激情电影 | 99免费在线播放99久久免费 | 欧美激情第一区 | 不卡精品视频 | 成人av在线影院 | 欧美一级免费高清 | 亚洲成人网av| 日韩av一区二区三区四区 | 日韩欧美一区二区三区在线 | 在线观看精品视频 | 97狠狠干| 日批视频在线观看免费 | 欧美精选一区二区三区 | 亚洲欧美视频在线 | 国产不卡一二三区 | 久久免费精品视频 | 狠狠狠色丁香婷婷综合激情 | 操操操干干干 | 99久久精品免费看 | 狠狠狠色丁香综合久久天下网 | 99色在线播放 | 中文字幕在线看片 | 91在线精品一区二区 | 国产大片免费久久 | 亚洲欧美精品在线 | 激情网站五月天 | 丁香激情婷婷 | 色噜噜狠狠狠狠色综合久不 | 国产高清中文字幕 | 久久国产精品视频免费看 | 国产色道 | 国产成人av电影在线 | 国产一区二区在线影院 | 久久爱影视i | 亚洲va欧美va| 天天草天天干天天射 | 97偷拍视频 | 久久综合久久综合九色 | 五月天婷婷在线视频 | 久久国产精品免费一区二区三区 | 亚洲国产中文字幕在线观看 | 亚洲黄a | 夜夜躁狠狠躁 | 成人在线免费看 | 亚洲精品国产日韩 | 久久久久久国产精品久久 | 91专区在线观看 | 看av免费| 欧美极品xxx | 亚洲美女视频在线观看 | 99精品国产一区二区三区麻豆 | 日本不卡视频 | 在线黄色免费 | 亚洲国产无 | 免费av小说 | 国产综合精品久久 | 99精品久久99久久久久 | 国产一级视屏 | 91高清免费观看 | 91在线看免费 | 日韩中文字幕免费视频 | 五月天激情视频在线观看 | 色999视频| 国产精品视频专区 | 美女黄频网站 | 久久国产精品色av免费看 | 精品国产_亚洲人成在线 | 国产黄色精品在线观看 | 99精品国产免费久久久久久下载 | 欧美激情综合五月 | av中文在线影视 | 亚洲一区网站 | 一级黄色大片 | 欧美日韩亚洲精品在线 | 色综合天天做天天爱 | 日韩免费 | av中文在线影视 | 欧美一区二区三区在线视频观看 | 婷婷国产一区二区三区 | 亚洲一级理论片 | 波多野结衣电影久久 | 亚洲1级片| 久草香蕉在线 | 日韩在线一区二区免费 | 亚洲国产免费 | 亚洲精选在线观看 | 碰超在线观看 | 久久99亚洲精品久久久久 | 日本黄色a级大片 | 探花视频在线观看免费 | 午夜视频99 | 国产不卡免费 | 亚洲精品国产成人 | 中文字幕中文中文字幕 | 色就是色综合 | 欧美小视频在线观看 | 成人羞羞视频在线观看免费 | 永久免费精品视频网站 | 免费视频区 | 精品国产免费av | 亚洲国产偷 | 亚洲黄色成人av | 欧美日韩精品区 | 国产91精品一区二区麻豆网站 | 国产又粗又猛又色又黄网站 | 久久综合精品国产一区二区三区 | 在线免费av电影 | 激情丁香5月| 免费视频18| 欧美日韩中文在线视频 | 日韩成人免费在线 | 日韩| 美女久久 | 91视频首页| 久艹视频免费观看 | 亚洲日本欧美在线 | 亚洲日本中文字幕在线观看 | 亚洲黄色a| 正在播放 久久 | 最近中文字幕免费av | 91麻豆网 | 狠狠干,狠狠操 | 成年人免费观看在线视频 | 国内精品免费久久影院 | 久久久免费观看完整版 | 99热国内精品 | 日韩国产精品久久久久久亚洲 | 亚洲人毛片 | 五月婷婷色综合 | 久久久久麻豆v国产 | 二区三区在线观看 | 国产一区二区三区免费在线 | 成人黄色在线看 | 日韩伦理片hd | 亚洲精品一区二区18漫画 | 日韩在线观看中文 | av直接看 | 最新中文字幕在线播放 | 久草在线综合网 | 成人午夜剧场在线观看 | 中文字幕资源站 | 国产这里只有精品 | 二区三区在线 | 欧美激情视频免费看 | 欧美一级电影免费观看 | 婷婷在线五月 | 久久精视频 | 一本—道久久a久久精品蜜桃 | 有码视频在线观看 | 亚洲视频第一页 | 天天天色| 婷婷在线不卡 | 久久久www成人免费毛片 | 久久午夜精品影院一区 | 久久精品一区二区三区国产主播 | 国产黄色片一级三级 | 成人一级片在线观看 | 一本一道久久a久久精品 | 五月天网站在线 | 五月婷婷一区二区三区 | 国产高清在线不卡 | 日韩精品一区二区免费 | 久久精品美女视频 | 91福利社区在线观看 | 久日视频| 日韩免费在线观看网站 | 国产麻豆视频在线观看 | 久草色在线观看 | 免费av观看 | 亚洲理论视频 | 国产精品资源在线观看 | av理论电影 | 九色精品免费永久在线 | 免费观看福利视频 | 国产国语在线 | 天操夜夜操| 久久国产欧美日韩 | 曰韩在线 | 国产黄免费在线观看 | 国产人在线成免费视频 | 色老板在线视频 | 最新日韩在线 | 国产尤物在线视频 | 久久99热精品这里久久精品 | 欧美一级日韩免费不卡 | 高清不卡一区二区在线 | 免费视频一级片 | 久久国产精品一国产精品 | 久久精品视频国产 | 国产精品久久麻豆 | 日日夜夜骑| 亚洲国产欧洲综合997久久, | 奇米影视999 | 女人久久久久 | 激情文学丁香 | 一区二区三区观看 | 国产精品一区二区中文字幕 | 亚洲精品美女视频 | 狠狠干我| 色婷婷视频在线观看 | 黄色.com| 免费av一级电影 | japanesexxxhd奶水| 久久综合亚洲鲁鲁五月久久 | 玖玖在线观看视频 | 欧美日韩精品免费观看 | 亚洲精品国产品国语在线 | 亚洲成人第一区 | 99精品国产亚洲 | 91麻豆精品国产91久久久久 | 成人av片免费观看app下载 | 在线观看91av | 五月天婷婷在线视频 | 午夜精品久久久久久久99 | 午夜国产福利视频 | 亚洲国产精品一区二区久久hs | 国产精品久久av | 久草视频在线资源 | 国产精品久久久久久久久久了 | 日韩激情三级 | 国产成人一区二区啪在线观看 | 亚洲一级片在线看 | 久久国产精品免费一区 | 成人国产精品av | 亚洲www天堂com | 99精品国产福利在线观看免费 | 99国产精品一区 | 久久亚洲精品电影 | 韩国精品一区二区三区六区色诱 | 国产黄在线 | 久久久噜噜噜久久久 | 亚洲人成人在线 | 欧美另类sm图片 | 国产精品 日韩精品 | 92精品国产成人观看免费 | 亚洲国产日韩精品 | 国产精品成人一区二区三区吃奶 | 亚洲国产精品99久久久久久久久 | 亚洲欧美日韩国产一区二区三区 | 国产精品资源在线观看 | 99久久99视频只有精品 | 美女视频a美女大全免费下载蜜臀 | 亚洲欧洲xxxx | 色婷婷激情电影 | 中文字幕av在线不卡 | 日韩欧美视频免费看 | 国产精久久久久久久 | 91av免费在线观看 | 国产免费不卡av | 午夜精品福利一区二区 | 97麻豆视频| 欧美日韩中文字幕综合视频 | 成人毛片一区 | 丰满少妇对白在线偷拍 | 黄a在线 | 黄色三级在线看 | 国际精品久久 | 欧美日韩国产一二三区 | 午夜视频免费播放 | 中文字幕观看av | 中文字幕精品在线 | 中文字幕免费观看 | 男女拍拍免费视频 | 一区二区三区在线视频观看58 | 婷婷精品国产一区二区三区日韩 | 欧美韩国日本在线 | 亚洲日本韩国一区二区 | 成全免费观看视频 | 亚洲人成人在线 | 一区二区三区av在线 | 国产日产在线观看 | 亚洲综合情 | 亚洲精品av中文字幕在线在线 | 五月天激情视频 | 在线涩涩| 亚洲aⅴ免费在线观看 | 99中文字幕视频 | 婷婷干五月 | 97涩涩视频 | 国产精品久久久久久久久搜平片 | 免费日韩 精品中文字幕视频在线 | 狠狠地操 | 狠狠色狠狠色合久久伊人 | 国产精品一区二区久久精品 | 天天草综合 | 欧美黑人性猛交 | 久久久久久美女 | 国产精品一级视频 | 人人玩人人添人人澡97 | 午夜精品电影一区二区在线 | 亚洲码国产日韩欧美高潮在线播放 | 91精品国自产在线 | 久草视频看看 | 亚洲精品国产高清 | 在线看片91 | 99精品视频在线观看视频 | 国产高清av免费在线观看 | 国产男女无遮挡猛进猛出在线观看 | 国产精品综合av一区二区国产馆 | 日韩三级免费 | 丁香婷婷综合网 | 亚洲激情国产精品 | 色综合久久久久久中文网 | 91视频免费播放 | 国产精品 日韩 欧美 | 久久久久久久久久久福利 | 亚洲综合色站 | 伊人干综合 | 99色人| 在线看中文字幕 | 国产精品一二三 | 久久天天综合网 | www亚洲一区 | 午夜av日韩 | 麻豆传媒一区二区 | 亚洲精品一区二区在线观看 | 欧洲亚洲女同hd | 99九九视频 | 日韩av不卡在线观看 | 午夜久久久精品 | 久操中文字幕在线观看 | 日韩一区二区三区观看 | www.国产毛片| 久久久久久久精 | 国产精品一区二区三区在线播放 | 成人a免费 | 9幺看片 | 免费91在线| 探花系列在线 | 激情av资源 | 亚洲精品乱码久久久久久蜜桃欧美 | 高清中文字幕 | 欧美男男tv网站 | 国产精品第二页 | 人人草人 | 天天色综合久久 | 黄色三级免费看 | 四虎成人精品永久免费av | 婷婷色六月天 | 精品国产三级a∨在线欧美 免费一级片在线观看 | 亚洲另类人人澡 | 国产网红在线 | 久久精品国产成人精品 | 有码中文在线 | 日韩视频免费在线 | 欧美怡红院视频 | 一本色道久久综合亚洲二区三区 | 黄色a在线 | 婷婷综合久久 | 国产一级一片免费播放放 | 丁香婷婷综合色啪 | 色视频网址 | 久久se视频 | 激情欧美一区二区免费视频 | 日日夜夜噜 | 91亚洲精品久久久久图片蜜桃 | 黄色一级在线视频 | 国产精品av在线免费观看 | 久一在线 | 在线观看中文字幕一区二区 | 成人福利av | 国产视频不卡 | 亚州精品天堂中文字幕 | 碰超在线观看 | 国产欧美日韩一区 | 99精品国自产在线 | 久久超级碰视频 | 在线观看视频一区二区三区 | 白丝av免费观看 | 久草在线一免费新视频 | 久草在线久草在线2 | 久久婷婷久久 | 91色蜜桃| 干干日日 | 99久精品| 色婷婷午夜 | 天天综合导航 | 中文在线中文a | 91精品在线播放 | 久99久在线 | 亚洲精品小视频在线观看 | 日韩精品欧美精品 | 国产精品18久久久久久首页狼 | 99视频在线免费 | 精品在线视频一区 | 亚洲精品网址在线观看 | 久久久久久久久久久久影院 | av在线日韩 | 久久国产一区二区 | 国产精品系列在线 | 欧美成人xxx | 日韩欧三级 | 亚洲日本一区二区在线 | 亚洲韩国一区二区三区 | 中文字幕一区三区 | 日韩免费在线观看视频 | 久草在线视频网 | 亚洲免费av网站 | 国产特级毛片aaaaaaa高清 | 香蕉97视频观看在线观看 | 久久精品久久综合 | 五月天亚洲婷婷 | 天天操天天操一操 | 久久久免费精品国产一区二区 | 蜜臀久久99精品久久久酒店新书 | 狠狠狠色丁香婷婷综合久久五月 | 日韩a在线观看 | 亚洲一区二区精品3399 | 美女视频一区二区 |