11.1 使用Spring WebFlux

  1. 导入依赖
    1. <dependency>
    2. <groupId>org.springframework.boot</groupId>
    3. <artifactId>spring-boot-starter-webflux</artifactId>
    4. </dependency>
    项目中其他依赖 ```java org.springframework.boot spring-boot-starter-parent 2.0.4.RELEASE

org.springframework.boot spring-boot-starter

  1. <dependency>
  2. <groupId>org.springframework.boot</groupId>
  3. <artifactId>spring-boot-devtools</artifactId>
  4. <scope>runtime</scope>
  5. <optional>true</optional>
  6. </dependency>
  7. <dependency>
  8. <groupId>com.h2database</groupId>
  9. <artifactId>h2</artifactId>
  10. <scope>runtime</scope>
  11. </dependency>
  12. <dependency>
  13. <groupId>org.projectlombok</groupId>
  14. <artifactId>lombok</artifactId>
  15. <optional>true</optional>
  16. </dependency>
  17. <!-- Necessary Boot starters -->
  18. <dependency>
  19. <groupId>org.springframework.boot</groupId>
  20. <artifactId>spring-boot-starter-data-jpa</artifactId>
  21. </dependency>
  22. <dependency>
  23. <groupId>org.springframework.boot</groupId>
  24. <artifactId>spring-boot-starter-data-rest</artifactId>
  25. </dependency>
  26. <!-- Test dependencies -->
  27. <dependency>
  28. <groupId>org.springframework.boot</groupId>
  29. <artifactId>spring-boot-starter-test</artifactId>
  30. <scope>test</scope>
  31. </dependency>
  32. <!-- Necessary Boot starters -->
  33. <dependency>
  34. <groupId>org.springframework.boot</groupId>
  35. <artifactId>spring-boot-starter-security</artifactId>
  36. </dependency>
  37. <dependency>
  38. <groupId>org.springframework.boot</groupId>
  39. <artifactId>spring-boot-starter-webflux</artifactId>
  40. </dependency>

  1. <a name="ZPntS"></a>
  2. ## 11.1.1 编写反应式控制器
  3. DesignTacoController片段:
  4. ```java
  5. @GetMapping("/recent")
  6. public Iterable<Taco> recentTacos() {
  7. PageRequest page = PageRequest.of(0, 12, Sort.by("createdAt").descending());
  8. return tacoRepository.findAll(page).getContent();
  9. }

重写为反应式:

  1. @GetMapping("/recent")
  2. public Flux<Taco> recentTacos() {
  3. return Flux.fromIterable(tacoRepo.findAll()).take(12);
  4. }

借助Flux.fromIterable(),我们可以将Iterable转换为Flux
如果repository⼀开始就给我们⼀个Flux那就更好了,就没有必要进⾏转换了。如果能够实现这⼀点,那么recentTacos()将会写成如下形式:

  1. @GetMapping("/recent")
  2. public Flux<Taco> recentTacos() {
  3. return tacoRepository.findAll().take(12);
  4. }

返回单个值
之前的样子:

  1. @GetMapping("/{id}")
  2. public Taco tacoById(@PathVariable("id") String id) {
  3. Optional<Taco> optTaco = tacoRepo.findById(id);
  4. if (optTaco.isPresent()) {
  5. return optTaco.get();
  6. }
  7. return null;
  8. }

重写为反应式:

  1. @GetMapping("/{id}")
  2. public Mono<Taco> tacoById(@PathVariable("id") String id){
  3. return tacoRepository.findById(id);
  4. }

实现输入的反应式
原始的postTaco()实现:

  1. @PostMapping(consumes="application/json")
  2. @ResponseStatus(HttpStatus.CREATED)
  3. public Taco postTaco(@RequestBody Taco taco) {
  4. return tacoRepo.save(taco);
  5. }

反应式实现:

  1. @PostMapping(consumes = "application/json")
  2. @ResponseStatus(HttpStatus.CREATED)
  3. public Mono<Taco> postTaco(@RequestBody Mono<Taco> tacoMono) {
  4. return tacoRepository.saveAll(tacoMono).next();
  5. }

11.2 定义函数式请求处理器

使⽤Spring的函数式编程模型编写API会涉及4个主要的类型:

  • RequestPredicate:声明要处理的请求类型。
  • RouterFunction:声明如何将请求路由到处理器代码中。
  • ServerRequest:代表⼀个HTTP请求,包括对请求头和请求体的访问。
  • ServerResponse:代表⼀个HTTP响应,包括响应头和响应体信息。

下⾯是⼀个将所有类型组合在⼀起的Hello World样例:

  1. @Configuration
  2. public class RouterFunctionConfig {
  3. @Bean
  4. public RouterFunction<?> helloRouterFunction() {
  5. return RouterFunctions.route(RequestPredicates.GET("/hello"),
  6. serverRequest -> ServerResponse.ok().body(Mono.just("hello world!"), String.class))
  7. .andRoute(RequestPredicates.GET("/bye"), serverRequest -> ServerResponse.ok().body(Mono.just("see ya!"), String.class));
  8. }
  9. }

如下的配置类是DesignTacoController的函数式实现:

  1. @Configuration
  2. public class RouterFunctionConfig {
  3. @Autowired
  4. private TacoRepository tacoRepository;
  5. @Bean
  6. public RouterFunction<?> routerFunction() {
  7. return RouterFunctions.route(RequestPredicates.GET("/design/taco"), this::recents)
  8. .andRoute(RequestPredicates.POST("/design"), this::postTaco);
  9. }
  10. public Mono<ServerResponse> recents(ServerRequest request) {
  11. return ServerResponse.ok()
  12. .body(tacoRepository.findAll().take(12), Taco.class);
  13. }
  14. public Mono<ServerResponse> postTaco(ServerRequest request) {
  15. Mono<Taco> taco = request.bodyToMono(Taco.class);
  16. Mono<Taco> savedTaco = tacoRepository.save(taco);
  17. return ServerResponse.created(URI.create("http://localhost:8080/design/taco/" + savedTaco.block().getId())).body(savedTaco, Taco.class);
  18. }
  19. }

11.3 测试反应式控制器

11.3.1 测试get请求

  1. public class DesignTacoControllerTest {
  2. @Test
  3. public void shouldReturnRecentTacos() {
  4. Taco[] tacos = {
  5. testTaco(1L), testTaco(2L),
  6. testTaco(3L), testTaco(4L),
  7. testTaco(5L), testTaco(6L),
  8. testTaco(7L), testTaco(8L),
  9. testTaco(9L), testTaco(10L),
  10. testTaco(11L), testTaco(12L),
  11. testTaco(13L), testTaco(14L),
  12. testTaco(15L), testTaco(16L)
  13. };
  14. Flux<Taco> tacoFlux = Flux.just(tacos);
  15. TacoRepository tacoRepository = Mockito.mock(TacoRepository.class);
  16. Mockito.when(tacoRepository.findAll()).thenReturn(tacoFlux);
  17. WebTestClient testClient = WebTestClient.bindToController(new DesignTacoController(tacoRepository)).build();
  18. testClient.get().uri("/design/recent")
  19. .exchange()
  20. .expectStatus().isOk()
  21. .expectBody()
  22. .jsonPath("$").isArray()
  23. .jsonPath("$").isNotEmpty()
  24. .jsonPath("$[0].id").isEqualTo(tacos[0].getId().toString())
  25. .jsonPath("$[0].name").isEqualTo("Taco 1")
  26. .jsonPath("$[1].id").isEqualTo(tacos[1].getId().toString())
  27. .jsonPath("$[1].name").isEqualTo("Taco 2")
  28. .jsonPath("$[11].id").isEqualTo(tacos[11].getId().toString())
  29. .jsonPath("$[11].name").isEqualTo("Taco 12")
  30. .jsonPath("$[12]").doesNotExist();
  31. }
  32. private Taco testTaco(Long number) {
  33. Taco taco = new Taco();
  34. taco.setId(UUID.randomUUID().toString());
  35. taco.setName("Taco " + number);
  36. ArrayList<Ingredient> ingredients = new ArrayList<>();
  37. ingredients.add(new Ingredient("INGA", "Ingredient A", Ingredient.Type.WRAP));
  38. ingredients.add(new Ingredient("INGB", "Ingredient B", Ingredient.Type.PROTEIN));
  39. taco.setIngredients(ingredients);
  40. return taco;
  41. }
  42. }

也可以用下面的替换上面对应的部分

  1. testClient.get().uri("/design/recent")
  2. .accept(MediaType.APPLICATION_JSON)
  3. .exchange()
  4. .expectStatus().isOk()
  5. .expectBodyList(Taco.class)
  6. .contains(Arrays.copyOf(tacos, 12));

11.3.2 测试Post请求

  1. @Test
  2. public void shouldSaveTaco() {
  3. TacoRepository tacoRepository = Mockito.mock(TacoRepository.class);
  4. Mono<Taco> unsavedTacoMono = Mono.just(testTaco(null));
  5. Taco savedTaco = testTaco(null);
  6. Mono<Taco> savedTacoMono = Mono.just(savedTaco);
  7. Mockito.when(tacoRepository.save(ArgumentMatchers.any())).thenReturn(savedTacoMono);
  8. WebTestClient testClient = WebTestClient.bindToController(new DesignTacoController(tacoRepository)).build();
  9. testClient.post()
  10. .uri("/design")
  11. .contentType(MediaType.APPLICATION_JSON)
  12. .body(unsavedTacoMono, Taco.class)
  13. .exchange()
  14. .expectStatus().isCreated()
  15. .expectBody(Taco.class)
  16. .isEqualTo(savedTaco);
  17. }

11.3.3 使用实时服务器进行测试

  1. @RunWith(SpringRunner.class)
  2. @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
  3. public class DesignTacoControllerWebTest {
  4. @Autowired
  5. private WebTestClient testClient;
  6. }

11.4 反应式消费API

11.4.1 获取资源