美文网首页
Spring Boot 响应式 WebFlux 入门

Spring Boot 响应式 WebFlux 入门

作者: 梅西爱骑车 | 来源:发表于2020-08-20 02:05 被阅读0次

    一、概述

    友情提示:Reactive Programming ,翻译为反应式编程,又称为响应式编程。国内多数叫响应式编程,本文我们统一使用响应式。不过,比较正确的叫法还是反应式。

    Spring Framework 5 在 2017 年 9 月份,发布了 GA 通用版本。既然是一个新的大版本,必然带来了非常多的改进,其中比较重要的一点,就是将响应式编程带入了 Spring 生态。也就是说,将响应式编程“真正”带入了 Java 生态之中。

    在此之前,相信(include me),对响应式编程的概念是非常模糊的。甚至说,截止到目前 2019 年 11 月份,对于国内的 Java 开发者,也是知之甚少。

    对于我们来说,最早看到的就是 Spring5 提供了一个新的 Web 框架,基于响应式编程的 Spring WebFlux 。至此,SpringMVC 在“干掉” Struts 之后,难道要开始进入 Spring 自己的两个 Web 框架的双雄争霸?

    实际上,WebFlux 在出来的两年时间里,据了解到的情况,鲜有项目从采用 SpringMVC 迁移到 WebFlux ,又或者新项目直接采用 WebFlux 。这又是为什么呢?

    响应式编程,对我们现有的编程方式,是一场颠覆,对于框架也是。

    • 在 Spring 提供的框架中,实际并没有全部实现好对响应式编程的支持。例如说,Spring Transaction 事务组件,在 Spring 5.2 M2 版本,才提供了支持响应式编程的 ReactiveTransactionManager 事务管理器。
    • 更不要说,Java 生态常用的框架,例如说 MyBatis、Jedis 等等,都暂未提供响应式编程的支持。

    所以,WebFlux 想要能够真正普及到我们的项目中,不仅仅需要 Spring 自己体系中的框架提供对响应式编程的很好的支持,也需要 Java 生态中的框架也要做到如此。

    即使如此,这也并不妨碍我们来对 WebFlux 进行一个小小的入门。毕竟,响应式编程这把火,终将熊熊燃起。Spring Cloud Gateway即使用的的WebFlux实现。

    1.1 响应式编程

    简单地说,响应式编程是关于非阻塞应用程序的,这些应用程序是异步的、事件驱动的,并且需要少量的线程来垂直伸缩(即在 JVM 中),而不是水平伸缩(即通过集群)。
    以后端 API 请求的处理来举例子。

    在现在主流的编程模型中,请求是被同步阻塞处理完成,返回结果给前端。
    在响应式的编程模型中,请求是被作为一个事件丢到线程池中执行,等到执行完毕,异步回调结果给主线程,最后返回给前端。
    通过这样的方式,主线程(实际是多个,这里只是方便描述哈)不断接收请求,不负责直接同步阻塞处理,从而避免自身被阻塞。

    1.2 Reactor 框架

    简单来说,Reactor 说是一个响应式编程框架,又快又不占用内存的那种。

    Reactor 有两个非常重要的基本概念:

    • Flux ,表示的是包含 0 到 N 个元素的异步序列。当消息通知产生时,订阅者(Subscriber)中对应的方法 #onNext(t), #onComplete(t)#onError(t) 会被调用。
    • Mono 表示的是包含 0 或者 1 个元素的异步序列。该序列中同样可以包含与 Flux 相同的三种类型的消息通知。
    • 同时,Flux 和 Mono 之间可以进行转换。例如:
      • 对一个 Flux 序列进行计数count操作,得到的结果是一个 Mono<Long> 对象。
      • 把两个 Mono 序列合并在一起,得到的是一个 Flux 对象。

    其实,可以先暂时简单把Mono 理解成 Object ,Flux 理解成 List

    1.3 Spring WebFlux

    Spring 官方文档对 Spring WebFlux 介绍如下:
    Spring Framework 5 提供了一个新的 spring-webflux 模块。该模块包含了:

    • 对响应式支持的 HTTP 和 WebSocket 客户端。
    • 对响应式支持的 Web 服务器,包括 Rest API、HTML 浏览器、WebSocket 等交互方式。

    在服务端方面,WebFlux 提供了 2 种编程模型(翻译成使用方式,可能更易懂):

    方式一,基于 Annotated Controller 方式实现:基于 @Controller 和 SpringMVC 使用的其它注解。也就是说,我们大体上可以像使用 SpringMVC 的方式,使用 WebFlux 。
    方式二,基于函数式编程方式:函数式,Java 8 lambda 表达式风格的路由和处理。可能有点晦涩,晚点我们看了示例就会明白。
    下面,开始让我们开始愉快的快速入门。

    2. 快速入门

    我们会使用 spring-boot-starter-webflux 实现 WebFlux 的自动化配置。然后实现用户的增删改查接口。接口列表如下:

    请求方法 URL 功能
    GET /users/list 查询用户列表
    GET /users/get 获得指定用户编号的用户
    POST /users/add 添加用户
    POST /users/update 更新指定用户编号的用户
    POST /users/delete 删除指定用户编号的用户

    天文1号不是发射了吗!下面,开始神秘的火星之旅~

    2.1 引入依赖

    在IDEA中,要创建WebFlux项目,必须勾选Spring Reactive Web而不是传统的Spring Web,这里为了简化代码使用到了Lombok。

    创建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>
            <!-- 实现对 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

    创建 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 方式实现

    创建 [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 类。
    • 因为是入门示例,我们会发现代码十分简单,淡定,淡定(让我想起来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) 方法,参数为 Publisher 类型,泛型为 UserAddDTO 类型,并且添加了 @RequestBody 注解,从 request 的 Body 中读取参数。注意,此时提交参数需要使用 "application/json" 等 Content-Type 内容类型。
    • #add(...) 方法,也可以使用 application/x-www-form-urlencodedmultipart/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 接口。

    • #update(Publisher<UserUpdateDTO> updateDTO)方法,和#add(Publisher<UserAddDTO> addDTO)方法一致,就不重复赘述。
    • #delete(Integer id)方法,和#get(Integer id)方法一致,就不重复赘述。

    2.4 基于函数式编程方式

    创建 [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) 方法,定义了一条路由。

      • 第一个参数 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 也更容易集成。

    3. 测试接口

    在开发完接口,我们会进行接口的自测。一般情况下,我们先启动项目,然后使用 Postmancurl、浏览器,手工模拟请求后端 API 接口。
    如访问url
    实际上,WebFlux 提供了 Web 测试客户端 WebTestClient 类,方便我们快速测试接口。下面,我们对 UserController提供的接口,进行下单元测试。
    MockMvc 提供了集成测试和单元测试的能力。

    3.1 集成测试

    创建 [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() // 执行请求
     .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" +
     "]"); // 响应结果
     }
    
     @Test
     public void testGet() {
     // 获得指定用户编号的用户
     webClient.get().uri("/users/get?id=1")
     .exchange() // 执行请求
     .expectStatus().isOk() // 响应状态码 200
     .expectBody().json("{\n" +
     "    \"id\": 1,\n" +
     "    \"username\": \"username:1\"\n" +
     "}"); // 响应结果
     }
    
     @Test
     public void testGet2() {
     // 获得指定用户编号的用户
     webClient.get().uri("/users/v2/get?id=1")
     .exchange() // 执行请求
     .expectStatus().isOk() // 响应状态码 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() // 执行请求
     .expectStatus().isOk() // 响应状态码 200
     .expectBody().json("1"); // 响应结果。因为没有提供 content 的比较,所以只好使用 json 来比较。竟然能通过
     }
    
     @Test
     public 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 来比较。竟然能通过
     }
    
     @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() // 执行请求
     .expectStatus().isOk() // 响应状态码 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() // 执行请求
     .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 。修改点如下:

    • 创建 [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 {
    
        @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() // 执行请求
                    .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 代理对象。如下图所示:[图片上传失败...(image-46836b-1596810612894)]

      • UserController 中,也会注入一个 UserService 属性,此时注入的就是该 Mock 出来的 UserService Bean 对象。

      • 默认情况下,

      • <1.1> 处,我们调用 UserService#get(Integer id) 方法,然后打印返回结果。执行结果如下:before mock:null, 结果竟然返回的是 null 空。理论来说,此时应该返回一个 id = 1 的 UserVO 对象。实际上,因为此时的 userService 是通过 Mockito 来 Mock 出来的对象,其所有调用它的方法,返回的都是空。

      • <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
        打印的就是我们 Mock 返回的 UserVO 对象。

    • 后续,使用 webClient 完成一次后端 API 调用,并进行断言结果是否正确。执行成功,单元测试通过。

    底线


    本文源代码使用 Apache License 2.0开源许可协议,这里是本文源码Gitee地址,可通过命令git clone+地址下载代码到本地,也可直接点击链接通过浏览器方式查看源代码。

    相关文章

      网友评论

          本文标题:Spring Boot 响应式 WebFlux 入门

          本文链接:https://www.haomeiwen.com/subject/gfqqdktx.html