Spring Boot 響應式 WebFlux 入門

一拒逮、概述

友情提示:Reactive Programming ,翻譯為反應式編程咱娶,又稱為響應式編程米间。國內多數(shù)叫響應式編程,本文我們統(tǒng)一使用響應式膘侮。不過屈糊,比較正確的叫法還是反應式。

Spring Framework 5 在 2017 年 9 月份琼了,發(fā)布了 GA 通用版本逻锐。既然是一個新的大版本夫晌,必然帶來了非常多的改進,其中比較重要的一點昧诱,就是將響應式編程帶入了 Spring 生態(tài)晓淀。也就是說,將響應式編程“真正”帶入了 Java 生態(tài)之中盏档。

在此之前凶掰,相信(include me),對響應式編程的概念是非常模糊的蜈亩。甚至說懦窘,截止到目前 2019 年 11 月份,對于國內的 Java 開發(fā)者稚配,也是知之甚少畅涂。

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

實際上,WebFlux 在出來的兩年時間里宦言,據(jù)了解到的情況扇单,鮮有項目從采用 SpringMVC 遷移到 WebFlux ,又或者新項目直接采用 WebFlux 奠旺。這又是為什么呢蜘澜?

響應式編程,對我們現(xiàn)有的編程方式响疚,是一場顛覆鄙信,對于框架也是。

  • 在 Spring 提供的框架中忿晕,實際并沒有全部實現(xiàn)好對響應式編程的支持装诡。例如說,Spring Transaction 事務組件践盼,在 Spring 5.2 M2 版本鸦采,才提供了支持響應式編程的 ReactiveTransactionManager 事務管理器。
  • 更不要說咕幻,Java 生態(tài)常用的框架渔伯,例如說 MyBatis、Jedis 等等肄程,都暫未提供響應式編程的支持锣吼。

所以选浑,WebFlux 想要能夠真正普及到我們的項目中,不僅僅需要 Spring 自己體系中的框架提供對響應式編程的很好的支持玄叠,也需要 Java 生態(tài)中的框架也要做到如此古徒。

即使如此,這也并不妨礙我們來對 WebFlux 進行一個小小的入門读恃。畢竟描函,響應式編程這把火,終將熊熊燃起狐粱。Spring Cloud Gateway即使用的的WebFlux實現(xiàn)。

1.1 響應式編程

簡單地說胆数,響應式編程是關于非阻塞應用程序的肌蜻,這些應用程序是異步的、事件驅動的必尼,并且需要少量的線程來垂直伸縮(即在 JVM 中)蒋搜,而不是水平伸縮(即通過集群)。
以后端 API 請求的處理來舉例子判莉。

在現(xiàn)在主流的編程模型中豆挽,請求是被同步阻塞處理完成,返回結果給前端券盅。
在響應式的編程模型中帮哈,請求是被作為一個事件丟到線程池中執(zhí)行,等到執(zhí)行完畢锰镀,異步回調結果給主線程娘侍,最后返回給前端。
通過這樣的方式泳炉,主線程(實際是多個憾筏,這里只是方便描述哈)不斷接收請求,不負責直接同步阻塞處理花鹅,從而避免自身被阻塞氧腰。

1.2 Reactor 框架

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

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

  • Flux ,表示的是包含 0 到 N 個元素的異步序列之景。當消息通知產生時斤富,訂閱者(Subscriber)中對應的方法 #onNext(t), #onComplete(t)#onError(t) 會被調用。
  • Mono 表示的是包含 0 或者 1 個元素的異步序列锻狗。該序列中同樣可以包含與 Flux 相同的三種類型的消息通知满力。
  • 同時焕参,F(xiàn)lux 和 Mono 之間可以進行轉換。例如:
    • 對一個 Flux 序列進行計數(shù)count操作油额,得到的結果是一個 Mono<Long> 對象叠纷。
    • 把兩個 Mono 序列合并在一起,得到的是一個 Flux 對象潦嘶。

其實涩嚣,可以先暫時簡單把Mono 理解成 Object ,F(xiàn)lux 理解成 List掂僵。

1.3 Spring WebFlux

Spring 官方文檔對 Spring WebFlux 介紹如下:
Spring Framework 5 提供了一個新的 spring-webflux 模塊航厚。該模塊包含了:

  • 對響應式支持的 HTTP 和 WebSocket 客戶端。
  • 對響應式支持的 Web 服務器锰蓬,包括 Rest API幔睬、HTML 瀏覽器、WebSocket 等交互方式芹扭。

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

方式一舱卡,基于 Annotated Controller 方式實現(xiàn):基于 @Controller 和 SpringMVC 使用的其它注解辅肾。也就是說,我們大體上可以像使用 SpringMVC 的方式轮锥,使用 WebFlux 矫钓。
方式二,基于函數(shù)式編程方式:函數(shù)式舍杜,Java 8 lambda 表達式風格的路由和處理份汗。可能有點晦澀蝴簇,晚點我們看了示例就會明白杯活。
下面,開始讓我們開始愉快的快速入門熬词。

2. 快速入門

我們會使用 spring-boot-starter-webflux 實現(xiàn) WebFlux 的自動化配置旁钧。然后實現(xiàn)用戶的增刪改查接口。接口列表如下:

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

天文1號不是發(fā)射了嗎互拾!下面歪今,開始神秘的火星之旅~

2.1 引入依賴

在IDEA中,要創(chuàng)建WebFlux項目颜矿,必須勾選Spring Reactive Web而不是傳統(tǒng)的Spring Web寄猩,這里為了簡化代碼使用到了Lombok。

創(chuàng)建WebFlux 項目

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.3.0.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>webflux</artifactId>

    <dependencies>
        <!-- 實現(xiàn)對 Spring WebFlux 的自動化配置 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-webflux</artifactId>
            <version>2.3.0.RELEASE</version>
        </dependency>

        <!-- 方便等會寫單元測試 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

    </dependencies>

</project>

2.2 Application

創(chuàng)建 Application.java 類替废,配置 @SpringBootApplication 注解即可。

package com.erbadagang.springboot.springwebflux;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }

}

2.3 基于 Annotated Controller 方式實現(xiàn)

創(chuàng)建 [UserController] 類泊柬。代碼如下:

package com.erbadagang.springboot.springwebflux.controller;

import com.erbadagang.springboot.springwebflux.dto.UserAddDTO;
import com.erbadagang.springboot.springwebflux.dto.UserUpdateDTO;
import com.erbadagang.springboot.springwebflux.service.UserService;
import com.erbadagang.springboot.springwebflux.vo.UserVO;
import org.reactivestreams.Publisher;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import java.util.ArrayList;
import java.util.List;

/**
 * 用戶 Controller
 */
@RestController
@RequestMapping("/users")
public class UserController {

    @Autowired
    private UserService userService;

    /**
     * 查詢用戶列表
     *
     * @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("/v2/get")
    public Mono<UserVO> get2(@RequestParam("id") Integer id) {
        // 查詢用戶
        UserVO user = userService.get(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 addDTO 添加用戶信息 DTO
     * @return 添加成功的用戶編號
     */
    @PostMapping("add2")
    public Mono<Integer> add2(Mono<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 改成 DELETE
    public Mono<Boolean> delete(@RequestParam("id") Integer id) {
        // 刪除用戶記錄
        Boolean success = true;
        // 返回是否更新成功
        return Mono.just(success);
    }

}
  • 在類和方法上状答,我們添加了 @Controller 和 SpringMVC 在使用的 @GetMapping@PostMapping 等注解,提供 API 接口刀崖,這個和我們在使用 SpringMVC 是一模一樣的惊科。
  • dtovo 包下,有 API 使用到的 DTO 和 VO 類亮钦。
  • 因為是入門示例译断,我們會發(fā)現(xiàn)代碼十分簡單,淡定或悲,淡定(讓我想起來Trump跟記者打架讓閉嘴,shutup堪唐,shutup......)巡语。在后文中,我們會提供和 Spring Data JPA淮菠、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) 方法,參數(shù)為 Publisher 類型低剔,泛型為 UserAddDTO 類型速梗,并且添加了 @RequestBody 注解,從 request 的 Body 中讀取參數(shù)襟齿。注意姻锁,此時提交參數(shù)需要使用 "application/json" 等 Content-Type 內容類型。
  • #add(...) 方法猜欺,也可以使用 application/x-www-form-urlencodedmultipart/form-data 這兩個 Content-Type 內容類型位隶,通過 request 的 Form Data 或 Multipart Data 傳遞參數(shù)。代碼如下:
// UserController.java

/**
 * 添加用戶
 *
 * @param addDTO 添加用戶信息 DTO
 * @return 添加成功的用戶編號
 */
@PostMapping("add2")
public Mono<Integer> add(Mono<UserAddDTO> addDTO) {
    // 插入用戶記錄开皿,返回編號
    Integer returnId = UUID.randomUUID().hashCode();
    // 返回用戶編號
    return Mono.just(returnId);
}

此時涧黄,參數(shù)為 Mono 類型篮昧,泛型為 UserAddDTO 類型。
當然弓熏,我們也可以直接使用參數(shù)為 UserAddDTO 類型挟冠。如果后續(xù)需要使用到 Reactor API 褒墨,則我們自己主動調用 Mono#just(T data) 方法,封裝出 Publisher 對象。注意羡榴,F(xiàn)lux 和 Mono 都實現(xiàn)了 Publisher 接口。

  • #update(Publisher<UserUpdateDTO> updateDTO)方法惧磺,和#add(Publisher<UserAddDTO> addDTO)方法一致青责,就不重復贅述。
  • #delete(Integer id)方法嫁赏,和#get(Integer id)方法一致其掂,就不重復贅述。

2.4 基于函數(shù)式編程方式

創(chuàng)建 [UserRouter]類潦蝇。代碼如下:

package com.erbadagang.springboot.springwebflux.controller;

import com.erbadagang.springboot.springwebflux.vo.UserVO;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.util.StringUtils;
import org.springframework.web.reactive.function.server.*;
import reactor.core.publisher.Mono;

import java.util.ArrayList;
import java.util.List;
import java.util.UUID;

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.*;

/**
 * 用戶 Router
 */
@Configuration
public class UserRouter {

    @Bean
    public RouterFunction<ServerResponse> userListRouterFunction() {
        return RouterFunctions.route(RequestPredicates.GET("/users2/list"),
                new HandlerFunction<ServerResponse>() {

                    @Override
                    public 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);
                    }

                });
    }

    @Bean
    public RouterFunction<ServerResponse> userGetRouterFunction() {
        return RouterFunctions.route(RequestPredicates.GET("/users2/get"),
                new HandlerFunction<ServerResponse>() {

                    @Override
                    public 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);
                    }

                });
    }

    @Bean
    public RouterFunction<ServerResponse> demoRouterFunction() {
        return route(GET("/users2/demo"), request -> ok().bodyValue("demo"));
    }

}
  • 在類上款熬,添加 @Configuration 注解,保證該類中的 Bean 們攘乒,都被掃描到贤牛。

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

    • 第一個參數(shù) predicate 參數(shù),是 RequestPredicate 類型沽讹,請求謂語般卑,用于匹配請求∷郏可以通過 RequestPredicates 來構建各種條件蝠检。
    • 第二個參數(shù) handlerFunction 參數(shù),是 RouterFunction 類型挚瘟,處理器函數(shù)蝇率。
  • 每個方法定義的路由,胖友自己看下代碼刽沾,一眼能看的明白本慕。一般來說,采用第三個方法的寫法侧漓,更加簡潔锅尘。注意,需要使用 static import 靜態(tài)引入,代碼如下:

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 方式實現(xiàn)的編程方式藤违,更符合我們現(xiàn)在的開發(fā)習慣浪腐,學習成本也相對低一些。同時顿乒,和 API 接口文檔工具 Swagger 也更容易集成议街。

3. 測試接口

在開發(fā)完接口,我們會進行接口的自測璧榄。一般情況下特漩,我們先啟動項目,然后使用 Postman骨杂、curl涂身、瀏覽器,手工模擬請求后端 API 接口搓蚪。
如訪問url
實際上蛤售,WebFlux 提供了 Web 測試客戶端 WebTestClient 類,方便我們快速測試接口妒潭。下面悴能,我們對 UserController提供的接口,進行下單元測試雳灾。
MockMvc 提供了集成測試和單元測試的能力漠酿。

3.1 集成測試

創(chuàng)建 [UserControllerTest]測試類,我們來測試一下簡單的 UserController 的每個操作佑女。核心代碼如下:

// UserControllerTest.java

@RunWith(SpringRunner.class)
@SpringBootTest(classes = Application.class)
@AutoConfigureWebFlux
@AutoConfigureWebTestClient
public class UserControllerTest {

 @Autowired
 private WebTestClient webClient;

 @Test
 public void testList() {
 webClient.get().uri("/users/list")
 .exchange() // 執(zhí)行請求
 .expectStatus().isOk() // 響應狀態(tài)碼 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" +
 "]"); // 響應結果
 }

 @Test
 public void testGet() {
 // 獲得指定用戶編號的用戶
 webClient.get().uri("/users/get?id=1")
 .exchange() // 執(zhí)行請求
 .expectStatus().isOk() // 響應狀態(tài)碼 200
 .expectBody().json("{\n" +
 "    \"id\": 1,\n" +
 "    \"username\": \"username:1\"\n" +
 "}"); // 響應結果
 }

 @Test
 public void testGet2() {
 // 獲得指定用戶編號的用戶
 webClient.get().uri("/users/v2/get?id=1")
 .exchange() // 執(zhí)行請求
 .expectStatus().isOk() // 響應狀態(tài)碼 200
 .expectBody().json("{\n" +
 "    \"id\": 1,\n" +
 "    \"username\": \"test\"\n" +
 "}"); // 響應結果
 }

 @Test
 public void testAdd() {
 Map<String, Object> params = new HashMap<>();
 params.put("username", "yudaoyuanma");
 params.put("password", "nicai");
 // 添加用戶
 webClient.post().uri("/users/add")
 .bodyValue(params)
 .exchange() // 執(zhí)行請求
 .expectStatus().isOk() // 響應狀態(tài)碼 200
 .expectBody().json("1"); // 響應結果。因為沒有提供 content 的比較谈竿,所以只好使用 json 來比較团驱。竟然能通過
 }

 @Test
 public void testAdd2() { // 發(fā)送文件的測試,可以參考 https://dev.to/shavz/sending-multipart-form-data-using-spring-webtestclient-2gb7 文章
 BodyInserters.FormInserter<String> formData = // Form Data 數(shù)據(jù)空凸,需要這么拼湊
 BodyInserters.fromFormData("username", "yudaoyuanma")
 .with("password", "nicai");
 // 添加用戶
 webClient.post().uri("/users/add2")
 .body(formData)
 .exchange() // 執(zhí)行請求
 .expectStatus().isOk() // 響應狀態(tài)碼 200
 .expectBody().json("1"); // 響應結果嚎花。因為沒有提供 content 的比較,所以只好使用 json 來比較呀洲。竟然能通過
 }

 @Test
 public void testUpdate() {
 Map<String, Object> params = new HashMap<>();
 params.put("id", 1);
 params.put("username", "yudaoyuanma");
 // 修改用戶
 webClient.post().uri("/users/update")
 .bodyValue(params)
 .exchange() // 執(zhí)行請求
 .expectStatus().isOk() // 響應狀態(tài)碼 200
 .expectBody(Boolean.class) // 期望返回值類型是 Boolean
 .consumeWith((Consumer<EntityExchangeResult<Boolean>>) result -> // 通過消費結果紊选,判斷符合是 true 。
 Assert.assertTrue("返回結果需要為 true", result.getResponseBody()));
 }

 @Test
 public void testDelete() {
 // 刪除用戶
 webClient.post().uri("/users/delete?id=1")
 .exchange() // 執(zhí)行請求
 .expectStatus().isOk() // 響應狀態(tài)碼 200
 .expectBody(Boolean.class) // 期望返回值類型是 Boolean
 .isEqualTo(true); // 這樣更加簡潔一些
//                .consumeWith((Consumer<EntityExchangeResult<Boolean>>) result -> // 通過消費結果道逗,判斷符合是 true 兵罢。
//                        Assert.assertTrue("返回結果需要為 true", result.getResponseBody()));
 }

}
  • 在類上,我們添加了 @AutoConfigureWebTestClient 注解滓窍,用于自動化配置我們稍后注入的 WebTestClient Bean 對象 webClient 卖词。在后續(xù)的測試中,我們會看到都是通過 webClient 調用后端 API 接口吏夯。而每一次調用后端 API 接口此蜈,都會執(zhí)行真正的后端邏輯即横。因此,整個邏輯裆赵,走的是集成測試东囚,會啟動一個真實的 Spring 環(huán)境。
  • 每次 API 接口的請求战授,都通過 RequestHeadersSpec 來構建页藻。構建完成后,通過 RequestHeadersSpec#exchange() 方法來執(zhí)行請求陈醒,返回 ResponseSpec 結果惕橙。
    • WebTestClient 的 #get()#head()钉跷、#delete()弥鹦、#options() 方法,返回的是 RequestHeadersUriSpec 對象爷辙。
    • WebTestClient 的 #post()彬坏、#put()#delete()膝晾、#patch() 方法栓始,返回的是 RequestBodyUriSpec 對象。
    • RequestHeadersUriSpec 和 RequestBodyUriSpec 都繼承了 RequestHeadersSpec 接口血当。
  • 執(zhí)行完請求后幻赚,通過調用 RequestBodyUriSpec 的各種斷言方法,添加對結果的預期臊旭,相當于做斷言落恼。如果不符合預期,則會拋出異常离熏,測試不通過佳谦。

3.2 單元測試

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

  • 創(chuàng)建 [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) 方法,獲得指定用戶編號的用戶兜粘。
創(chuàng)建 [UserControllerTest2]測試類申窘,我們來測試一下簡單的 UserController 的新增的這個 API 操作。代碼如下:

// UserControllerTest2.java

@RunWith(SpringRunner.class)
@WebFluxTest(UserController.class)
public class UserControllerTest2 {

    @Autowired
    private WebTestClient webClient;

    @MockBean
    private UserService userService;

    @Test
    public 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() // 執(zhí)行請求
                .expectStatus().isOk() // 響應狀態(tài)碼 200
                .expectBody().json("{\n" +
                "    \"id\": 1,\n" +
                "    \"username\": \"username:1\"\n" +
                "}"); // 響應結果
    }

}
  • 在類上添加 @WebFluxTest 注解孔轴,并且傳入的是 UserController 類剃法,表示我們要對 UserController 進行單元測試。
  • 同時路鹰,@WebFluxTest 注解贷洲,是包含了 @UserController 的組合注解,所以它會自動化配置我們稍后注入的 WebTestClient Bean 對象 mvc 晋柱。在后續(xù)的測試中优构,我們會看到都是通過 webClient 調用后端 API 接口。但是雁竞!每一次調用后端 API 接口钦椭,并不會執(zhí)行真正的后端邏輯,而是走的 Mock 邏輯碑诉。也就是說彪腔,整個邏輯,走的是單元測試进栽,會啟動一個 Mock 的 Spring 環(huán)境德挣。

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

  • userService 屬性快毛,我們添加了 [@MockBean]注解格嗅,實際這里注入的是一個使用 Mockito 創(chuàng)建的 UserService Mock 代理對象。如下圖所示:[圖片上傳失敗...(image-46836b-1596810612894)]

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

    • 默認情況下没隘,

    • <1.1> 處懂扼,我們調用 UserService#get(Integer id) 方法禁荸,然后打印返回結果右蒲。執(zhí)行結果如下:before mock:null, 結果竟然返回的是 null 空。理論來說赶熟,此時應該返回一個 id = 1 的 UserVO 對象瑰妄。實際上,因為此時的 userService 是通過 Mockito 來 Mock 出來的對象映砖,其所有調用它的方法间坐,返回的都是空。

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

    • <1.3> 處蜈七,再次調用 UserService#get(Integer id) 方法秒拔,然后打印返回結果。執(zhí)行結果如下:after cn.iocoder.springboot.lab27.springwebflux.vo.UserVO@23202c31
      打印的就是我們 Mock 返回的 UserVO 對象飒硅。

  • 后續(xù)砂缩,使用 webClient 完成一次后端 API 調用,并進行斷言結果是否正確三娩。執(zhí)行成功庵芭,單元測試通過。

底線


本文源代碼使用 Apache License 2.0開源許可協(xié)議雀监,這里是本文源碼Gitee地址双吆,可通過命令git clone+地址下載代碼到本地,也可直接點擊鏈接通過瀏覽器方式查看源代碼滔悉。

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末伊诵,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子回官,更是在濱河造成了極大的恐慌曹宴,老刑警劉巖,帶你破解...
    沈念sama閱讀 207,113評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件歉提,死亡現(xiàn)場離奇詭異笛坦,居然都是意外死亡,警方通過查閱死者的電腦和手機苔巨,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評論 2 381
  • 文/潘曉璐 我一進店門版扩,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人侄泽,你說我怎么就攤上這事礁芦。” “怎么了悼尾?”我有些...
    開封第一講書人閱讀 153,340評論 0 344
  • 文/不壞的土叔 我叫張陵柿扣,是天一觀的道長。 經常有香客問我闺魏,道長未状,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,449評論 1 279
  • 正文 為了忘掉前任析桥,我火速辦了婚禮司草,結果婚禮上艰垂,老公的妹妹穿的比我還像新娘。我一直安慰自己埋虹,他們只是感情好猜憎,可當我...
    茶點故事閱讀 64,445評論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著搔课,像睡著了一般拉宗。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上辣辫,一...
    開封第一講書人閱讀 49,166評論 1 284
  • 那天旦事,我揣著相機與錄音,去河邊找鬼急灭。 笑死姐浮,一個胖子當著我的面吹牛,可吹牛的內容都是我干的葬馋。 我是一名探鬼主播卖鲤,決...
    沈念sama閱讀 38,442評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼畴嘶!你這毒婦竟也來了蛋逾?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 37,105評論 0 261
  • 序言:老撾萬榮一對情侶失蹤窗悯,失蹤者是張志新(化名)和其女友劉穎区匣,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體蒋院,經...
    沈念sama閱讀 43,601評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡亏钩,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 36,066評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了欺旧。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片姑丑。...
    茶點故事閱讀 38,161評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖辞友,靈堂內的尸體忽然破棺而出栅哀,到底是詐尸還是另有隱情,我是刑警寧澤称龙,帶...
    沈念sama閱讀 33,792評論 4 323
  • 正文 年R本政府宣布留拾,位于F島的核電站,受9級特大地震影響茵瀑,放射性物質發(fā)生泄漏间驮。R本人自食惡果不足惜躬厌,卻給世界環(huán)境...
    茶點故事閱讀 39,351評論 3 307
  • 文/蒙蒙 一马昨、第九天 我趴在偏房一處隱蔽的房頂上張望竞帽。 院中可真熱鬧,春花似錦鸿捧、人聲如沸屹篓。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,352評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽堆巧。三九已至,卻和暖如春泼菌,著一層夾襖步出監(jiān)牢的瞬間谍肤,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,584評論 1 261
  • 我被黑心中介騙來泰國打工哗伯, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留荒揣,地道東北人。 一個月前我還...
    沈念sama閱讀 45,618評論 2 355
  • 正文 我出身青樓焊刹,卻偏偏與公主長得像系任,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子虐块,可洞房花燭夜當晚...
    茶點故事閱讀 42,916評論 2 344