実際に動かして学ぶ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
  1. localhost:8080: [reactor-http-nio-3 1]のログ出力し、1回目のリクエスト
  2. localhost::8000: 1回目のリクエストを受け、5秒待ってログ出力し、レスポンス
  3. localhost:8080: [reactor-http-nio-3 2]のログ出力し、2回目のリクエスト
  4. localhost::8000: 2回目のリクエストを受け、5秒待ってログ出力し、レスポンス
  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のリクエスト先のログ
  1. localhost:8080: [reactor-http-nio-3 1]のログ出力し、1回目のリクエスト
  2. localhost:8080: [reactor-http-nio-3 2]のログ出力し、2回目のリクエスト
  3. localhost:8080: [reactor-http-nio-3 3]のログ出力
  4. localhost::8000: 1回目のリクエストを受け、5秒待ってログ出力し、レスポンス
  5. localhost::8000: 2回目のリクエストを受け、5秒待ってログ出力し、レスポンス

1回目のリクエストが終わった後、localhost:8000からのレスポンスを待たず、すぐさま2回目のリクエストを送信していることが伺えます。

図で表すとこのようになります。

さらに、下記のようにブラウザを2つ並べて、ほぼ同時にリクエストをした場合、それぞれのレスポンスは一瞬で返ってきました。このことからも、reactor-http-nio-3がノンブロッキングで動作していたことが実感できます。

以上で、ノンブロッキングコードの場合ではThreadに待ちを発生させることがなく、効率的にThreadを使用できていることがわかります。