実際に動かして学ぶSpring WebFlux
Keywords
- Spring Boot
- リアクティブ
- Spring WebFlux
Contents
- 1. Spring WebFluxとは
- 2. SpringBootのコード
- 3. 他のサーバのコード
- 4. 結果
- 4-1. ブロッキングコードの場合(RestTemplate)
- 4-2. ノンブロッキングコードの場合(WebClient)
Spring WebFluxとは
Spring WebFluxとは、Threadに待ちを発生させず(ノンブロッキング)、少量のThreadを最大限に活かして、Webアプリケーションを作成するための技術です。
本稿では、WebFlux(+WebClient)を使って、Threadに待ちが発生しない処理の様子を実際に見ていきます。
ブラウザからWebFluxのSpring BootのWebサーバに対して、アクセスするとそこからWebClientを使用して、さらに他のWebサーバ(このサーバはWebFluxでもなんでもありません)にアクセスしてみます。
あるThreadが一度HTTPリクエストしてから、HTTPレスポンスを受け取るのを待たずに、更にもう一度HTTPリクエストをし、その後、一回目と二回目のHTTPレスポンスを受ける、というコードを見ていきたいと思います。
本稿で使用するコードは下記にあります。
SpringBootのコード
このサーバはlocalhost:8080で動いています。
package com.example.webfluxsample;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;
@RestController
public class SampleController {
@GetMapping("/nonblocking")
public Mono<String> nonblocking(){
WebClient webClient = WebClient
.builder()
.baseUrl("http://localhost:8000")
.build();
printWithThread(1);
webClient
.get()
.retrieve()
.bodyToMono(String.class)
.subscribe();
printWithThread(2);
webClient
.get()
.retrieve()
.bodyToMono(String.class)
.subscribe();
printWithThread(3);
return Mono.just("Hello, World");
}
@GetMapping("/blocking")
public Mono<String> blocking(){
RestTemplate restTemplate = new RestTemplate();
printWithThread(1);
restTemplate.getForObject("http://localhost:8000", String.class);
printWithThread(2);
restTemplate.getForObject("http://localhost:8000", String.class);
printWithThread(3);
return Mono.just("Hello, World");
}
// print with thread name and time.
public static void printWithThread(Object obj) {
System.out.println(System.currentTimeMillis() + ": " + Thread.currentThread().getName() + "\t" + obj);
}
}
上記のコードは途中でsubscribe()を呼んでいますが、飽くまでprintWithThread()を呼び順序を分かりやすくしたかっただけであり、いざWebFluxを使う場合は、このようにsubscribeすることはあまりないでしょう。
他のサーバのコード
リクエストが来たら、5秒待ちその後時間を表示しています。リクエストボディに特に意味はありません。このサーバはlocalhost:8000で動いています。
(ns app.web.controller.image)
(defn getList []
(do
(Thread/sleep 5000)
(println (System/currentTimeMillis))
{:status 200
:headers {"Content-Type" "application/json"}
:body
'(
{:category "FOOD", :name "ramen", :price 960},
{:category "DRINK", :name "beer", :price 350},
{:category "FOOD", :name "potato", :price 300},
{:category "FOOD", :name "rice & curry", :price 800},
{:category "DRINK", :name "water", :price 100},
{:category "FOOD", :name "tomato", :price 70},
{:category "DRINK", :name "soda", :price 120},
{:category "FOOD", :name "pasta", :price 900},
{:category "DRINK", :name "orange juice", :price 150150},
{:category "FOOD", :name "rice", :price 100},
{:category "DRINK", :name "tea", :price 300},
{:category "FOOD", :name "meat", :price 2000},
{:category "FOOD", :name "sushi", :price 25000},
{:category "FOOD", :name "fish", :price 5060},
{:category "DRINK", :name "coffee", :price 550},
{:category "DRINK", :name "sake", :price 1350},
)
}
)
)
結果
ブロッキングコードの場合(RestTemplate)
RestTemplateを使った場合は、下記のようになりました。SpringBootの標準出力と他のサーバの標準出力を一つに合わせています。
1611751069560: reactor-http-nio-3 1
1611751074564←WebClientのリクエスト先のログ
1611751074566: reactor-http-nio-3 2
1611751079572←WebClientのリクエスト先のログ
1611751079573: reactor-http-nio-3 3
- localhost:8080: [reactor-http-nio-3 1]のログ出力し、1回目のリクエスト
- localhost::8000: 1回目のリクエストを受け、5秒待ってログ出力し、レスポンス
- localhost:8080: [reactor-http-nio-3 2]のログ出力し、2回目のリクエスト
- localhost::8000: 2回目のリクエストを受け、5秒待ってログ出力し、レスポンス
- localhost:8080: [reactor-http-nio-3 3]のログ出力
1回目のリクエストが終わった後、localhost:8000からのレスポンスを5秒待っており、その間は何もしていません。
図で表すとこのようになります。ドットの線はThreadがHTTPレスポンスを待っており、何もしていないことを表しています。
さらに、下記のようにブラウザを2つ並べて、ほぼ同時にリクエストをした場合、1回目のリクエストの戻りが10秒かかり、さらに2回目のリクエストで10秒かかるので、合計20秒かかりました。これは、リクエストを待ち受けるThreadがreactor-http-nio-3の1つしかなく、また、ブロッキング処理であったためです。
ノンブロッキングコードの場合(WebClient)
WebClientを使った場合は、下記のとおりになりました。最初に1回目のリクエストを待たずに、2回目のリクエストを送信しています。また、リクエスト先のサーバのログとしては、同じ時刻(もう少し時間の精度を高めれば、別タイミングのはずだが)にリクエストが来ていることが確認できます。
1611751208260: reactor-http-nio-3 1
1611751208261: reactor-http-nio-3 2
1611751208262: reactor-http-nio-3 3
1611751213263←WebClientのリクエスト先のログ
1611751213263←WebClientのリクエスト先のログ
- localhost:8080: [reactor-http-nio-3 1]のログ出力し、1回目のリクエスト
- localhost:8080: [reactor-http-nio-3 2]のログ出力し、2回目のリクエスト
- localhost:8080: [reactor-http-nio-3 3]のログ出力
- localhost::8000: 1回目のリクエストを受け、5秒待ってログ出力し、レスポンス
- localhost::8000: 2回目のリクエストを受け、5秒待ってログ出力し、レスポンス
1回目のリクエストが終わった後、localhost:8000からのレスポンスを待たず、すぐさま2回目のリクエストを送信していることが伺えます。
図で表すとこのようになります。
さらに、下記のようにブラウザを2つ並べて、ほぼ同時にリクエストをした場合、それぞれのレスポンスは一瞬で返ってきました。このことからも、reactor-http-nio-3がノンブロッキングで動作していたことが実感できます。
以上で、ノンブロッキングコードの場合ではThreadに待ちを発生させることがなく、効率的にThreadを使用できていることがわかります。