Skip to content

12.1.2 Viết các controller phản ứng

Bạn có thể nhớ rằng trong chương 7, bạn đã tạo một vài controller cho REST API của Taco Cloud. Các controller đó có các phương thức xử lý yêu cầu liên quan đến đầu vào và đầu ra dưới dạng các kiểu miền (chẳng hạn như TacoOrderTaco) hoặc các collection của các kiểu đó. Để nhắc lại, hãy xem đoạn mã sau từ TacoController mà bạn đã viết trong chương 7:

java
@RestController
@RequestMapping(path="/api/tacos",
      produces="application/json")
@CrossOrigin(origins="*")
public class TacoController {

...

  @GetMapping(params="recent")
  public Iterable<Taco> recentTacos() {
    PageRequest page = PageRequest.of(
          0, 12, Sort.by("createdAt").descending());
    return tacoRepo.findAll(page).getContent();
  }

...

}

Như đã viết, controller recentTacos() xử lý các yêu cầu HTTP GET đến /api/tacos?recent để trả về danh sách các taco mới được tạo gần đây. Cụ thể hơn, nó trả về một Iterable của kiểu Taco. Đó là vì phương thức findAll() của repository trả về như vậy, hoặc chính xác hơn là từ phương thức getContent() trên đối tượng Page được trả về bởi findAll().

Điều đó vẫn hoạt động tốt, nhưng Iterable không phải là một kiểu phản ứng. Bạn sẽ không thể áp dụng bất kỳ thao tác phản ứng nào lên nó, và framework cũng không thể tận dụng nó như một kiểu phản ứng để phân chia công việc qua nhiều luồng. Điều bạn muốn là để recentTacos() trả về một Flux<Taco>.

Một cách đơn giản nhưng hơi hạn chế là viết lại recentTacos() để chuyển đổi Iterable sang Flux. Và trong khi làm điều đó, bạn có thể loại bỏ đoạn mã phân trang và thay bằng gọi take() trên Flux như sau:

java
@GetMapping(params="recent")
public Flux<Taco> recentTacos() {
  return Flux.fromIterable(tacoRepo.findAll()).take(12);
}

Sử dụng Flux.fromIterable(), bạn chuyển đổi Iterable<Taco> sang Flux<Taco>. Và một khi bạn đã làm việc với Flux, bạn có thể dùng take() để giới hạn số lượng phần tử trả về tối đa là 12 đối tượng Taco. Mã không những đơn giản hơn, mà còn làm việc với một Flux phản ứng thay vì Iterable đơn thuần.

Viết mã phản ứng cho đến nay là một bước đi đúng đắn. Nhưng sẽ tốt hơn nữa nếu repository ban đầu trả về một Flux để bạn không phải chuyển đổi. Nếu đúng như vậy, recentTacos() có thể được viết như sau:

java
@GetMapping(params="recent")
public Flux<Taco> recentTacos() {
  return tacoRepo.findAll().take(12);
}

Điều đó còn tốt hơn! Lý tưởng nhất, một controller phản ứng nên là điểm đầu trong một stack phản ứng hoàn chỉnh từ đầu đến cuối, bao gồm controller, repository, cơ sở dữ liệu, và bất kỳ service trung gian nào. Một stack phản ứng toàn diện như vậy được minh họa trong hình 12.3.

Hình 12.3
Hình 12.3 Để tận dụng tối đa lợi ích của một framework web phản ứng, nó nên là một phần của toàn bộ stack phản ứng.

Một stack như vậy yêu cầu repository được viết để trả về Flux thay vì Iterable. Chúng ta sẽ tìm hiểu cách viết repository phản ứng trong chương tiếp theo, nhưng dưới đây là một cái nhìn sơ lược về một TacoRepository phản ứng có thể trông như thế nào:

java
package tacos.data;

import org.springframework.data.repository.reactive.ReactiveCrudRepository;
import tacos.Taco;

public interface TacoRepository
      extends ReactiveCrudRepository<Taco, Long> {
}

Điều quan trọng nhất cần lưu ý lúc này là, ngoại trừ việc làm việc với Flux thay vì Iterable, cũng như cách bạn có được Flux, mô hình lập trình để định nghĩa một controller WebFlux phản ứng không khác gì so với controller Spring MVC không phản ứng. Cả hai đều sử dụng annotation @RestController@RequestMapping ở cấp lớp. Và cả hai đều có các hàm xử lý yêu cầu được chú thích với @GetMapping ở cấp phương thức. Thực chất chỉ khác nhau ở kiểu dữ liệu mà các phương thức handler trả về.

Một điểm quan trọng khác là mặc dù bạn nhận được một Flux<Taco> từ repository, bạn có thể trả về nó mà không cần gọi subscribe(). Thực tế là framework sẽ tự động gọi subscribe() cho bạn. Điều này có nghĩa là khi một yêu cầu đến /api/tacos?recent được xử lý, phương thức recentTacos() sẽ được gọi và trả về trước cả khi dữ liệu được truy xuất từ cơ sở dữ liệu!

TRẢ VỀ GIÁ TRỊ ĐƠN

Một ví dụ khác, hãy xem phương thức tacoById() trong TacoController như đã viết trong chương 7:

java
@GetMapping("/{id}")
public Taco tacoById(@PathVariable("id") Long id) {
  Optional<Taco> optTaco = tacoRepo.findById(id);
  if (optTaco.isPresent()) {
    return optTaco.get();
  }
  return null;
}

Phương thức này xử lý các yêu cầu GET đến /tacos/{id} và trả về một đối tượng Taco duy nhất. Vì findById() của repository trả về Optional, bạn cũng phải viết thêm mã để xử lý trường hợp không có giá trị. Nhưng giả sử rằng findById() trả về Mono<Taco> thay vì Optional<Taco>. Trong trường hợp đó, bạn có thể viết lại tacoById() như sau:

java
@GetMapping("/{id}")
public Mono<Taco> tacoById(@PathVariable("id") Long id) {
  return tacoRepo.findById(id);
}

Thật tuyệt vời! Đơn giản hơn rất nhiều. Quan trọng hơn, bằng cách trả về Mono<Taco> thay vì Taco, bạn cho phép Spring WebFlux xử lý phản hồi một cách phản ứng. Kết quả là, API của bạn sẽ có khả năng mở rộng tốt hơn khi phải xử lý tải lớn.

LÀM VIỆC VỚI KIỂU RxJava

Cần lưu ý rằng mặc dù các kiểu của Reactor như FluxMono là lựa chọn tự nhiên khi làm việc với Spring WebFlux, bạn cũng có thể chọn làm việc với các kiểu của RxJava như ObservableSingle. Ví dụ, giả sử có một service nằm giữa TacoController và repository backend sử dụng RxJava, thì bạn có thể viết phương thức recentTacos() như sau:

java
@GetMapping(params = "recent")
public Observable<Taco> recentTacos() {
  return tacoService.getRecentTacos();
}

Tương tự, phương thức tacoById() cũng có thể viết để xử lý Single của RxJava thay vì Mono, như sau:

java
@GetMapping("/{id}")
public Single<Taco> tacoById(@PathVariable("id") Long id) {
  return tacoService.lookupTaco(id);
}

Ngoài ra, các phương thức controller của Spring WebFlux cũng có thể trả về Completable của RxJava, tương đương với Mono<Void> trong Reactor. WebFlux cũng có thể trả về Flowable của RxJava như một lựa chọn thay thế cho Observable hoặc Flux của Reactor.

XỬ LÝ ĐẦU VÀO MỘT CÁCH PHẢN ỨNG

Cho đến giờ, chúng ta mới chỉ quan tâm đến kiểu dữ liệu phản ứng mà các phương thức controller trả về. Nhưng với Spring WebFlux, bạn cũng có thể nhận một Mono hoặc Flux làm đầu vào cho một phương thức handler. Để minh họa, hãy xem lại cách cài đặt ban đầu của postTaco() từ TacoController:

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

Như ban đầu, postTaco() không chỉ trả về một đối tượng Taco đơn giản mà còn nhận một đối tượng Taco được ánh xạ từ nội dung trong phần thân của yêu cầu. Điều này có nghĩa là postTaco() không thể được gọi cho đến khi payload trong yêu cầu được giải mã hoàn toàn và sử dụng để tạo đối tượng Taco. Nó cũng đồng nghĩa postTaco() sẽ không trả về cho đến khi lời gọi chặn tới save() trong repository hoàn thành. Tóm lại, yêu cầu bị chặn hai lần: khi vào postTaco() và bên trong postTaco(). Nhưng bằng cách áp dụng một chút mã phản ứng vào postTaco() như sau, bạn có thể biến nó thành một phương thức xử lý yêu cầu hoàn toàn không chặn:

java
@PostMapping(consumes = "application/json")
@ResponseStatus(HttpStatus.CREATED)
public Mono<Taco> postTaco(@RequestBody Mono<Taco> tacoMono) {
  return tacoRepo.saveAll(tacoMono).next();
}

Ở đây, postTaco() nhận một Mono<Taco> và gọi phương thức saveAll() của repository, vốn chấp nhận bất kỳ implementation nào của Reactive Streams Publisher, bao gồm Mono hoặc Flux. Phương thức saveAll() trả về một Flux<Taco>, nhưng vì bạn bắt đầu với một Mono, bạn biết rằng sẽ có tối đa một Taco được phát ra bởi Flux. Do đó, bạn có thể gọi next() để lấy một Mono<Taco> và trả về từ postTaco().

Bằng cách nhận Mono<Taco> làm đầu vào, phương thức được gọi ngay lập tức mà không cần chờ Taco được giải mã từ request body. Và vì repository cũng là phản ứng, nó sẽ chấp nhận Mono và lập tức trả về một Flux<Taco>, từ đó bạn gọi next() và trả về Mono<Taco> kết quả... tất cả xảy ra trước cả khi request được xử lý xong!

Ngoài ra, bạn cũng có thể cài đặt postTaco() như sau:

java
@PostMapping(consumes = "application/json")
@ResponseStatus(HttpStatus.CREATED)
public Mono<Taco> postTaco(@RequestBody Mono<Taco> tacoMono) {
  return tacoMono.flatMap(tacoRepo::save);
}

Cách tiếp cận này đảo ngược quy trình để tacoMono là phần dẫn động hành động. Đối tượng Taco trong tacoMono được đưa vào phương thức save() của repository thông qua flatMap(), tạo ra một Mono<Taco> mới được trả về.

Cả hai cách đều hoạt động tốt, và có lẽ còn nhiều cách khác để viết postTaco(). Hãy chọn cách phù hợp và dễ hiểu nhất đối với bạn.

Spring WebFlux là một lựa chọn tuyệt vời thay thế cho Spring MVC, cung cấp khả năng viết ứng dụng web phản ứng bằng cùng một mô hình phát triển như Spring MVC. Nhưng Spring còn có một điều thú vị khác. Hãy cùng khám phá cách tạo API phản ứng bằng phong cách lập trình hàm của Spring.

Released under the MIT License.