Spring WebFlux和Reactive程式設計

banq發表於2019-01-10

在看到Jurgen Hoeller引入新的Spring 5功能後,我終於開始嘗試在尚未釋出的Spring Boot 2.0.0 Snapshot中嘗試新的Spring WebFlux專案。開始吧:

Maven WebFlux專案生成
  • 轉到Spring啟動應用程式生成器
  • 在Spring Boot版本中選擇“2.0.0”以上版本
  • 在依賴項中搜尋“ Reactive Web ”
  • 儲存生成的maven專案

演示(反應端點)
在剛剛生成的Spring WebFlux專案中,讓我們構建一個REST端點,以Reactive方式獲取通用儲存Item 。首先讓我們開始:

@RestController
public class ItemsReactiveController {

    @Autowired
    private IItemsService iItemsService;

    public Flux<Item> findById(String id) {
        try {
            System.out.println("Getting the data from DB...");
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return Flux.just(iItemsService.findById(id));
    }

    @GetMapping(value = "/store/{id}", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public void getItemById(@PathVariable String id) {
        System.out.println("Controller start....");
        findById(id).subscribe(new Subscriber<Item>() {

            @Override
            public void onSubscribe(Subscription s) {
                s.request(Long.MAX_VALUE);
            }
            @Override
            public void onNext(Item t) {
                System.out.println("Retrieved: "+t.getName());
            }
            @Override
            public void onError(Throwable t) {
            }
            @Override
            public void onComplete() {
            }
        });
        System.out.println("End of method.");
    }
}

程式碼詳細
方法findById返回Flux <Item>型別。Flux是一種以反應方式返回0..N項的資料型別。另外一種可能性是使用返回0或1項的Mono<Item>型別。
Jurgen Hoeller清楚地提到如果端點返回上述資料型別之一,那麼實際上沒有返回結果,但是Spring給呼叫者一個管道,其中結果將最終落地。正如您從NodeJS所知,但是在Spring方式中,引擎蓋下的機制非常接近EventLoop。要從Reactive端點獲取任何資料,您需要訂閱返回的管道。如果您想獲得結果的回撥,那麼管道上的訂閱階段或錯誤就會被使用來自反應流的訂閱者,這是Project reactor的底層實現。

第一次測試:
專案原始碼:https://bitbucket.org/tomask79/spring-reactive-rest.git
讓我們用Oracle Store中現有Item的{id}呼叫先前建立的端點(我不會厭煩使用的JPA配置,它不是演示的主題)。點選瀏覽器:http://localhost:8081/store/{id}

系統輸出:

Controller start....
Getting the data from DB...
<p class="indent">[EL Fine]: sql: 2017-03-20 14:19:56.321--ServerSession(26882836)--Connection(29914401)--SELECT ID, ITEM_NAME FROM ITEMS WHERE (ID = ?)
        bind => [1 parameter bound]
Retrieved: <Name of the Item>
End of method.


如您所見,程式碼輸出每個步驟:
  • 呼叫控制器(Controller start ....)
  • 獲取item呼叫服務(Retrieved: <Name of the Item>)
  • 達到了方法的結束。(End of method)

預設情況下,從主執行緒獲取訂閱資料,當然因為我使用的資料儲存不提供反應式驅動程式(Oracle),因此呼叫儲存是阻塞的。目前,支援反應式程式設計(解鎖通話)的儲存是:
  • Mongo
  • Redis
  • Cassandra
  • Postgres

當然,啟用檢查新專案Spring Data“Kay”,才能在上面提到的Spring Data專案中啟用反應範例。

要實際啟用非同步釋出我們的專案,我們需要將控制器更改為:

package com.example.controller;

import com.example.domain.Item;
import com.example.service.IItemsService;
import org.reactivestreams.Subscriber;
import org.reactivestreams.Subscription;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;
import reactor.core.scheduler.Scheduler;
import reactor.core.scheduler.Schedulers;

/**
 * Created by Tomas.Kloucek on 17.3.2017.
 */
@RestController
public class ItemsReactiveController {

    @Autowired
    private IItemsService iItemsService;

    public Flux<Item> findById(String id) {
        try {
            System.out.println("Getting the data from DB...");
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return Flux.just(iItemsService.findById(id)).publishOn(Schedulers.parallel());
    }

    @GetMapping(value = "/store/{id}", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public void getItemById(@PathVariable String id) {
        System.out.println("Controller start....");
        findById(id).subscribe(v -> {
           System.out.println("Consumed: "+v.getId());
        });
        System.out.println("End of method.");
    }
}

  • 改變了訂閱只獲得結果。如果你知道RxJava你應該熟悉。
  • 新增了publishOn方法,並排程了用於非同步釋出Item的執行緒。

現在,如果我們從瀏覽器再次點選端點,輸出將是:

Controller start....
Getting the data from DB...
<p class="indent">[EL Fine]: sql: 2017-03-20 14:16:49.69--ServerSession(18245293)--Connection(9048111)--SELECT ID, ITEM_NAME FROM ITEMS WHERE (ID = ?)
        bind => [1 parameter bound]
End of method.
Consumed: <ID of your Item entity>


正如您所看到的,Spring在給出訂閱請求資料之前就已到達方法的最後(End of method)。

如何建立客戶端以呼叫Reactive Endpoint
讓我們用程式碼建立另一個Spring Boot WebReactive應用程式:

@SpringBootApplication
public class DemoApplication {

    @Bean
    public WebClient webClient() {
        return WebClient.create("http://<reactiveAppHost>:<reactiveAppPort>");
    }

    @Bean
    CommandLineRunner launch(WebClient webClient) {
        return args -> {
            webClient.get().Yuri("/store/{id}")
                    .accept(MediaType.TEXT_EVENT_STREAM)
                    .exchange()
                    .flatMap(cr -> cr.bodyToFlux(Item.class))
                    .subscribe(v -> {
                        System.out.println("Received from MS: "+v.getName());
                    });
        };
    }

    public static void main(String args[]) {
        new SpringApplicationBuilder(DemoApplication.class).properties
                (Collections.singletonMap("server.port", "8082"))
                .run(args);
    }
}


程式碼詳細:
要呼叫Reactive端點,您需要首先獲取WebClient例項。在演示案例中放入create方法http://localhost:8081.。由WebClient.exchange()方法呼叫執行的自呼叫方法,
但要實際在管道上放置訂閱以獲取結果,您需要呼叫ClientRequest.bodyToFlux(<ResultClassMapping> .class),這種訂閱才是可能的。如果我們執行這個應用程式,那麼結果應該是:

Started DemoApplication in 4.631 seconds (JVM running for 4.954)
Received from MS: <Item text>


這部分客戶端程式碼:
git clone https://tomask79@bitbucket.org/tomask79/spring-reactive-rest-client.git

用於非同步呼叫REST端點的API是否必要?
我對這種訂閱和非同步呼叫端點的新反應趨勢的觀點是一種悲觀。如果我需要非同步呼叫端點,那麼JMS或AMPQ是第一個讓我進入大腦的想法,特別是在MicroServices中。但我們會看到這將如何發展。Spring Framework 5中的其他計劃更改很有希望:

  • 帶有Angular的函式框架,類似Router
  • 支援Project Jigsaw
  • 註冊beanLamdas。 

​​​​​​​本站文章點選標題看原文!

相關文章