1. 程式人生 > >Spring Boot 實踐折騰記(11):使用 Spring 5的WebFlux快速構建效響應式REST API

Spring Boot 實踐折騰記(11):使用 Spring 5的WebFlux快速構建效響應式REST API

關於Spring 5中的反應式程式設計支援Reactor類庫,上一篇文章
Spring Boot 實踐折騰記(10):2.0+版本中的反應式程式設計支援——Reactor》
已經簡要介紹過,Spring 5 框架所包含的內容很多,本文還會繼續介紹其中新增的 WebFlux 模組。開發人員可以使用 WebFlux 建立高效能的 Web 應用和客戶端。然後,我們再結合Spring Boot 2中對Spring 5的支援,來快速構建一個響應式的Web API。

WebFlux 簡介

WebFlux 模組的名稱是 spring-webflux,基礎類庫其實還是來源於 Reactor 中對Reactive Streams規範的實現。該模組中包含了對反應式 HTTP、伺服器推送事件和 WebSocket 的客戶端和伺服器端的支援。

WebFlux 需要底層提供執行時的支援,WebFlux 可以執行在支援 Servlet 3.1 非阻塞 IO API 的 Servlet 容器上,或是其他非同步執行時環境,如 Netty 和 Undertow。Spring Boot 2預設選擇的是Netty作為非阻塞 IO API 的 Servlet 容器。

在伺服器端,WebFlux支援兩種不同的程式設計模型:

  1. Spring MVC 中使用的基於 Java 註解的方式;
  2. 基於 Java 8 的 lambda 表示式的函數語言程式設計模型。

這兩種程式設計模型只是在程式碼編寫方式上存在不同。它們執行在同樣的反應式底層架構之上,因此在執行時是相同的,層級關係如下圖所示:

這裡寫圖片描述
要注意的是,適用於反應式的HTTP請求響應——ServerHttpRequest和ServerHttpResponse,會將請求和響應的正文轉換為Flux ,而不是InputStream和OutputStream。而REST風格的JSON和XML,在序列化和反序列化時也會將正文轉換為為Flux ,同理,HTML的檢視渲染和伺服器傳送事件也會進行相應轉換。

實戰

下面我們通過幾個簡單的例子來上手實戰一下響應式的API開發,Spring Boot 2已經集成了反應式的支援,我們只需要使用spring-boot-starter-webflux的啟動器POM,它提供了預設為Netty支援的Spring WebFlux。 有關詳細資訊,還可以檢視Spring Boot參考文件。

Java 註解程式設計模型

基於註解的程式設計正規化,對於Java開發人員來說是非常熟悉和簡單的,而在反應式程式設計裡,同樣很快就能上手,我們來看一個簡單的Hello World例子。

StartAppFlux程式碼:

@SpringBootApplication
@RestController
public class StartAppFlux {

    public static void main(String[] args) {
        SpringApplication.run(StartAppFlux.class,args);
    }

    @GetMapping("/hello_world_mono")
    public Mono<String> helloWorld() {
        return Mono.just("Hello World with flux's mono");
    }

    @GetMapping("/hello_world_flux")
    public Flux<String> helloWorldFlux() {
        return Flux.just("Hello", "World");
    }
}

REST API

簡單的例子只是剛開始,實際應用中,開發API可能才是更重要的工作之一。我們來實戰一個簡單的例子。該 REST API 用來對使用者資料進行基本的 CRUD 操作。POJO物件Man有四個基本屬性id、name、age、phone。用於操作物件的服務QueryUserServices,使用一個Map來模擬資料庫的讀寫操作。

Man物件程式碼:

public class Man {

    private String id;
    private String name;
    private int age;
    private String phone;

    public Man(String id, String name, int age, String phone) {
        this.id = id;
        this.name = name;
        this.age = age;
        this.phone = phone;
    }
//省略get、set
}

QueryUserServices程式碼 :

@Service
public class QueryUserServices {

    private final Map<String, Man> data = new ConcurrentHashMap<>();

    @PostConstruct
    void init(){
        Man man1 = new Man("1","mickjoust",66,"21313123132");
        Man man2 = new Man("2","mia",66,"21313123132");
        Man man3 = new Man("3","max",66,"21313123132");

        data.put(man1.getId(),man1);
        data.put(man2.getId(),man2);
        data.put(man3.getId(),man3);
    }

    Flux<Man> list() {
        return Flux.fromIterable(this.data.values());
    }

    Flux<Man> getById(final Flux<String> ids) {
        return ids.flatMap(id -> Mono.justOrEmpty(this.data.get(id)));
    }

    Mono<Man> getById(final String id) {
        return Mono.justOrEmpty(this.data.get(id))
                .switchIfEmpty(Mono.error(new RuntimeException()));
    }

    Flux<Man> createOrUpdate(final Flux<Man> mans) {
        return mans.doOnNext(man -> this.data.put(man.getId(), man));
    }

    Mono<Man> createOrUpdate(final Man man) {
        this.data.put(man.getId(), man);
        return Mono.just(man);
    }

    Mono<Man> delete(final String id) {
        return Mono.justOrEmpty(this.data.remove(id));
    }
}

WebFluxController程式碼:

@RestController
@RequestMapping("/")
@SpringBootApplication
public class WebFluxController {

    @Autowired
    private QueryUserServices queryUserServices;

    public static void main(String[] args) {
        SpringApplication.run(WebFluxController.class,args);
    }

    @GetMapping("/get_flux")
    public Flux<Man> getFlux(){
        return queryUserServices.list();
    }

    @GetMapping("/get_flux_ids/{ids}")
    public Flux<Man> getFluxById(@PathVariable("ids") final String idStr){
        //逗號隔開,模擬,實際可以直接傳post請求
        String[] testStr = idStr.split(",");
        Flux<String> ids = Flux.fromArray(testStr);
        return queryUserServices.getById(ids);
    }

    @GetMapping("/get_mono/{id}")
    public Mono<Man> getMono(@PathVariable("id") final String id){
        return queryUserServices.getById(id);
    }

    @PostMapping("/create_flux")
    public Flux<Man> createFlux(@RequestBody final Flux<Man>  mans){
        return queryUserServices.createOrUpdate(mans);
    }

    @PostMapping("/update_mono/{id}")
    public Mono<Man> updateMono(@PathVariable("id") final String id, @RequestBody final Man man){
        Objects.requireNonNull(man);
        man.setId(id);
        return queryUserServices.createOrUpdate(man);
    }

    @GetMapping("/delete_mono/{id}")
    public Mono<Man> delete(@PathVariable("id") final String id){
        return queryUserServices.delete(id);
    }

}

函數語言程式設計模型

在上節中介紹了基於 Java 註解的程式設計模型,WebFlux 還支援另一種基於 lambda 表示式的函數語言程式設計模型。與基於 Java 註解的程式設計模型相比,函數語言程式設計模型的抽象層次更低,程式碼編寫更靈活,可以滿足一些對動態性要求更高的場景。不過在編寫時的程式碼複雜度也較高,學習曲線也較陡。開發人員可以根據實際的需要來選擇合適的程式設計模型。目前 Spring Boot 不支援在一個應用中同時使用兩種不同的程式設計模式。

客戶端

除了伺服器端實現之外,WebFlux 也提供了反應式客戶端,可以訪問 HTTP、SSE 和 WebSocket 伺服器端。

HTTP

對於 HTTP 和 SSE,可以使用 WebFlux 模組中的類 org.springframework.web.reactive.function.client.WebClient。如下程式碼中的 RESTClient 用來訪問前面小節中建立的 REST API。首先使用 WebClient.create 方法來建立一個新的 WebClient 物件,然後使用方法 post 來建立一個 POST 請求,並使用方法 body 來設定 POST 請求的內容。方法 exchange 的作用是傳送請求並得到以 Flux表示的 HTTP 響應。最後對得到的響應進行處理並輸出結果。response 的 bodyToMono 方法把響應內容轉換成類 Man的物件,最終得到的結果是 Flux物件。呼叫 createdUser.blockFirst() 方法的作用是等待請求完成並得到所產生的類 Man 的物件。

RESTClient程式碼:

public class RESTClient {
    public static void main(final String[] args) {
        Man man = new Man();
        man.setId("11");
        man.setAge(33);
        man.setName("Test");
        man.setPhone("1232312313");
        WebClient client = WebClient.create("http://localhost:8080/create_flux");
        Flux<Man> createdUser = client.post()
                .uri("")
                .accept(MediaType.APPLICATION_JSON)
                .body(Mono.just(man), Man.class)
                .exchange()
                .flatMapMany(response -> response.bodyToFlux(Man.class));
        System.out.println(createdUser.blockFirst());
        //會報錯Exception in thread "main" java.lang.NoClassDefFoundError: reactor/core/CoreSubscriber
    }
}

遺留問題

本來想加上函數語言程式設計,但感覺資訊量很大,後續單獨加一篇文章來進行實戰,同時,在客服端一節中的測試程式碼WebClient,會報異常,目前暫時未解決,導致了測試本身執行有點問題,後續文章解決後再更新。

小結

本文對 WebFlux 模組進行了簡要介紹,主要是其中的 HTTP 支援。對於Sprng 5新增的WebFlux模組,基於Java註解模型的程式設計正規化,是對反應式模型的一種實現,對於需要進行反應式的開發場景有很好的支援。

參考資源

我的其它穿越門——持續踐行,我們一路同行。
頭條號:說言風語
簡書ID:mickjoust
公號:說言風語