12.2 Định nghĩa các handler xử lý yêu cầu theo kiểu hàm
Mô hình lập trình dựa trên annotation của Spring MVC đã tồn tại từ Spring 2.5 và rất phổ biến. Tuy nhiên, nó cũng có một vài hạn chế.
Đầu tiên, bất kỳ mô hình lập trình nào dựa trên annotation cũng đều dẫn đến sự tách biệt giữa phần định nghĩa annotation là gì và cách nó được thực thi. Annotation chỉ định nghĩa cái gì (what); còn cách thực thi (how) được định nghĩa ở một nơi khác trong mã nguồn framework. Sự chia tách này làm cho mô hình lập trình trở nên phức tạp hơn khi muốn tùy chỉnh hoặc mở rộng, vì các thay đổi như vậy yêu cầu phải thao tác trong phần mã nằm ngoài annotation. Thêm vào đó, việc debug mã này cũng khó khăn hơn vì bạn không thể đặt breakpoint vào một annotation.
Ngoài ra, khi Spring ngày càng trở nên phổ biến, các lập trình viên mới đến từ các ngôn ngữ hoặc framework khác có thể thấy Spring MVC (và WebFlux) dựa trên annotation là điều lạ lẫm. Như một giải pháp thay thế cho WebFlux, Spring cung cấp một mô hình lập trình hàm (functional programming) để định nghĩa các API phản ứng.
Mô hình lập trình mới này được sử dụng giống như một thư viện hơn là một framework, cho phép bạn ánh xạ các yêu cầu đến mã xử lý mà không cần dùng annotation. Việc viết API bằng mô hình lập trình hàm của Spring bao gồm bốn loại chính sau:
- RequestPredicate —— Khai báo loại yêu cầu nào sẽ được xử lý
- RouteFunction —— Khai báo cách định tuyến yêu cầu phù hợp đến mã xử lý
- ServerRequest —— Đại diện cho một yêu cầu HTTP, bao gồm truy cập thông tin header và body
- ServerResponse —— Đại diện cho một phản hồi HTTP, bao gồm thông tin header và body
Là một ví dụ đơn giản sử dụng tất cả các loại trên, hãy xem ví dụ HelloWorld sau:
package hello;
import static org.springframework.web
.reactive.function.server.RequestPredicates.GET;
import static org.springframework.web
.reactive.function.server.RouterFunctions.route;
import static org.springframework.web
.reactive.function.server.ServerResponse.ok;
import static reactor.core.publisher.Mono.just;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.function.server.RouterFunction;
@Configuration
public class RouterFunctionConfig {
@Bean
public RouterFunction<?> helloRouterFunction() {
return route(GET("/hello"),
request -> ok().body(just("Hello World!"), String.class))
;
}
}Điều đầu tiên bạn có thể thấy là bạn đã import tĩnh một vài class tiện ích để sử dụng khi tạo các thành phần functional ở trên. Bạn cũng import tĩnh Mono để giúp phần còn lại của mã dễ đọc và hiểu hơn.
Trong lớp @Configuration này, bạn có một phương thức @Bean duy nhất kiểu RouterFunction<?>. Như đã đề cập, một RouterFunction khai báo ánh xạ giữa một hoặc nhiều RequestPredicate và các hàm xử lý các yêu cầu khớp với chúng.
Phương thức route() từ RouterFunctions nhận hai tham số: một RequestPredicate và một hàm để xử lý các yêu cầu phù hợp. Trong trường hợp này, phương thức GET() từ RequestPredicates khai báo một RequestPredicate khớp với các yêu cầu HTTP GET đến đường dẫn /hello.
Về phía hàm xử lý, nó được viết dưới dạng lambda, mặc dù cũng có thể là một tham chiếu phương thức (method reference). Mặc dù không khai báo rõ ràng, lambda này nhận vào một ServerRequest làm tham số. Nó trả về một ServerResponse sử dụng ok() từ ServerResponse và body() từ BodyBuilder, được trả về từ ok(). Mục đích là tạo ra một phản hồi với mã trạng thái HTTP 200 (OK) và phần thân có nội dung "Hello World!"
Như được viết, phương thức helloRouterFunction() khai báo một RouterFunction xử lý duy nhất một loại yêu cầu. Nhưng nếu bạn cần xử lý loại yêu cầu khác, bạn không cần phải viết thêm một phương thức @Bean (dù bạn có thể làm vậy). Bạn chỉ cần gọi andRoute() để khai báo thêm một ánh xạ giữa RequestPredicate và hàm xử lý. Ví dụ, sau đây là cách bạn có thể thêm một handler cho yêu cầu GET đến /bye:
@Bean
public RouterFunction<?> helloRouterFunction() {
return route(GET("/hello"),
request -> ok().body(just("Hello World!"), String.class))
.andRoute(GET("/bye"),
request -> ok().body(just("See ya!"), String.class))
;
}Các ví dụ Hello World là tốt để bắt đầu tìm hiểu một khái niệm mới. Nhưng hãy nâng cấp nó một chút để xem cách sử dụng mô hình lập trình web hàm của Spring trong các kịch bản thực tế.
Để minh họa cách mô hình lập trình hàm có thể được sử dụng trong một ứng dụng thực tế, hãy viết lại chức năng của TacoController theo phong cách hàm. Lớp cấu hình sau đây là phiên bản hàm tương đương với TacoController:
package tacos.web.api;
import static org.springframework.web.reactive.function.server.RequestPredicates.GET;
import static org.springframework.web.reactive.function.server.RequestPredicates.POST;
import static org.springframework.web.reactive.function.server.RequestPredicates.queryParam;
import static org.springframework.web.reactive.function.server.RouterFunctions.route;
import java.net.URI;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import reactor.core.publisher.Mono;
import tacos.Taco;
import tacos.data.TacoRepository;
@Configuration
public class RouterFunctionConfig {
@Autowired
private TacoRepository tacoRepo;
@Bean
public RouterFunction<?> routerFunction() {
return route(GET("/api/tacos").
and(queryParam("recent", t->t != null )),
this::recents)
.andRoute(POST("/api/tacos"), this::postTaco);
}
public Mono<ServerResponse> recents(ServerRequest request) {
return ServerResponse.ok()
.body(tacoRepo.findAll().take(12), Taco.class);
}
public Mono<ServerResponse> postTaco(ServerRequest request) {
return request.bodyToMono(Taco.class)
.flatMap(taco -> tacoRepo.save(taco))
.flatMap(savedTaco -> {
return ServerResponse
.created(URI.create(
"http://localhost:8080/api/tacos/" +
savedTaco.getId()))
.body(savedTaco, Taco.class);
});
}
}Như bạn thấy, phương thức routerFunction() khai báo một bean RouterFunction<?>, giống như ví dụ Hello World. Nhưng nó khác biệt ở chỗ loại yêu cầu được xử lý và cách chúng được xử lý. Trong trường hợp này, RouterFunction được tạo để xử lý yêu cầu GET đến /api/tacos?recent và yêu cầu POST đến /api/tacos.
Điều đáng chú ý hơn là các route này được xử lý bằng tham chiếu phương thức (method references). Lambda rất tiện khi hành vi đằng sau RouterFunction khá đơn giản và ngắn gọn. Tuy nhiên, trong nhiều trường hợp, tốt hơn là trích xuất logic xử lý ra thành một phương thức riêng (hoặc thậm chí là một phương thức trong một lớp khác) để giữ cho mã dễ đọc.
Trong trường hợp của bạn, yêu cầu GET đến /api/tacos?recent sẽ được xử lý bởi phương thức recents(). Phương thức này sử dụng TacoRepository đã được inject để lấy một Flux<Taco>, từ đó chọn 12 phần tử. Sau đó, nó bao bọc Flux<Taco> này trong một Mono<ServerResponse> để đảm bảo rằng phản hồi có mã trạng thái HTTP 200 (OK) bằng cách gọi ok() từ ServerResponse. Điều quan trọng là phải hiểu rằng dù có tối đa 12 taco được trả về, nhưng chỉ có một phản hồi HTTP – vì vậy nó được bao bọc trong Mono chứ không phải Flux. Nội bộ, Spring vẫn sẽ stream Flux<Taco> về phía client dưới dạng Flux.
Trong khi đó, các yêu cầu POST đến /api/tacos được xử lý bởi phương thức postTaco(), phương thức này sẽ trích xuất một Mono<Taco> từ nội dung của ServerRequest đến. Sau đó, postTaco() sử dụng một chuỗi các thao tác flatMap() để lưu taco đó vào TacoRepository và tạo một ServerResponse với mã trạng thái HTTP 201 (CREATED) cùng với đối tượng Taco đã lưu trong phần thân phản hồi.
Các thao tác flatMap() được sử dụng để đảm bảo rằng tại mỗi bước trong luồng xử lý, kết quả ánh xạ đều được bao bọc trong một Mono, bắt đầu với Mono<Taco> sau flatMap() đầu tiên và cuối cùng là Mono<ServerResponse> được trả về từ postTaco().
