引子:被譽為“中國大資料第一人”的塗子沛先生在其成名作《資料之巔》裡提到,摩爾定律、社交媒體、資料探勘是大資料的三大成因。IBM的研究稱,整個人類文明所獲得的全部資料中,有90%是過去兩年內產生的。在此背景下,包括NoSQL,Hadoop, Spark, Storm, Kylin在內的大批新技術應運而生。其中以RxJava和Reactor為代表的響應式(Reactive)程式設計技術針對的就是經典的大資料4V定義(Volume,Variety,Velocity,Value)中的Velocity,即高併發問題,而在即將釋出的Spring 5中,也引入了響應式程式設計的支援。在接下來的幾周,我會圍繞響應式程式設計分三期與你分享我的一些學習心得。本篇是第三篇(下),通過一個簡單的Spring 5示例應用,探一探即將於下月底釋出的Spring 5的究竟。
前情概要:
1 回顧
上篇介紹瞭如何使用Spring MVC註解實現一個響應式Web應用(以下簡稱RP應用),本篇接著介紹另一種實現方式——Router Functions。
2 實戰
2.1 Router Functions
Router Functions是Spring 5新引入的一套Reactive風格(基於Flux和Mono)的函式式介面,主要包括RouterFunction
,HandlerFunction
和HandlerFilterFunction
,分別對應Spring MVC中的@RequestMapping
,@Controller
和HandlerInterceptor
(或者Servlet規範中的Filter
)。
和Router Functions搭配使用的是兩個新的請求/響應模型,ServerRequest
和ServerResponse
,這兩個模型同樣提供了Reactive風格的介面。
2.2 示例程式碼
下面接著看我GitHub上的示例工程裡的例子。
2.2.1 自定義RouterFunction和HandlerFilterFunction
@Configuration
public class RestaurantServer implements CommandLineRunner {
@Autowired
private RestaurantHandler restaurantHandler;
/**
* 註冊自定義RouterFunction
*/
@Bean
public RouterFunction<ServerResponse> restaurantRouter() {
RouterFunction<ServerResponse> router = route(GET("/reactive/restaurants").and(accept(APPLICATION_JSON_UTF8)), restaurantHandler::findAll)
.andRoute(GET("/reactive/delay/restaurants").and(accept(APPLICATION_JSON_UTF8)), restaurantHandler::findAllDelay)
.andRoute(GET("/reactive/restaurants/{id}").and(accept(APPLICATION_JSON_UTF8)), restaurantHandler::get)
.andRoute(POST("/reactive/restaurants").and(accept(APPLICATION_JSON_UTF8)).and(contentType(APPLICATION_JSON_UTF8)), restaurantHandler::create)
.andRoute(DELETE("/reactive/restaurants/{id}").and(accept(APPLICATION_JSON_UTF8)), restaurantHandler::delete)
// 註冊自定義HandlerFilterFunction
.filter((request, next) -> {
if (HttpMethod.PUT.equals(request.method())) {
return ServerResponse.status(HttpStatus.BAD_REQUEST).build();
}
return next.handle(request);
});
return router;
}
@Override
public void run(String... args) throws Exception {
RouterFunction<ServerResponse> router = restaurantRouter();
// 轉化為通用的Reactive HttpHandler
HttpHandler httpHandler = toHttpHandler(router);
// 適配成Netty Server所需的Handler
ReactorHttpHandlerAdapter httpAdapter = new ReactorHttpHandlerAdapter(httpHandler);
// 建立Netty Server
HttpServer server = HttpServer.create("localhost", 9090);
// 註冊Handler並啟動Netty Server
server.newHandler(httpAdapter).block();
}
}
可以看到,使用Router Functions實現RP應用時,你需要自己建立和管理容器,也就是說Spring 5並沒有針對Router Functions提供IoC支援,這是Router Functions和Spring MVC相比最大的不同。除此之外,你需要通過RouterFunction
的API(而不是註解)來配置路由表和過濾器。對於簡單的應用,這樣做問題不大,但對於上規模的應用,就會導致兩個問題:1)Router的定義越來越龐大;2)由於URI和Handler分開定義,路由表的維護成本越來越高。那為什麼Spring 5會選擇這種方式定義Router呢?接著往下看。
2.2.2 自定義HandlerFunction
@Component
public class RestaurantHandler {
/**
* 擴充套件ReactiveCrudRepository介面,提供基本的CRUD操作
*/
private final RestaurantRepository restaurantRepository;
/**
* spring-boot-starter-data-mongodb-reactive提供的通用模板
*/
private final ReactiveMongoTemplate reactiveMongoTemplate;
public RestaurantHandler(RestaurantRepository restaurantRepository, ReactiveMongoTemplate reactiveMongoTemplate) {
this.restaurantRepository = restaurantRepository;
this.reactiveMongoTemplate = reactiveMongoTemplate;
}
public Mono<ServerResponse> findAll(ServerRequest request) {
Flux<Restaurant> result = restaurantRepository.findAll();
return ok().contentType(APPLICATION_JSON_UTF8).body(result, Restaurant.class);
}
public Mono<ServerResponse> findAllDelay(ServerRequest request) {
Flux<Restaurant> result = restaurantRepository.findAll().delayElements(Duration.ofSeconds(1));
return ok().contentType(APPLICATION_JSON_UTF8).body(result, Restaurant.class);
}
public Mono<ServerResponse> get(ServerRequest request) {
String id = request.pathVariable("id");
Mono<Restaurant> result = restaurantRepository.findById(id);
return ok().contentType(APPLICATION_JSON_UTF8).body(result, Restaurant.class);
}
public Mono<ServerResponse> create(ServerRequest request) {
Flux<Restaurant> restaurants = request.bodyToFlux(Restaurant.class);
Flux<Restaurant> result = restaurants
.buffer(10000)
.flatMap(rs -> reactiveMongoTemplate.insert(rs, Restaurant.class));
return ok().contentType(APPLICATION_JSON_UTF8).body(result, Restaurant.class);
}
public Mono<ServerResponse> delete(ServerRequest request) {
String id = request.pathVariable("id");
Mono<Void> result = restaurantRepository.deleteById(id);
return ok().contentType(APPLICATION_JSON_UTF8).build(result);
}
}
對比上篇的RestaurantController
,由於去除了路由資訊,RestaurantHandler
變得非常函式化,可以說就是一組相關的HandlerFunction
的集合,同時各個方法的可複用性也大為提升。這就回答了上一小節提出的疑問,即以犧牲可維護性為代價,換取更好的函式特性。
2.3 單元測試
@RunWith(SpringRunner.class)
@SpringBootTest
public class RestaurantHandlerTests extends BaseUnitTests {
@Autowired
private RouterFunction<ServerResponse> restaurantRouter;
@Override
protected WebTestClient prepareClient() {
WebTestClient webClient = WebTestClient.bindToRouterFunction(restaurantRouter)
.configureClient().baseUrl("http://localhost:9090").responseTimeout(Duration.ofMinutes(1)).build();
return webClient;
}
}
和針對Controller的單元測試相比,編寫Handler的單元測試的主要區別在於初始化WebTestClient
方式的不同,測試方法的主體可以完全複用。
3 小結
到此,有關響應式程式設計的介紹就暫且告一段落。回顧這四篇文章,我先是從響應式宣言說起,然後介紹了響應式程式設計的基本概念和關鍵特性,並且詳解了Spring 5中和響應式程式設計相關的新特性,最後以一個示例應用結尾。希望讀完這些文章,對你理解響應式程式設計能有所幫助。歡迎你到我的留言板分享,和大家一起過過招。