引子:被譽爲「中國大數據第一人」的塗子沛先生在其成名做《數據之巔》裏提到,摩爾定律、社交媒體、數據挖掘是大數據的三大成因。IBM的研究稱,整我的類文明所得到的所有數據中,有90%是過去兩年內產生的。在此背景下,包括NoSQL,Hadoop, Spark, Storm, Kylin在內的大批新技術應運而生。其中以RxJava和Reactor爲表明的響應式(Reactive)編程技術針對的就是經典的大數據4V定義(Volume,Variety,Velocity,Value)中的Velocity,即高併發問題,而在即將發佈的Spring 5中,也引入了響應式編程的支持。在接下來的幾周,我會圍繞響應式編程分三期與你分享個人一些學習心得。本篇是第三篇,經過一個簡單的Spring 5示例應用,探一探即將於下月底發佈的Spring 5的究竟。html
前情概要:java
經過前兩篇的介紹,相信你對響應式編程和Spring 5已經有了一個初步的瞭解。下面我將以一個簡單的Spring 5應用爲例,介紹如何使用Spring 5快速搭建一個響應式Web應用(如下簡稱RP應用)。react
首先,從GitHub下載個人這個示例應用,地址是github.com/emac/spring…。git
而後,從MongoDB官網下載最新版本的MongoDB,而後在命令行下運行mongod &
啓動服務。github
如今,能夠先試着跑一下項目中自帶的測試用例。web
./gradlew clean build
spring
接下來,看一下這個示例應用裏的和響應式編程相關的依賴。mongodb
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 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層,全部數據都不落地,經由各類Flux
和Mono
鋪設的「管道」,直供調用端。就像農夫山泉那句著名的廣告詞,咱們不生產水,咱們只是大天然的搬運工。編程
和非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應用的單元測試,一樣也是數據不落地的流式風格。
在示例應用中能夠找到更多的單元測試。
以上就是Spring 5裏第一種,相信也將會是最經常使用的編寫RP應用的實現方式。介於篇幅緣由,這篇就先到這裏。下篇我將詳細介紹第二種方式,Router Functions。歡迎你到個人留言板分享,和你們一塊兒過過招。