- 【Spring 5】响应式Web框架前瞻
- 响应式编程总览
- 【Spring 5】响应式Web框架实战(上)
上篇介绍了如何使用Spring MVC注解实现一个响应式Web应用(以下简称RP应用),本篇接着介绍另一种实现方式——Router Functions。
Router Functions是Spring 5新引入的一套Reactive风格(基于Flux和Mono)的函数式接口,主要包括RouterFunction
,HandlerFunction
和HandlerFilterFunction
,分别对应Spring MVC中的@RequestMapping
,@Controller
和HandlerInterceptor
(或者Servlet规范中的Filter
)。
和Router Functions搭配使用的是两个新的请求/响应模型,ServerRequest
和ServerResponse
,这两个模型同样提供了Reactive风格的接口。
下面接着看我GitHub上的示例工程里的例子。
@Configuration
public class RestaurantServer implements CommandLineRunner {
@Autowired
private RestaurantHandler restaurantHandler;
/**
* 注册自定义RouterFunction
*/
@Bean
public RouterFunction restaurantRouter() {
RouterFunction 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 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呢?接着往下看。
@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 findAll(ServerRequest request) {
Flux result = restaurantRepository.findAll();
return ok().contentType(APPLICATION_JSON_UTF8).body(result, Restaurant.class);
}
public Mono findAllDelay(ServerRequest request) {
Flux result = restaurantRepository.findAll().delayElements(Duration.ofSeconds(1));
return ok().contentType(APPLICATION_JSON_UTF8).body(result, Restaurant.class);
}
public Mono get(ServerRequest request) {
String id = request.pathVariable("id");
Mono result = restaurantRepository.findById(id);
return ok().contentType(APPLICATION_JSON_UTF8).body(result, Restaurant.class);
}
public Mono create(ServerRequest request) {
Flux restaurants = request.bodyToFlux(Restaurant.class);
Flux result = restaurants
.buffer(10000)
.flatMap(rs -> reactiveMongoTemplate.insert(rs, Restaurant.class));
return ok().contentType(APPLICATION_JSON_UTF8).body(result, Restaurant.class);
}
public Mono delete(ServerRequest request) {
String id = request.pathVariable("id");
Mono result = restaurantRepository.deleteById(id);
return ok().contentType(APPLICATION_JSON_UTF8).build(result);
}
}
对比上篇的RestaurantController
,由于去除了路由信息,RestaurantHandler
变得非常函数化,可以说就是一组相关的HandlerFunction
的集合,同时各个方法的可复用性也大为提升。这就回答了上一小节提出的疑问,即以牺牲可维护性为代价,换取更好的函数特性。
@RunWith(SpringRunner.class)
@SpringBootTest
public class RestaurantHandlerTests extends BaseUnitTests {
@Autowired
private RouterFunction 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
方式的不同,测试方法的主体可以完全复用。
到此,有关响应式编程的介绍就暂且告一段落。回顾这四篇文章,我先是从响应式宣言说起,然后介绍了响应式编程的基本概念和关键特性,并且详解了Spring 5中和响应式编程相关的新特性,最后以一个示例应用结尾。希望读完这些文章,对你理解响应式编程能有所帮助。欢迎你到我的留言板分享,和大家一起过过招。