➜ Old React website
Chung Cheuk Hang MichaelJava Web Developer
批量下載檔案Docker 基本操作

Spring Webflux(一)

Table of contents

1 Spring Webflux

Reactive programming 可以畀我地靈活啲咁使用系統資源,
Why was Spring WebFlux created?
Part of the answer is the need for a non-blocking web stack to handle concurrency with a small number of threads and scale with fewer hardware resources.
The other part of the answer is functional programming.

1.1 應用場景(vs 傳統 Spring MVC)

到底我地應唔應該由傳統 Spring MVC 一下子轉曬做 reactive 既 Spring Webflux?
Spring MVC or WebFlux?
  • If you have a Spring MVC application that works fine, there is no need to change. Imperative programming is the easiest way to write, understand, and debug code. You have maximum choice of libraries, since, historically, most are blocking.
  • In a microservice architecture, you can have a mix of applications with either Spring MVC or Spring WebFlux controllers or with Spring WebFlux functional endpoints. Having support for the same annotation-based programming model in both frameworks makes it easier to re-use knowledge while also selecting the right tool for the right job.
  • A simple way to evaluate an application is to check its dependencies. If you have blocking persistence APIs (JPA, JDBC) or networking APIs to use, Spring MVC is the best choice for common architectures at least. It is technically feasible with both Reactor and RxJava to perform blocking calls on a separate thread but you would not be making the most of a non-blocking web stack.
  • If you have a Spring MVC application with calls to remote services, try the reactive WebClient. You can return reactive types (Reactor, RxJava, or other) directly from Spring MVC controller methods. The greater the latency per call or the interdependency among calls, the more dramatic the benefits. Spring MVC controllers can call other reactive components too.
  • If you have a large team, keep in mind the steep learning curve in the shift to non-blocking, functional, and declarative programming. A practical way to start without a full switch is to use the reactive WebClient. Beyond that, start small and measure the benefits. We expect that, for a wide range of applications, the shift is unnecessary.

2 動手寫

2.1 Maven dependencies

1<dependencyManagement> 2 <dependencies> 3 <dependency> 4 <groupId>org.springframework.boot</groupId> 5 <artifactId>spring-boot-dependencies</artifactId> 6 <version>2.7.1</version> 7 <type>pom</type> 8 <scope>import</scope> 9 </dependency> 10 </dependencies> 11</dependencyManagement> 12 13<dependencies> 14 <dependency> 15 <groupId>org.springframework.boot</groupId> 16 <artifactId>spring-boot-starter-webflux</artifactId> 17 </dependency> 18 19 <dependency> 20 <groupId>org.projectlombok</groupId> 21 <artifactId>lombok</artifactId> 22 <scope>provided</scope> 23 </dependency> 24</dependencies>

2.2 寫 Java code

Project structure:
  • src/main/java
    • /code
      • /config
        • ApiConfig.java
      • /controller
        • OrderController.java
        • OrderControllerRx.java
      • /dao
        • OrderDao.java
      • /model
        • Order.java
      • MainApplication.java
OrderController.java
1@RestController 2@RequestMapping(path = "orders", produces = MediaType.APPLICATION_JSON_VALUE) 3public class OrderController { 4 5 @Autowired OrderDao orderDao; 6 7 @GetMapping 8 public List<Order> getAllOrders() { 9 return orderDao.getAllOrders(); 10 } 11}
OrderControllerRx.java
1@RestController 2@RequestMapping(path = "rx/orders", produces = MediaType.TEXT_EVENT_STREAM_VALUE) 3public class OrderControllerRx { 4 5 @Autowired OrderDao orderDao; 6 @Autowired WebClient orderApi; 7 8 @GetMapping 9 public Flux<Order> getAllOrdersRx() { 10 return orderDao.getAllOrdersRx(); 11 } 12 13 @GetMapping("v2") 14 public Flux<Order> getAllOrdersRxFromSelf() { 15 return orderApi.get().uri("/rx/orders").exchangeToFlux(res -> { 16 if (res.statusCode()==HttpStatus.OK) { 17 return res.bodyToFlux(Order.class); 18 } else { 19 return res.createException().flatMapMany(Mono::error); 20 } 21 }); 22 } 23}
OrderDao.java
1@Repository 2public class OrderDao { 3 4 public List<Order> getAllOrders() { 5 return range(0, 10).mapToObj(e -> new Order().setId(UUID.randomUUID().toString()) 6 .setDate(LocalDateTime.now()) 7 .setTotal(new BigDecimal(new Random().nextInt(10000)+""))) 8 .peek(e -> { 9 try { 10 Thread.sleep(1000); 11 } catch (InterruptedException ex) { 12 } 13 }) 14 .collect(toList()); 15 } 16 17 public Flux<Order> getAllOrdersRx() { 18 return Flux.range(0, 10) 19 .delayElements(Duration.ofSeconds(1)) 20 .doOnNext(e -> { 21 System.out.println(e); 22 }) 23 .map(e -> new Order().setId(UUID.randomUUID().toString()) 24 .setDate(LocalDateTime.now()) 25 .setTotal(new BigDecimal(new Random().nextInt(10000)+""))); 26 } 27}
ApiConfig
1@Configuration 2public class ApiConfig { 3 4 @Bean 5 public WebClient orderApi() { 6 return WebClient.create("http://localhost:8080"); 7 } 8}
Order.java
1@Data 2@Accessors(chain = true) 3@FieldDefaults(level = PRIVATE) 4public class Order { 5 String id; 6 LocalDateTime date; 7 BigDecimal total; 8}

3 測試

3.1 Postman

現時 Postman 唔支援 streaming events 既 API。

3.2 cURL

傳統 Spring MVC,會 block:
curl -v http://localhost:8080/orders
Reactive 既 Spring Webflux,唔會 block,逐啲逐啲咁返回:
curl -v http://localhost:8080/rx/orders
curl -v http://localhost:8080/rx/orders/v2

3.3 瀏覽器

3.3.1 直接打開

傳統 Spring MVC API(10 秒後一口氣返回):
Reactive 既 Spring Webflux APIs(每過 1 秒就返回 stream event data,total 10 次):

3.3.2 JavaScript Fetch API

3.3.2.1 傳統 Spring MVC API

執行下面既 JavaScript(可以用 NodeJS CLI 執行):
1async function printOrders() { 2 const response = await fetch("http://localhost:8080/orders"); 3 const data = await response.json(); 4 console.log(data); 5} 6 7printOrders();

3.3.2.2 Reactive 既 Spring Webflux APIs

API 用其中一個都可以:
執行下面既 JavaScript(可以用 NodeJS CLI 執行):
1async function printOrders() { 2 const response = await fetch("http://localhost:8080/rx/orders/v2"); 3 const stream = response.body.pipeThrough(new TextDecoderStream()); 4 const reader = stream.getReader(); 5 6 while (true) { 7 const { value, done } = await reader.read(); 8 console.log(done); 9 console.log(value); 10 11 if (done) { 12 break; 13 } 14 } 15} 16 17printOrders();
如果想不停咁 stream:
1// 用 NodeJS 既話要 npm install eventsource,再加下面果句: 2// global.EventSource = require("eventsource"); 3 4const eventSource = new EventSource("http://localhost:8080/rx/orders/v2"); 5 6eventSource.onopen = () => { 7 console.log("Open"); 8}; 9 10eventSource.onmessage = ({ data }) => { 11 console.log(data); 12};
註:
  • EventSource 既話,就會見到佢每過 1 秒就會收到 event data,而 10 次之後佢會再 Open,然後又會重複再收多 10 次,無限 loop。