Spring Boot 响应式 WebFlux 入门

2020-08-20  本文已影响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 。这又是为什么呢?

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

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

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

1.1 响应式编程

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

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

1.2 Reactor 框架

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

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

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

1.3 Spring WebFlux

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

在服务端方面,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);
    }

}
// 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 接口。

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"));
    }

}
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()));
 }

}

3.2 单元测试

为了更好的展示 WebFlux 单元测试的示例,我们需要改写 UserController 的代码,让其会依赖 UserService 。修改点如下:

// UserService.java

    @Service
    public class UserService {

     public UserVO get(Integer id) {
     return new UserVO().setId(id).setUsername("test");
     }

    }
// 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" +
                "}"); // 响应结果
    }

}

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

底线


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

上一篇 下一篇

猜你喜欢

热点阅读