【Spring 5】響應式Web框架實戰(下)

Emac發表於2017-07-18

引子:被譽為“中國大資料第一人”的塗子沛先生在其成名作《資料之巔》裡提到,摩爾定律、社交媒體、資料探勘是大資料的三大成因。IBM的研究稱,整個人類文明所獲得的全部資料中,有90%是過去兩年內產生的。在此背景下,包括NoSQL,Hadoop, Spark, Storm, Kylin在內的大批新技術應運而生。其中以RxJavaReactor為代表的響應式(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)的函式式介面,主要包括RouterFunctionHandlerFunctionHandlerFilterFunction,分別對應Spring MVC中的@RequestMapping@ControllerHandlerInterceptor(或者Servlet規範中的Filter)。

和Router Functions搭配使用的是兩個新的請求/響應模型,ServerRequestServerResponse,這兩個模型同樣提供了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,主要有兩點區別:

  • 所有方法的引數和返回值型別固定為ServerRequest和Mono以符合HandlerFunction的定義,所有請求相關的物件(queryParam, pathVariable,header, session等)都通過ServerRequest獲取。
  • 由於去除了路由資訊,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中和響應式程式設計相關的新特性,最後以一個示例應用結尾。希望讀完這些文章,對你理解響應式程式設計能有所幫助。歡迎你到我的留言板分享,和大家一起過過招。

4 參考

相關文章