响应式框架webflux

353 阅读7分钟

第一章 webflux快速入门

1.1、Reactive Stack简介

image.png

由上图对比可以看出,Spring boot2.0推出了全新的开发技术栈:reactive,相对于Servlet Stack技术栈,它的最大优势在于:使用了非阻塞的编程模型,能够有效地利用下一代的多核CPU处理器,处理海量的并发连接。 boosGroup用于Accetpt连接建立事件并分发请求, workerGroup用于处理I/O读写事件和业务逻辑每个Boss NioEventLoop循环执行的任务包含3步:

image.png

  • 1 轮询accept事件
  • 2 处理accept I/O事件,与Client建立连接,生成NioSocketChannel,并将NioSocketChannel注册到某个Worker NioEventLoop的Selector上
  • 3 处理任务队列中的任务,runAllTasks。任务队列中的任务包括用户调用eventloop.execute或schedule执行的任务,或者其它线程提交到该eventloop的任务。

每个Worker NioEventLoop循环执行的任务包含3步:

  • 1 轮询read、write事件;
  • 2 处I/O事件,即read、write事件,在NioSocketChannel可读、可写事件发生时进行处理
  • 3 处理任务队列中的任务,runAllTasks。

1.2、webflux性能会提高么?

官网原话:

Reactive and non-blocking generally do not make applications run faster.docs.spring.io/spring/docs…

所以,使用webflux,并不会缩短一个服务接口的响应时间。webflux的优势在于:在同样的资源(CPU和内存)下,提供了更大的吞吐量和更好的伸缩性。它特别适合应用在 IO 密集型的服务中,比如微服务网关这样的应用中。

PS: IO 密集型包括:磁盘IO密集型, 网络IO密集型,微服务网关就属于网络 IO 密集型,使用异步非阻塞式编程模型,能够显著地提升网关对下游服务转发的吞吐量。

1.3、Spring Boot 2.0 WebFlux 组件

Spring Boot WebFlux 官方提供了很多 Starter 组件,每个模块会有多种技术实现选型支持,来实现各种复杂的业务需求:Web:Spring WebFlux模板引擎:Thymeleaf存储:Redis、MongoDB、Cassandra,注意目前不支持 MySQL这种关系型数据库内嵌容器:Tomcat、Jetty、Undertow

1.4、编程模型

Spring 5 web 模块包含了 Spring WebFlux 的 HTTP 抽象。类似 Servlet API , WebFlux 提供了 WebHandler API 去定义非阻塞 API 抽象接口。可以选择以下两种编程模型实现:

  • 注解式编程模型。和 MVC 保持一致,WebFlux 也支持响应性 @RequestBody 注解。
  • Functional Endpoint编程模型(国内有人翻译叫做功能性端点,我觉得不准确我就不翻译)。函数式编程模型,用来路由和处理请求的小工具。

1.4.1使用Functional Endpoint实现helloworld

image.png

image.png

image.png

下一步,下一步等待项目创建成功。创建成功后,maven依赖是这个样子的

 <dependencies>
        <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>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>io.projectreactor</groupId>
            <artifactId>reactor-test</artifactId>
            <scope>test</scope>
        </dependency>
 </dependencies>

编写处理器类 Handler

@Component
public class HelloHandler {
    public Mono<ServerResponse> helloCity(ServerRequest request) {
        return ServerResponse.ok().contentType(MediaType.TEXT_PLAIN)
                .body(BodyInserters.fromObject("Hello, City!"));
    }
}

ServerResponse 是对响应的封装,可以设置响应状态、响应头、响应正文。比如 ok 代表的是 200 响应码、MediaType 枚举是代表这文本内容类型、返回的是 String 的对象。这里用 Mono 作为返回对象,是因为返回包含了一个 ServerResponse 对象。返回多个元素用Flux。

编写路由器类


@Configuration
public class HelloRouter {
    @Bean
    public RouterFunction<ServerResponse> route(HelloHandler cityHandler) {
        return RouterFunctions
                .route(RequestPredicates.GET("/hello")
                                .and(RequestPredicates.accept(MediaType.TEXT_PLAIN)),
                        cityHandler::helloCity);
    }
}

RouterFunctions 对请求路由处理类,即将请求路由到处理器,这里将一个 GET 请求 /hello 路由到处理器 cityHandler 的 helloCity 方法上。跟 Spring MVC 模式下的 HandleMapping 的作用类似。RouterFunctions.route(RequestPredicate, HandlerFunction) 方法,对应的入参是请求参数和处理函数,如果请求匹配,就调用对应的处理器函数。

image.png

image.png

第二章、注解方式实现restful接口

2.1.实体

实体类City

@Data
public class City {
    private Long id;
    private Long provinceId;
    private String cityName;
    private String description;
}

2.2.持久层

模拟实现的持久层,并未真正的做持久化

@Repository
public class CityRepository {
​
    private ConcurrentMap<Long, City> repository = new ConcurrentHashMap<>();
​
    private static final AtomicLong idGenerator = new AtomicLong(0);
​
    public Long save(City city) {
        Long id = idGenerator.incrementAndGet();
        city.setId(id);
        repository.put(id, city);
        return id;
    }
​
    public Collection<City> findAll() {
        return repository.values();
    }
​
​
    public City findCityById(Long id) {
        return repository.get(id);
    }
​
    public Long updateCity(City city) {
        repository.put(city.getId(), city);
        return city.getId();
    }
​
    public Long deleteCity(Long id) {
        repository.remove(id);
        return id;
    }
}

2.3.业务处理层

实现请求处理的Handler,可以看作是服务层代码


@Component
public class CityHandler {
​
    private final CityRepository cityRepository;
​
    @Autowired
    public CityHandler(CityRepository cityRepository) {
        this.cityRepository = cityRepository;
    }
​
    public Mono<Long> save(City city) {
        return Mono.create(cityMonoSink -> cityMonoSink.success(cityRepository.save(city)));
    }
​
    public Mono<City> findCityById(Long id) {
        return Mono.justOrEmpty(cityRepository.findCityById(id));
    }
​
    public Flux<City> findAllCity() {
        return Flux.fromIterable(cityRepository.findAll());
    }
​
    public Mono<Long> modifyCity(City city) {
        return Mono.create(cityMonoSink -> cityMonoSink.success(cityRepository.updateCity(city)));
    }
​
    public Mono<Long> deleteCity(Long id) {
        return Mono.create(cityMonoSink -> cityMonoSink.success(cityRepository.deleteCity(id)));
    }
}

从返回值可以看出,Mono 和 Flux 适用于两个场景,即:

  1. Mono:实现发布者,并返回 0 或 1 个元素,即单对象。
  2. Flux:实现发布者,并返回 N 个元素,即 List 列表对象。

有人会问,这为啥不直接返回对象,比如返回 City/Long/List。原因是,直接使用 Flux 和 Mono 是非阻塞写法,相当于回调方式。利用函数式可以减少了回调,因此会看不到相关接口。这恰恰是 WebFlux 的好处:集合了非阻塞 + 异步。

Mono 常用的方法有:

  • Mono.create():使用 MonoSink 来创建 Mono。
  • Mono.justOrEmpty():从一个 Optional 对象或 null 对象中创建 Mono。
  • Mono.error():创建一个只包含错误消息的 Mono。
  • Mono.never():创建一个不包含任何消息通知的 Mono。
  • Mono.delay():在指定的延迟时间之后,创建一个 Mono,产生数字 0 作为唯一值。

Flux 常用的方法:

Flux 最值得一提的是 fromIterable 方法,fromIterable(Iterable it) 可以发布 Iterable 类型的元素。当然,Flux 也包含了基础的操作:map、merge、concat、flatMap、take,这里就不展开介绍了。

2.4.控制层

控制层的写法,除了返回值类型使用Mono和Flux,其他都与我们传统的springMVC注解写法一致。


@RestController
@RequestMapping(value = "/city")
public class CityWebFluxController {
​
    @Autowired
    private CityHandler cityHandler;
​
    @GetMapping(value = "/{id}")
    public Mono<City> findCityById(@PathVariable("id") Long id) {
        return cityHandler.findCityById(id);
    }
​
    @GetMapping()
    public Flux<City> findAllCity() {
        return cityHandler.findAllCity();
    }
​
    @PostMapping()
    public Mono<Long> saveCity(@RequestBody City city) {
        return cityHandler.save(city);
    }
​
    @PutMapping()
    public Mono<Long> modifyCity(@RequestBody City city) {
        return cityHandler.modifyCity(city);
    }
​
    @DeleteMapping(value = "/{id}")
    public Mono<Long> deleteCity(@PathVariable("id") Long id) {
        return cityHandler.deleteCity(id);
    }
}

第三章、webflux整和mongodb

3.1、新增 POM 依赖与配置

在 pom.xml 配置新的依赖:

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-mongodb-reactive</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-mongodb</artifactId>
        </dependency>

在这里网上说的只需要第一个依赖,但不加第二个出现了bean未注入,因此加了两个,如果未出现该类情况,可不加第二个依赖。

在 application.yml 配置下上面启动的 MongoDB 配置:

spring:
  data:
    mongodb:
      port: 27017
      host: 127.0.0.1
      database: test

如果连接不上还有另一种连接方式,可百度解决。

3.2、代码小实验

使用上一节中用到的City实体类,加上@Id


@Data
public class City {
​
    @Id
    private Long id;
    private Long provinceId;
    private String cityName;
    private String description;
}

注意: @Id 注解标记对应库表的主键或者唯一标识符。因为这个是我们的 PO,数据访问对象一一映射到数据存储。

MongoDB 数据访问层 CityRepository


@Repository
public interface CityRepository extends ReactiveMongoRepository<City, Long> {
    Flux<City> findByCityName(String cityName);
}

CityRepository 接口只要继承 ReactiveMongoRepository 类即可,默认会提供很多实现,比如 CRUD 和列表查询参数相关的实现。ReactiveMongoRepository 接口默认实现了如下:


    <S extends T> Mono<S> insert(S var1);
​
    <S extends T> Flux<S> insert(Iterable<S> var1);
​
    <S extends T> Flux<S> insert(Publisher<S> var1);
​
    <S extends T> Flux<S> findAll(Example<S> var1);
​
    <S extends T> Flux<S> findAll(Example<S> var1, Sort var2);

如图,ReactiveMongoRepository 的继承了ReactiveSortingRepository、ReactiveCrudRepository 实现了更多的常用的接口。支持关键字推断

关键字方法命名
AndfindByNameAndPwd
OrfindByNameOrSex
IdfindById
BetweenfindByIdBetween
LikefindByNameLike
NotLikefindByNameNotLike
OrderByfindByIdOrderByXDesc
NotfindByNameNot

常用案例,代码如下:


    Flux<Person> findByLastname(String lastname);
​
    @Query("{ 'firstname': ?0, 'lastname': ?1}")
    Mono<Person> findByFirstnameAndLastname(String firstname, String lastname);
​
    // Accept parameter inside a reactive type for deferred execution
    Flux<Person> findByLastname(Mono<String> lastname);
​
    Mono<Person> findByFirstnameAndLastname(Mono<String> firstname, String lastname);

3.3、处理器类 Handler 和控制器类 Controller


@Component
public class CityHandler {
​
    private final CityRepository cityRepository;
​
    @Resource
    private MongoTemplate mongoTemplate;
​
    @Autowired
    public CityHandler(CityRepository cityRepository) {
        this.cityRepository = cityRepository;
    }
​
​
    public Mono<City> save(City city) {
        return cityRepository.save(city);
    }
​
    public Mono<City> findCityById(Long id) {
        return cityRepository.findById(id);
    }
​
    public Flux<City> findAllCity() {
​
        return cityRepository.findAll();
    }
​
    public Flux<City> search(String cityName) {
​
        return cityRepository.findByCityName(cityName);
    }
​
    public Mono<City> modifyCity(City city) {
​
        return cityRepository.save(city);
    }
​
    public Mono<Long> deleteCity(Long id) {
        // 使用mongoTemplate来做删除,直接使用提供的删除方法不行
        Query query = Query.query(Criteria.where("id").is(id));
        mongoTemplate.remove(query, City.class);
​
        //cityRepository.deleteById(id);  这个方法无法删除数据
        return Mono.create(cityMonoSink -> cityMonoSink.success(id));
    }
}

在使用deleteById方法时候出现了问题,最好还是用mongoTemplate注入方法解决的。我感觉这个就是异步导致的bug,webflux可能还是不够稳定,问题较多。不要对 Mono、Flux 陌生,把它当成对象即可。继续修改控制器类 Controller,代码如下:

@RestController
@RequestMapping(value = "/citys")
public class CityWebFluxController {
​
    @Resource
    private CityHandler cityHandler;
​
    @GetMapping(value = "/{id}")
    public Mono<City> findCityById(@PathVariable("id") Long id) {
        return cityHandler.findCityById(id);
    }
​
    @GetMapping()
    public Flux<City> findAllCity() {
        return cityHandler.findAllCity();
    }
​
    @GetMapping(value = "/search/city")
    public Flux<City> search(@RequestParam("cityName") String cityName) {
        return cityHandler.search(cityName);
    }
​
    @PostMapping()
    public Mono<City> saveCity(@RequestBody City city) {
        return cityHandler.save(city);
    }
​
    @PutMapping()
    public Mono<City> modifyCity(@RequestBody City city) {
        return cityHandler.modifyCity(city);
    }
​
    @DeleteMapping(value = "/{id}")
    public Mono<Long> deleteCity(@PathVariable("id") Long id) {
        return cityHandler.deleteCity(id);
    }
}

第四章、webclient单元测试的编写

4.1、使用@SpringBootTest测试

@SpringBootTest也可用于测试 Spring WebFlux 控制器,它会将整个应用启动,并注入WebTestClient ,CityWebFluxController以及它所依赖的CityHandler.


@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class CityWebFluxControllerTest {
​
    @Autowired
    private WebTestClient webClient;
​
    private static Map<String, City> cityMap = new HashMap<>();
​
    @BeforeClass
    public static void setup() throws Exception {
        City bj= new City();
        bj.setId(1L);
        bj.setProvinceId(2L);
        bj.setCityName("BJ");
        bj.setDescription("welcome to beijing");
        cityMap.put("BJ", bj);
    }
​
    @Test
    public void testSave() throws Exception {
​
        City expectCity = webClient.post().uri("/city")
                .contentType(MediaType.APPLICATION_JSON)
                .body(BodyInserters.fromObject(cityMap.get("BJ")))
                .exchange()
                .expectStatus().isOk()
                .expectBody(City.class).returnResult().getResponseBody();
​
        Assert.assertNotNull(expectCity);
        Assert.assertEquals(expectCity.getId(), cityMap.get("BJ").getId());
        Assert.assertEquals(expectCity.getCityName(), cityMap.get("BJ").getCityName());
    }
​
}

WebTestClient.post() 方法构造了 POST 测试请求,并使用 uri 指定路由。expectStatus() 用于验证返回状态是否为 ok(),即 200 返回码。expectBody(City.class) 用于验证返回对象体是为 City 对象,并利用 returnResult 获取对象。Assert 是以前我们常用的断言方法验证测试结果。

4.2、使用@WebFluxTest和Mockito测试

@WebFluxTest 注入了 WebTestClient 对象,只用于测试 WebFlux 控制器@Controller,好处是快速,并不会将所有的Component都注入到 容器。所以Controller层依赖的CityHandler我们需要自己Mock。如果忘记了请回看第二章的内容。

@RunWith(SpringRunner.class)
@WebFluxTest
public class CityWebFluxControllerTest {
​
    @Autowired
    private WebTestClient webTestClient;
​
    @MockBean
    private CityHandler cityHandler;
​
    private static Map<String, City> cityMap = new HashMap<>();
​
    @BeforeClass
    public static void setup() throws Exception {
        City bj= new City();
        bj.setId(1L);
        bj.setProvinceId(2L);
        bj.setCityName("BJ");
        bj.setDescription("welcome to beijing");
        cityMap.put("BJ", bj);
    }
​
    @Test
    public void testSave() throws Exception {
​
        Mockito.when(this.cityHandler.save(cityMap.get("BJ")))
                .thenReturn(Mono.create(cityMonoSink -> cityMonoSink.success(cityMap.get("BJ"))));
​
        City expectCity = webTestClient.post().uri("/city")
                .contentType(MediaType.APPLICATION_JSON)
                .body(BodyInserters.fromObject(cityMap.get("BJ")))
                .exchange()
                .expectStatus().isOk()
                .expectBody(City.class).returnResult().getResponseBody();
​
​
​
        Assert.assertNotNull(expectCity);
        Assert.assertEquals(expectCity.getId(), cityMap.get("BJ").getId());
        Assert.assertEquals(expectCity.getCityName(), cityMap.get("BJ").getCityName());
    }
​
}