2 实战
2.1 环境准备
首先,从GitHub下载我的这个示例应用,地址是https://github.com/emac/spring5-features-demo。
然后,从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')
- 1
- 2
- 3
- 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);
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
可以看到,实现一个RP Controller和一个普通的Controller是非常类似的,最核心的区别是,优先使用RP中最基础的两种数据类型,Flux
(对应多值)和Mono
(单值),尤其是方法的参数和返回值。即便是空返回值,也应封装为Mono<Void>
。这样做的目的是,使得应用能够以一种统一的符合RP规范的方式处理数据,最理想的情况是从最底层的数据库(或者其他系统外部调用),到最上层的Controller层,所有数据都不落地,经由各种Flux
和Mono
铺设的“管道”,直供调用端。就像农夫山泉那句著名的广告词,我们不生产水,我们只是大自然的搬运工。
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));
})
);
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
创建WebTestClient
实例时,首先要绑定一下待测试的RP Controller。可以看到,和业务类一样,编写RP应用的单元测试,同样也是数据不落地的流式风格。
在示例应用中可以找到更多的单元测试。
3 小结
以上就是Spring 5里第一种,相信也将会是最常用的编写RP应用的实现方式.