【Spring 5】響應式Web框架實戰(上)

Emac發表於2017-06-29

引子:被譽為“中國大資料第一人”的塗子沛先生在其成名作《資料之巔》裡提到,摩爾定律、社交媒體、資料探勘是大資料的三大成因。IBM的研究稱,整個人類文明所獲得的全部資料中,有90%是過去兩年內產生的。在此背景下,包括NoSQL,Hadoop, Spark, Storm, Kylin在內的大批新技術應運而生。其中以RxJavaReactor為代表的響應式(Reactive)程式設計技術針對的就是經典的大資料4V定義(Volume,Variety,Velocity,Value)中的Velocity,即高併發問題,而在即將釋出的Spring 5中,也引入了響應式程式設計的支援。在接下來的幾周,我會圍繞響應式程式設計分三期與你分享我的一些學習心得。本篇是第三篇,通過一個簡單的Spring 5示例應用,探一探即將於下月底釋出的Spring 5的究竟。

前情概要:

1 回顧

通過前兩篇的介紹,相信你對響應式程式設計和Spring 5已經有了一個初步的瞭解。下面我將以一個簡單的Spring 5應用為例,介紹如何使用Spring 5快速搭建一個響應式Web應用(以下簡稱RP應用)。

2 實戰

2.1 環境準備

首先,從GitHub下載我的這個示例應用,地址是github.com/emac/spring…

然後,從MongoDB官網下載最新版本的MongoDB,然後在命令列下執行mongod &啟動服務。

現在,可以先試著跑一下專案中自帶的測試用例。

./gradlew clean build

2.2 依賴介紹

接下來,看一下這個示例應用裡的和響應式程式設計相關的依賴。

compile('org.springframework.boot:spring-boot-starter-webflux')
compile('org.springframework.boot:spring-boot-starter-data-mongodb-reactive')
testCompile('io.projectreactor.addons:reactor-test')複製程式碼
  • spring-boot-starter-webflux: 啟用Spring 5的RP(Reactive Programming)支援,這是使用Spring 5開發RP應用的必要條件,就好比spring-boot-starter-web之於傳統的Spring MVC應用。
  • spring-boot-starter-data-mongodb-reactive: Spring 5中新引入的針對MongoDB的Reactive Data擴充套件庫,允許通過統一的RP風格的API操作MongoDB。
  • io.projectreactor.addons:reactor-test: Reactor(Spring 5預設使用的RP框架)提供的官方測試工具庫。

2.3 示例程式碼

不知道你是否還記得,在本系列第一篇【Spring 5】響應式Web框架前瞻裡提到,Spring 5提供了Spring MVC註解和Router Functions兩種方式來編寫RP應用。本篇我就先用大家最熟悉的MVC註解來展示如何編寫一個最簡單的RP Controller。

@RestController
public class RestaurantController {

    /**
     * 擴充套件ReactiveCrudRepository介面,提供基本的CRUD操作
     */
    private final RestaurantRepository restaurantRepository;

    /**
     * spring-boot-starter-data-mongodb-reactive提供的通用模板
     */
    private final ReactiveMongoTemplate reactiveMongoTemplate;

    public RestaurantController(RestaurantRepository restaurantRepository, ReactiveMongoTemplate reactiveMongoTemplate) {
        this.restaurantRepository = restaurantRepository;
        this.reactiveMongoTemplate = reactiveMongoTemplate;
    }

    @GetMapping("/reactive/restaurants")
    public Flux<Restaurant> findAll() {
        return restaurantRepository.findAll();
    }

    @GetMapping("/reactive/restaurants/{id}")
    public Mono<Restaurant> get(@PathVariable String id) {
        return restaurantRepository.findById(id);
    }

    @PostMapping("/reactive/restaurants")
    public Flux<Restaurant> create(@RequestBody Flux<Restaurant> restaurants) {
        return restaurants
                .buffer(10000)
                .flatMap(rs -> reactiveMongoTemplate.insert(rs, Restaurant.class));
    }

    @DeleteMapping("/reactive/restaurants/{id}")
    public Mono<Void> delete(@PathVariable String id) {
        return restaurantRepository.deleteById(id);
    }
}複製程式碼

可以看到,實現一個RP Controller和一個普通的Controller是非常類似的,最核心的區別是,優先使用RP中最基礎的兩種資料型別,Flux(對應多值)和Mono(單值),尤其是方法的引數和返回值。即便是空返回值,也應封裝為Mono<Void>。這樣做的目的是,使得應用能夠以一種統一的符合RP規範的方式處理資料,最理想的情況是從最底層的資料庫(或者其他系統外部呼叫),到最上層的Controller層,所有資料都不落地,經由各種FluxMono鋪設的“管道”,直供呼叫端。就像農夫山泉那句著名的廣告詞,我們不生產水,我們只是大自然的搬運工。

2.4 單元測試

和非RP應用的單元測試相比,RP應用的單元測試主要是使用了一個Spring 5新引入的測試工具類,WebTestClient,專門用於測試RP應用。

@RunWith(SpringRunner.class)
@SpringBootTest
public class RestaurantControllerTests {

    @Test
    public void testNormal() throws InterruptedException {
        // start from scratch
        restaurantRepository.deleteAll().block();

        // prepare
        WebTestClient webClient = WebTestClient.bindToController(new RestaurantController(restaurantRepository, reactiveMongoTemplate)).build();
        Restaurant[] restaurants = IntStream.range(0, 100)
                .mapToObj(String::valueOf)
                .map(s -> new Restaurant(s, s, s))
                .toArray(Restaurant[]::new);

        // create
        webClient.post().uri("/reactive/restaurants")
                .accept(MediaType.APPLICATION_JSON_UTF8)
                .syncBody(restaurants)
                .exchange()
                .expectStatus().isOk()
                .expectHeader().contentType(MediaType.APPLICATION_JSON_UTF8)
                .expectBodyList(Restaurant.class)
                .hasSize(100)
                .consumeWith(rs -> Flux.fromIterable(rs)
                        .log()
                        .subscribe(r1 -> {
                            // get
                            webClient.get()
                                    .uri("/reactive/restaurants/{id}", r1.getId())
                                    .accept(MediaType.APPLICATION_JSON_UTF8)
                                    .exchange()
                                    .expectStatus().isOk()
                                    .expectHeader().contentType(MediaType.APPLICATION_JSON_UTF8)
                                    .expectBody(Restaurant.class)
                                    .consumeWith(r2 -> Assert.assertEquals(r1, r2));
                        })
                );
    }
}複製程式碼

建立WebTestClient例項時,首先要繫結一下待測試的RP Controller。可以看到,和業務類一樣,編寫RP應用的單元測試,同樣也是資料不落地的流式風格。

在示例應用中可以找到更多的單元測試。

3 小結

以上就是Spring 5裡第一種,相信也將會是最常用的編寫RP應用的實現方式。介於篇幅原因,這篇就先到這裡。下篇我將詳細介紹第二種方式,Router Functions。歡迎你到我的留言板分享,和大家一起過過招。

4 參考

相關文章