引子:被譽(yù)為“中國(guó)大數(shù)據(jù)第一人”的涂子沛先生在其成名作《數(shù)據(jù)之巔》里提到蹬昌,摩爾定律混驰、社交媒體、數(shù)據(jù)挖掘是大數(shù)據(jù)的三大成因。IBM的研究稱栖榨,整個(gè)人類文明所獲得的全部數(shù)據(jù)中昆汹,有90%是過(guò)去兩年內(nèi)產(chǎn)生的。在此背景下婴栽,包括NoSQL满粗,Hadoop, Spark, Storm, Kylin在內(nèi)的大批新技術(shù)應(yīng)運(yùn)而生。其中以RxJava和Reactor為代表的響應(yīng)式(Reactive)編程技術(shù)針對(duì)的就是經(jīng)典的大數(shù)據(jù)4V定義(Volume愚争,Variety映皆,Velocity,Value)中的Velocity轰枝,即高并發(fā)問(wèn)題捅彻,而在即將發(fā)布的Spring 5中,也引入了響應(yīng)式編程的支持狸膏。在接下來(lái)的幾周沟饥,我會(huì)圍繞響應(yīng)式編程分三期與你分享我的一些學(xué)習(xí)心得。本篇是第三篇(下)湾戳,通過(guò)一個(gè)簡(jiǎn)單的Spring 5示例應(yīng)用贤旷,探一探即將于下月底發(fā)布的Spring 5的究竟。
前情概要:
1 回顧
上篇介紹了如何使用Spring MVC注解實(shí)現(xiàn)一個(gè)響應(yīng)式Web應(yīng)用(以下簡(jiǎn)稱RP應(yīng)用)砾脑,本篇接著介紹另一種實(shí)現(xiàn)方式——Router Functions幼驶。
2 實(shí)戰(zhàn)
2.1 Router Functions
Router Functions是Spring 5新引入的一套R(shí)eactive風(fēng)格(基于Flux和Mono)的函數(shù)式接口,主要包括RouterFunction
韧衣,HandlerFunction
和HandlerFilterFunction
盅藻,分別對(duì)應(yīng)Spring MVC中的@RequestMapping
,@Controller
和HandlerInterceptor
(或者Servlet規(guī)范中的Filter
)畅铭。
和Router Functions搭配使用的是兩個(gè)新的請(qǐng)求/響應(yīng)模型氏淑,ServerRequest
和ServerResponse
,這兩個(gè)模型同樣提供了Reactive風(fēng)格的接口硕噩。
2.2 示例代碼
下面接著看我GitHub上的示例工程里的例子假残。
2.2.1 自定義RouterFunction和HandlerFilterFunction
@Configuration
public class RestaurantServer implements CommandLineRunner {
@Autowired
private RestaurantHandler restaurantHandler;
/**
* 注冊(cè)自定義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)
// 注冊(cè)自定義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();
// 轉(zhuǎn)化為通用的Reactive HttpHandler
HttpHandler httpHandler = toHttpHandler(router);
// 適配成Netty Server所需的Handler
ReactorHttpHandlerAdapter httpAdapter = new ReactorHttpHandlerAdapter(httpHandler);
// 創(chuàng)建Netty Server
HttpServer server = HttpServer.create("localhost", 9090);
// 注冊(cè)Handler并啟動(dòng)Netty Server
server.newHandler(httpAdapter).block();
}
}
可以看到,使用Router Functions實(shí)現(xiàn)RP應(yīng)用時(shí)炉擅,你需要自己創(chuàng)建和管理容器辉懒,也就是說(shuō)Spring 5并沒(méi)有針對(duì)Router Functions提供IoC支持,這是Router Functions和Spring MVC相比最大的不同谍失。除此之外眶俩,你需要通過(guò)RouterFunction
的API(而不是注解)來(lái)配置路由表和過(guò)濾器。對(duì)于簡(jiǎn)單的應(yīng)用快鱼,這樣做問(wèn)題不大颠印,但對(duì)于上規(guī)模的應(yīng)用纲岭,就會(huì)導(dǎo)致兩個(gè)問(wèn)題:1)Router的定義越來(lái)越龐大;2)由于URI和Handler分開定義嗽仪,路由表的維護(hù)成本越來(lái)越高荒勇。那為什么Spring 5會(huì)選擇這種方式定義Router呢?接著往下看闻坚。
2.2.2 自定義HandlerFunction
@Component
public class RestaurantHandler {
/**
* 擴(kuò)展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);
}
}
對(duì)比上篇的RestaurantController
,由于去除了路由信息窿凤,RestaurantHandler
變得非常函數(shù)化仅偎,可以說(shuō)就是一組相關(guān)的HandlerFunction
的集合,同時(shí)各個(gè)方法的可復(fù)用性也大為提升雳殊。這就回答了上一小節(jié)提出的疑問(wèn)橘沥,即以犧牲可維護(hù)性為代價(jià),換取更好的函數(shù)特性夯秃。
2.3 單元測(cè)試
@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;
}
}
和針對(duì)Controller的單元測(cè)試相比座咆,編寫Handler的單元測(cè)試的主要區(qū)別在于初始化WebTestClient
方式的不同,測(cè)試方法的主體可以完全復(fù)用仓洼。
3 小結(jié)
到此介陶,有關(guān)響應(yīng)式編程的介紹就暫且告一段落∩ǎ回顧這四篇文章哺呜,我先是從響應(yīng)式宣言說(shuō)起,然后介紹了響應(yīng)式編程的基本概念和關(guān)鍵特性箕戳,并且詳解了Spring 5中和響應(yīng)式編程相關(guān)的新特性某残,最后以一個(gè)示例應(yīng)用結(jié)尾。希望讀完這些文章陵吸,對(duì)你理解響應(yīng)式編程能有所幫助玻墅。歡迎你到我的留言板分享,和大家一起過(guò)過(guò)招壮虫。