Upgrade to Pro — share decks privately, control downloads, hide ads and more …

業務で使いたいWebFluxによるReactiveプログラミング / Introduction to Reactive Programming using Spring WebFlux

業務で使いたいWebFluxによるReactiveプログラミング / Introduction to Reactive Programming using Spring WebFlux

Spring Fest 2018
業務で使いたいWebFluxによるReactiveプログラミング
#jsug #sf_h6

Shin Tanimoto

October 31, 2018
Tweet

More Decks by Shin Tanimoto

Other Decks in Programming

Transcript

  1. 業務で使いたいWebFluxによる
    Reactiveプログラミング
    #sf_h6
    Acroquest Technology株式会社 / 日本Javaユーザーグループ
    谷本 心

    View full-size slide

  2. #sf_h6
    自己紹介
     谷本 心 (Shin Tanimoto)
     Twitter: @cero_t
     Acroquest Technology 株式会社
     システムアーキテクト、コンサルティング、トラブルシューティング
     著書「Java本格入門」
     コミュニティ活動
     日本Javaユーザーグループ / 関西Javaエンジニアの会
     Java Champion
     対戦格闘ゲーム / たこ焼き / BABYMETAL

    View full-size slide

  3. #sf_h6
    質問
     Java8のStream APIを使ったことがある人?(8割ぐらい)
     仕事で使ってる人?(6割ぐらい)
     Spring BootのRestTemplateを使ったことがある人?(7-8割ぐらい)
     仕事で使ってる人?(5割ぐらい)
     Spring Web FluxとWebClientを使ったことがある人?(1割未満)
     仕事で使ってる人?(ごく数名)

    View full-size slide

  4. #sf_h6
    今日のお話
     リアクティブプログラミングの基本的なパターンを把握する
     Stream APIの stream / map / collect に相当するもの
     ノンブロッキングなソースを「使う」ことを学ぶ
     「作る側」の話はあまりしません
     Stream APIのSpliteratorとか使って生成する人って多くないでしょ?
     Spring WebFluxを題材にして実装を学ぶ
     Project Reactorを使った実装
     RxJavaの人は適宜読み替えて
     Spring WebFluxやReactorを利用していない人が、利用の仕方や雰囲気を掴む、
    というのがこのセッションの主題

    View full-size slide

  5. #sf_h6
    1. リアクティブプログラミングとは

    View full-size slide

  6. #sf_h6
    リアクティブプログラミングとは?
     リアクティブプログラミングとは
     データの作成や変更の通知をきっかけにして処理をするようなプログラミング
     Pub-subモデルのイベントハンドラ、というイメージに近い
     例 : 税額 = 商品金額 * 税率
     商品金額が変われば、税額は変わる
     × : 商品金額のイベントハンドラが、税額を更新する
     〇 : 商品金額が変わった通知を受け、税額が自分を更新する
     概念の話は苦手! 定義をよく知りたい人は、ググると出てくる資料を読んで!
     このセッションで実装の話をたくさんするから、雰囲気をつかんで!

    View full-size slide

  7. #sf_h6
    リアクティブプログラミングまでの背景
     同期処理
     皆さんがよく書く、DBに書き込んで、読み込んで、更新して、外部システムを呼び
    出して、メールを送って・・・という処理を順番に行うもの
     実は「順番」でなくとも良いことが多い
     独立した2つのテーブルを更新するときに、どっちを先に更新しても良い
     一部の処理が遅いと、他の処理が待たされる
     全部ひとりで仕事をするワンオペのイメージ
     あるいは、他の人に仕事を任せるものの、それが終わるまでずっと待っている
     非効率このうえない

    View full-size slide

  8. #sf_h6
    リアクティブプログラミングまでの背景
     スレッドを利用した非同期処理
     「DBに書き込む」「DBを更新する」「外部システムを呼び出す」「メールを送る」
    などを1スレッド1処理に割り当てて全て同時に行ない、終わるまで待つ
     一部の遅い処理が終わるまで待つ間に、他の処理が進む
     複数のスレッドを利用してしまう
     「Webアプリケーションではスレッドを起こすべきではない」というプラクティス
    に違反する
     複数の部下に指示を出して結果を待つ、偉そうな上司のイメージ
     部下が疲弊する

    View full-size slide

  9. #sf_h6
    リアクティブプログラミングまでの背景
     リアクティブプログラミング
     「DBに書き込むよう依頼」「それが終わるのを待つ間にDBを更新するよう依頼」
    「それが終わるのを待つ間に外部システムを呼び出す依頼」「それが終わるのを待
    つ間にメールを送るよう依頼」を同時に進めて、結果が出たものから片付ける
     一部の遅い処理が終わるまで待つ間に、他の処理が進む
     少ないスレッドの利用だけで済む
     いくつかの作業を他者に委譲して、結果が出たら通知をもらうよう伝え、
    通知が来たものから順に片付ける人のイメージ

    View full-size slide

  10. #sf_h6
    リアクティブプログラミングとは?
     リアクティブプログラミングのキーポイント
     時間の掛かる部分や得意でない処理を外部に委譲する
     データストアへのアクセス
     外部サービスへのアクセス
     大量の計算
     待たない
     外部に委譲した処理の結果を待たず、次に進められる処理を進める
     外部から一部でも応答が返ってきたら、それを利用して次の処理を進める
     処理が終わった所までをアウトプットする

    View full-size slide

  11. #sf_h6
    リアクティブプログラミングとは?
     余談「リアクティブシステム」と「リアクティブプログラミング」
     リアクティブシステム
     message-driven(キューなどを介してメッセージドリブンで処理する)
     elsatic(負荷に応じてリソースを増減できる)
     resilient(一部がダウンしても継続する)
     responsive(素早く反応する)
     リアクティブプログラミングなど一切使わなくても作れる
     リアクティブプログラミング
     別にキューなどを使うためにあるわけではない
     このセッションでは「非同期/ノンブロッキングな処理を簡単に書ける仕組み」と定義
     性能やリソース消費を抑えるため

    View full-size slide

  12. #sf_h6
    今日紹介するReactorとSpring WebFlux
     Project Reactor
     リアクティブプログラミングを行うためのライブラリ
     Pivotal社が主に開発を主導
     Java 8対応(8以降が必須)
     Reactive Streams(標準仕様)に準拠
     Spring WebFlux
     Spring MVC 5から導入された
     ReactorをSpringのWeb(サーバ、クライアント)で使えるようにしたもの
     マイクロサービス間を非同期/ノンブロッキングに通信しやすくなるもの

    View full-size slide

  13. #sf_h6
    2. はじめてのReactor

    View full-size slide

  14. #sf_h6
    はじめてのReactor
     所感
     Java8のStream APIのようなAPIが凄い数あるイメージ
     https://projectreactor.io/docs/core/release/api/reactor/core/publisher/Flux.html
     非同期で動作するため、同期処理脳だと「書いた通りに動かない」ってなる
     基本の基本は「Mono」と「Flux」
     Mono
     単一の要素を扱う
     Optionalの非同期版
     Flux
     複数の要素を使う
     Streamの非同期版

    View full-size slide

  15. #sf_h6
    はじめてのReactor
    Mono mono = Mono.just("abc");
    mono.subscribe(s -> System.out.println(s));
    Flux flux = Flux.just("a", "b", "c", "d", "e");
    flux.subscribe(s -> System.out.println(s));
    abc
    a
    b
    c
    d
    e

    View full-size slide

  16. #sf_h6
    ふつうはこう書くよね
    Mono.just("abc")
    .subscribe(System.out::println);
    Flux.just("a", "b", "c", "d", "e")
    .subscribe(System.out::println);
    abc
    a
    b
    c
    d
    e

    View full-size slide

  17. #sf_h6
    最初に覚えるメソッド
     Mono /Fluxを作る
     Flux.just
     既に存在するデータからMono / Fluxを作る
     Flux.interval
     一定時間ごとにデータを作る(カウントアップする)
     Mono / Fluxを使う
     subscribe ( ≒ forEach )
     MonoやFluxのデータを受け取るたびに処理をする
     map ( ≒ map )
     MonoやFluxのデータを変換する

    View full-size slide

  18. #sf_h6
    一定時間おきにデータを作る
    Flux.interval(Duration.ofMillis(100))
    .map(i -> i + " " + LocalDateTime.now())
    .subscribe(System.out::println);
    なにも表示されずに終わる

    View full-size slide

  19. #sf_h6
    最初の次に覚えるメソッド
     Mono / Fluxを使う
     take
     指定した件数だけ受け取る
     doOnComplete
     受け取り終わった際に処理を行う
     Mono / Fluxと合わせて使う
     CountDownLatchクラス
     コンストラクタ
     いくつカウントダウンすれば良いかを指定する
     await
     コンストラクタで指定した回数だけカウントダウンされるまで待つ

    View full-size slide

  20. #sf_h6
    一定時間おきにデータを作る(正しく)
    var latch = new CountDownLatch(1);
    Flux.interval(Duration.ofMillis(100))
    .map(i -> i + " " + LocalDateTime.now())
    .take(10)
    .doOnComplete(latch::countDown)
    .subscribe(System.out::println);
    latch.await();

    View full-size slide

  21. #sf_h6
    一定時間おきにデータを作る(正しく)
    var latch = new CountDownLatch(1);
    Flux.interval(Duration.ofMillis(100))
    .map(i -> i + " " + LocalDateTime.now())
    .take(10)
    .doOnComplete(latch::countDown)
    .subscribe(System.out::println);
    latch.await();
    0 2018-11-01T13:55:45.969860800
    1 2018-11-01T13:55:46.051231
    2 2018-11-01T13:55:46.150832200
    3 2018-11-01T13:55:46.250867900

    7 2018-11-01T13:55:46.660427100
    8 2018-11-01T13:55:46.760725800
    9 2018-11-01T13:55:46.860524100
    10件だけ取得して
    completeする

    View full-size slide

  22. #sf_h6
    ログで状況を見る
    var latch = new CountDownLatch(1);
    Flux.interval(Duration.ofMillis(100))
    .map(i -> i + " " + LocalDateTime.now())
    .take(10)
    .doOnComplete(latch::countDown)
    .log()
    .subscribe(System.out::println);
    latch.await();

    View full-size slide

  23. #sf_h6
    ログで状況を見る
    var latch = new CountDownLatch(1);
    Flux.interval(Duration.ofMillis(100))
    .map(i -> i + " " + LocalDateTime.now())
    .take(10)
    .doOnComplete(latch::countDown)
    .log()
    .subscribe(System.out::println);
    latch.await();
    13:56:58.067 [main] INFO reactor.Flux.Peek.1 - onSubscribe(FluxPeek.PeekSubscriber)
    13:56:58.067 [main] INFO reactor.Flux.Peek.1 - request(unbounded)
    13:56:58.225 [parallel-1] INFO reactor.Flux.Peek.1 - onNext(0 2018-11-05T13:56:58.191329900)
    0 2018-11-05T13:56:58.191329900
    13:56:58.281 [parallel-1] INFO reactor.Flux.Peek.1 - onNext(1 2018-11-05T13:56:58.281027200)
    1 2018-11-05T13:56:58.281027200
    ログ出力する
    メソッド
    (副作用なし)
    スレッド名と
    処理内容をログに出力

    View full-size slide

  24. #sf_h6
    この章の振り返り
     Monoが単一のオブジェクト、Fluxが複数のオブジェクトに対応する
     MonoやFluxのメソッドは非同期で実行される
     作成のためのメソッド
     just / interval など
     利用のためのメソッド
     map / subscribe / take / doOnComplete など

    View full-size slide

  25. #sf_h6
    2. はじめてのSpring WebFlux

    View full-size slide

  26. #sf_h6
    はじめてのSpring WebFlux
     概要
     Spring MVC(spring-boot-starter-web)で戻り値にMono / Fluxが使えるようになる
     WebClientという新しいクライアントが使えるようになる
     TomcatではなくNettyで起動する
     基本はSpring MVCと変わらない
     @RestController
     @GetMapping / @PostMapping

    View full-size slide

  27. #sf_h6
    はじめてのSpring WebFlux
    @RestController
    public class ReactorDemo {
    @GetMapping
    public Flux demo() {
    return Flux.interval(Duration.ofMillis(300))
    .map(i -> i + " " + LocalDateTime.now())
    .take(10);
    }
    }

    View full-size slide

  28. #sf_h6
    はじめてのSpring WebFlux
    @RestController
    public class ReactorDemo {
    @GetMapping
    public Flux demo() {
    return Flux.interval(Duration.ofMillis(300))
    .map(i -> i + " " + LocalDateTime.now())
    .take(10);
    }
    }
    0 2018-11-01T14:09:52.0456159001 2018-11-01T14:09:52.3458706002 2018-11-
    01T14:09:52.6454726003 2018-11-01T14:09:52.9452633004 2018-11-01T14:09:53.2467024005
    2018-11-01T14:09:53.5471006006 2018-11-01T14:09:53.8456003007 2018-11-
    01T14:09:54.1456879008 2018-11-01T14:09:54.4452391009 2018-11-01T14:09:54.747032300
    戻り値にMonoや
    Fluxを利用可能
    RestControllerなどは
    SpringMVCと共通
    数秒待たされた後
    一気に表示される

    View full-size slide

  29. #sf_h6
    Fluxはevent-streamとして返す
     Fluxを返すだけでは、レスポンスはまとめて返るだけ
     徐々にレスポンスを返すためには Server-Sent Eventにする必要がある
     レスポンスヘッダに「Content-Type: text/event-stream;」をつける
     @GetMapping(produces = MediaType.TEXT_EVENT_STREAM_VALUE)

    View full-size slide

  30. #sf_h6
    Fluxはevent-streamとして返す
    @GetMapping(produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public Flux demo() {
    return Flux.interval(Duration.ofMillis(300))
    .map(i -> i + " " + LocalDateTime.now())
    .take(10);
    }

    View full-size slide

  31. #sf_h6
    Fluxはevent-streamとして返す
    @GetMapping(produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public Flux demo() {
    return Flux.interval(Duration.ofMillis(300))
    .map(i -> i + " " + LocalDateTime.now())
    .take(10);
    }
    data:0 2018-11-05T14:16:43.520237200
    data:1 2018-11-05T14:16:43.809622100
    data:2 2018-11-05T14:16:44.120498

    data:7 2018-11-05T14:16:45.620371900
    data:8 2018-11-05T14:16:45.920298700
    data:9 2018-11-05T14:16:46.220127400
    300msごとに
    1データずつ表示される
    アノテーションを
    追加

    View full-size slide

  32. #sf_h6
    通常のSpring MVCと同様の使い方も可
    @GetMapping(value = "/list")
    public List list() {
    return Arrays.asList("a", "b", "c");
    }

    View full-size slide

  33. #sf_h6
    大きく変わるのはクライアントサイド
     RestTemplateより洗練されたWebClientが使える
     RestTemplateのココがイケてない
     Generics対応が不便
     getForObjectやgetForEntityで型指定する際に、Genericsを利用できない
     ParameterizedTypeReferenceクラスとexchangeメソッドを使う必要がある
     event-streamの扱いが面倒
     getForObjectやexchangeメソッドを用いた型変換ができない

    View full-size slide

  34. #sf_h6
    Genericsが弱いRestTemplate
    String url = "http://localhost:8081/students/list";
    var restTemplate = new RestTemplate();
    // getForObjectはGenerics指定できず
    List list = restTemplate.getForObject(url, List.class);
    list.forEach(e -> System.out.println(e.getClass() + " " + e));
    List.classとして
    取得するため
    Genericsが使えない

    View full-size slide

  35. #sf_h6
    Genericsが面倒なRestTemplate
    String url = "http://localhost:8081/students/list";
    var restTemplate = new RestTemplate();
    // Genericsを使うならこのクラスを使う必要がある
    ParameterizedTypeReference> type =
    new ParameterizedTypeReference<>() {};
    List students = restTemplate.exchange(url, HttpMethod.GET, null, type)
    .getBody();
    students.forEach(s -> System.out.println("list: " + s));
    これが必要
    exchangeメソッドの
    引数に渡す

    View full-size slide

  36. #sf_h6
    event-streamを処理しづらい
    RestTemplate
    String url = "http://localhost:8081/students/flux";
    var restTemplate = new RestTemplate();
    // いずれの呼び方でもエラー
    restTemplate.getForObject(url, List.class);
    ParameterizedTypeReference> type =
    new ParameterizedTypeReference<>() {};
    restTemplate.exchange(url, HttpMethod.GET, null, type).getBody();
    呼び出し先が
    event-streamの
    場合

    View full-size slide

  37. #sf_h6
    event-streamを処理しづらい
    RestTemplate
    String url = "http://localhost:8081/students/flux";
    var restTemplate = new RestTemplate();
    // BufferedReader経由で読み込みならできる
    restTemplate.execute(url, HttpMethod.GET, request -> {
    }, response -> {
    try (var reader = new BufferedReader(new InputStreamReader(response.getBody()))) {
    reader.lines().forEach(s -> System.out.println("flux: " + s));
    }
    return response;
    });
    取得した文字列を
    自分でパースする
    必要がある

    View full-size slide

  38. #sf_h6
    WebClientならGenericsもevent-stream
    も問題なし
     WebClientのココが便利
     Listの代わりとなるFluxを利用する際に、型を指定できる
     通常のレスポンスもevent-streamも、同様に扱える

    View full-size slide

  39. #sf_h6
    WebClientの基本的な使い方
    var webClient = WebClient.builder().build(); // WebClientのインスタンスを作成
    webClient.get() // 最初にHTTPメソッドを指定
    .uri("localhost:8081/list") // 次にURLを指定
    .retrieve() // HTTPアクセスをして結果を取り出す
    .bodyToFlux(Employee.class); // レスポンスボディをFluxにマッピング

    View full-size slide

  40. #sf_h6
    WebClientならGenericsも問題なし
    var webClient = WebClient.builder().build();
    var latch = new CountDownLatch(1);
    Flux students = webClient.get()
    .uri("localhost:8081/students/list")
    .retrieve()
    .bodyToFlux(Student.class)
    .doOnComplete(latch::countDown);
    students.subscribe(s -> System.out.println(LocalDateTime.now() + " list: " + s));
    latch.await();

    View full-size slide

  41. #sf_h6
    WebClientならevent-streamも問題なし
    var webClient = WebClient.builder().build();
    var latch = new CountDownLatch(1);
    Flux students = webClient.get()
    .uri("localhost:8081/students/flux")
    .retrieve()
    .bodyToFlux(Student.class)
    .doOnComplete(latch::countDown);
    students.subscribe(s -> System.out.println(LocalDateTime.now() + " list: " + s));
    latch.await();
    呼び出し先が
    event-streamになっても
    処理内容が変わらない

    View full-size slide

  42. #sf_h6
    WebClientの落とし穴
     FluxやMonoのblockメソッドを用いて、ListやObjectを取り出すことができる
     しかしNetty上でblockメソッドを呼び出すとエラーとなる
     block()/blockFirst()/blockLast() are blocking, which is not supported
     mapメソッドを用いるなど、blockメソッドに頼らない実装が必要となる
     Optional.get() を使わないのと同じ
     Streamでcollectせず、Streamのまま処理を続けるのと同じ

    View full-size slide

  43. #sf_h6
    WebClientの落とし穴
     FluxやMonoのblockメソッドを用いて、ListやObjectを取り出すことができる
     しかしNetty上でblockメソッドを呼び出すとエラーとなる
     block()/blockFirst()/blockLast() are blocking, which is not supported
     mapメソッドを用いるなど、blockメソッドに頼らない実装が必要となる
     Optional.get() を使わないのと同じ
     Streamでcollectせず、Streamのまま処理を続けるのと同じ
     でもそれって、簡単にできることなんでしたっけ?

    View full-size slide

  44. #sf_h6
    この章の振り返り
     Spring WebFluxではMonoやFluxを戻り値として利用できるようになる
     Spring MVCと同様のオブジェクトやListを戻り値として利用することもできる
     RestTemplateより洗練されたWebClientが利用できるようになった
     ただしWebClientではReactorの利用が必須となる

    View full-size slide

  45. #sf_h6
    (中休み)

    View full-size slide

  46. #sf_h6
    今日のお話の背景と目的を整理
     リアクティブプログラミングはリソース効率などの面で優れており、
    また時代の流れとしても求められているため、習得する価値がある
     積極的な理由
     WebClientを使おうとする限り、Reactorの習得は避けられない
     消極的な理由
     しかしながらReactorのAPIは習得が大変
     そのため、身近な題材でパターンを学ぶ

    View full-size slide

  47. #sf_h6
    今日のお話の背景と目的を整理
     身近な題材と言えば・・・

    View full-size slide

  48. #sf_h6
    今日のお話の背景と目的を整理
     身近な題材と言えば・・・
    N+1問題

    View full-size slide

  49. #sf_h6
    3. N+1問題にWebFluxで挑む

    View full-size slide

  50. #sf_h6
    課題設定
     課題の概要
     1回のマイクロサービスアクセスで、N件のデータを取る
     N件のデータに対して、1件ずつマイクロサービスにアクセスし、詳細データを取る
     RestTemplateで処理した場合と、WebClientで処理した場合の違いを見る
     具体的な課題
     /students にアクセスして生徒の一覧を取得する
     /scores/{id} にアクセスしてそれぞれの生徒の成績一覧を取得する
     その結果をマージして返す

    View full-size slide

  51. #sf_h6
    課題設定
     特別な課題設定
     /students にアクセスして生徒の一覧を取得する
     この処理がとても重いものと想定する
     生徒の人数あたり100msの取得時間が掛かる
     Fluxの場合は100msごとに生徒を1人ずつ返す
     Listの場合は生徒の人数 * 100ms後に全員分を返す
     /scores/{id} にアクセスして生徒の成績一覧を取得する
     この処理は軽いと想定する
     何件取得しても100msのオーバーヘッド時間のみで取得できる
     (性能はだいぶ恣意的な例なので、参考程度に)

    View full-size slide

  52. #sf_h6
    生徒の情報を提供するサービス (1/2)
    Student[] students = { new Student(1, "武藤"), new Student(2, "三吉"), }
    @GetMapping(value = "/list")
    public List getAsList() throws InterruptedException {
    Thread.sleep(students.length * 100L);
    return Arrays.asList(students);
    }

    View full-size slide

  53. #sf_h6
    生徒の情報を提供するサービス (2/2)
    @GetMapping(value = "/flux", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public Flux getAsFlux() {
    return Flux.interval(Duration.ofMillis(100))
    .map(i -> students[i.intValue()])
    .take(students.length);
    }

    View full-size slide

  54. #sf_h6
    成績の情報を提供するサービス
    Map> scoreStore = new HashMap<>();
    // 初期化処理は割愛
    public List getAsList(@PathVariable List ids) throws InterruptedException {
    Thread.sleep(100);
    return ids.stream()
    .flatMap(id -> scoreStore.get(id).stream())
    .collect(Collectors.toList());
    }

    View full-size slide

  55. #sf_h6
    RestTemplateを用いた場合 (1/2)
    ParameterizedTypeReference> studentType =
    new ParameterizedTypeReference<>() {
    };
    ParameterizedTypeReference> scoreType =
    new ParameterizedTypeReference<>() {
    };
    List students = restTemplate
    .exchange("http://localhost:8081/students/list", HttpMethod.GET, null, studentType)
    .getBody();

    View full-size slide

  56. #sf_h6
    RestTemplateを用いた場合 (1/2)
    ParameterizedTypeReference> studentType =
    new ParameterizedTypeReference<>() {
    };
    ParameterizedTypeReference> scoreType =
    new ParameterizedTypeReference<>() {
    };
    List students = restTemplate
    .exchange("http://localhost:8081/students/list", HttpMethod.GET, null, studentType)
    .getBody();
    生徒一覧の取得

    View full-size slide

  57. #sf_h6
    RestTemplateを用いた場合 (2/2)
    List studentScores = students.stream()
    .map(student -> {
    String url = "http://localhost:8081/scores/" + student.id;
    List scores = restTemplate
    .exchange(url, HttpMethod.GET, null, scoreType)
    .getBody();
    return new StudentScore(student, scores);
    })
    .collect(Collectors.toList());

    View full-size slide

  58. #sf_h6
    RestTemplateを用いた場合 (2/2)
    List studentScores = students.stream()
    .map(student -> {
    String url = "http://localhost:8081/scores/" + student.id;
    List scores = restTemplate
    .exchange(url, HttpMethod.GET, null, scoreType)
    .getBody();
    return new StudentScore(student, scores);
    })
    .collect(Collectors.toList());
    生徒を1件ずつ
    処理して
    生徒1件に対する
    成績を取得して
    生徒と成績を
    ペアにする

    View full-size slide

  59. #sf_h6
    WebClientを用いた場合
    Flux students = webClient.get()
    .uri("localhost:8081/students/flux")
    .retrieve()
    .bodyToFlux(Student.class);
    Flux studentScore = students.flatMap(student ->
    webClient.get()
    .uri("localhost:8081/scores/" + student.id)
    .retrieve()
    .bodyToFlux(Score.class)
    .collectList()
    .map(scores -> new StudentScore(student, scores)));

    View full-size slide

  60. #sf_h6
    WebClientを用いた場合
    Flux students = webClient.get()
    .uri("localhost:8081/students/flux")
    .retrieve()
    .bodyToFlux(Student.class);
    Flux studentScore = students.flatMap(student ->
    webClient.get()
    .uri("localhost:8081/scores/" + student.id)
    .retrieve()
    .bodyToFlux(Score.class)
    .collectList()
    .map(scores -> new StudentScore(student, scores)));
    生徒の一覧を
    Fluxとして取得
    生徒を1件ずつ
    処理して
    生徒1件に対する
    成績を取得して
    生徒と成績を
    ペアにする

    View full-size slide

  61. #sf_h6
    WebClientを用いた場合
    Flux students = webClient.get()
    .uri("localhost:8081/students/flux")
    .retrieve()
    .bodyToFlux(Student.class);
    Flux studentScore = students.flatMap(student ->
    webClient.get()
    .uri("localhost:8081/scores/" + student.id)
    .retrieve()
    .bodyToFlux(Score.class)
    .collectList()
    .map(scores -> new StudentScore(student, scores)));
    マッパー処理の戻り値が
    MonoかFluxの場合にはflatMapを用いる。
    ここでmapメソッドにすると
    戻り値が Flux> になる
    マッパー処理の戻り値が
    オブジェクトの場合には
    mapを用いる

    View full-size slide

  62. #sf_h6
    WebClientを用いた場合
    Flux students = webClient.get()
    .uri("localhost:8081/students/flux")
    .retrieve()
    .bodyToFlux(Student.class);
    Flux studentScore = students.flatMap(student ->
    webClient.get()
    .uri("localhost:8081/scores/" + student.id)
    .retrieve()
    .bodyToFlux(Score.class)
    .collectList()
    .map(scores -> new StudentScore(student, scores)));
    StudentScoreには
    FluxではなくListを渡したい
    しかしblockはできないので
    collectList()を用いてMonoにする
    Monoはmap/flatMapの
    マッパー処理内でListとして扱える

    View full-size slide

  63. #sf_h6
    利用機会が多いflatMap
     ReactorのmapとflatMap
     マッパー処理の戻り値の違い
     Mapメソッドのマッパー処理はオブジェクトを返す
     flatMapメソッドのマッパー処理はMono / Fluxを返す
     Stream APIではmapをよく利用するが、FluxではflatMapを利用することが多い
     Reactorでは特にマッパー処理の中でMonoを返すことが多いため

    View full-size slide

  64. #sf_h6
    性能の違い / 振る舞いの違い
     RestTemplateよりもWebClientを利用した場合のほうが早かった
     生徒の取得が進むたびに成績を取りに行っているため
    Front
    Service
    Score
    Service
    Student
    Service
    Front
    Service
    Score
    Service
    Student
    Service

    View full-size slide

  65. #sf_h6
    課題設定 その2
     課題の概要
     1回のマイクロサービスアクセスで、N件のデータを取る
     N件のデータに対して、1回のマイクロサービスアクセスで、詳細データを取る
     RestTemplateで処理した場合と、WebClientで処理した場合の違いを見る
     具体的な課題
     /students にアクセスして生徒の一覧を取得する
     /scores/{ids} にアクセスして生徒の成績一覧を取得する
     その結果をマージして返す

    View full-size slide

  66. #sf_h6
    RestTemplateを用いた場合 (1/2)
    ParameterizedTypeReference> studentType =
    new ParameterizedTypeReference<>() {
    };
    ParameterizedTypeReference> scoreType =
    new ParameterizedTypeReference<>() {
    };
    List students = restTemplate
    .exchange("http://localhost:8081/students/list", HttpMethod.GET, null, studentType)
    .getBody();

    View full-size slide

  67. #sf_h6
    RestTemplateを用いた場合 (2/2)
    String url = "http://localhost:8081/scores/" + ids(students);
    List scores = restTemplate
    .exchange(url, HttpMethod.GET, null, scoreType)
    .getBody();
    Map> scoreMap = scores.stream()
    .collect(Collectors.groupingBy(s -> s.id));
    List studentScores = students.stream()
    .map(student -> new StudentScore(student, scoreMap.get(student.id)))
    .collect(Collectors.toList());

    View full-size slide

  68. #sf_h6
    RestTemplateを用いた場合 (2/2)
    String url = "http://localhost:8081/scores/" + ids(students);
    List scores = restTemplate
    .exchange(url, HttpMethod.GET, null, scoreType)
    .getBody();
    Map> scoreMap = scores.stream()
    .collect(Collectors.groupingBy(s -> s.id));
    List studentScores = students.stream()
    .map(student -> new StudentScore(student, scoreMap.get(student.id)))
    .collect(Collectors.toList());
    複数の生徒の成績を
    まとめて取得
    利用しやすいよう
    いったんMapに変換
    生徒と成績を
    ペアにする
    生徒一覧を生徒IDの
    カンマ区切りに変換

    View full-size slide

  69. #sf_h6
    WebClientを用いた場合 (1/2)
    Mono> students = webClient.get()
    .uri("localhost:8081/students/flux")
    .retrieve()
    .bodyToFlux(Student.class)
    .collectList();

    View full-size slide

  70. #sf_h6
    WebClientを用いた場合 (1/2)
    Mono> students = webClient.get()
    .uri("localhost:8081/students/flux")
    .retrieve()
    .bodyToFlux(Student.class)
    .collectList();
    成績を「まとめて検索」するために
    すべての生徒の情報を取得しきってから
    処理をしたい
    やはりblockを使うわけにはいかないため
    collectListを利用する

    View full-size slide

  71. #sf_h6
    WebClientを用いた場合 (2/2)
    Mono> studentScore = students.flatMap(studentList -> webClient.get()
    .uri("localhost:8081/scores/" + ids(studentList))
    .retrieve().bodyToFlux(Score.class).collectList()
    .map(scores -> {
    Map> scoreMap = scores.stream()
    .collect(Collectors.groupingBy(s -> s.id));
    return studentList.stream()
    .map(s -> new StudentScore(s, scoreMap.get(s.id)))
    .collect(Collectors.toList());
    }));

    View full-size slide

  72. #sf_h6
    WebClientを用いた場合 (2/2)
    Mono> studentScore = students.flatMap(studentList -> webClient.get()
    .uri("localhost:8081/scores/" + ids(studentList))
    .retrieve().bodyToFlux(Score.class).collectList()
    .map(scores -> {
    Map> scoreMap = scores.stream()
    .collect(Collectors.groupingBy(s -> s.id));
    return studentList.stream()
    .map(s -> new StudentScore(s, scoreMap.get(s.id)))
    .collect(Collectors.toList());
    }));
    利用しやすいよう
    いったんMapに変換
    生徒と成績をペアにする
    流れがRestTemplateの時と
    同じ
    Studentではなく
    Listとして
    処理できる
    生徒一覧を生徒IDの
    カンマ区切りに変換

    View full-size slide

  73. #sf_h6
    Monoでまとめて処理
     まとめて処理をしたいときはFluxではなくMono>を操作する
     FluxのcollectListメソッドを使うことでMonoにできる
     ただしFluxを使った逐次処理のような良さはなくなる
     mapメソッドの中では、通常のList操作をするのと何ら変わらない様子になる

    View full-size slide

  74. #sf_h6
    性能の違い / 振る舞いの違い
     RestTemplateもWebClientを性能が変わらない
     すべての生徒の取得が終わるまで先に進まないため
    Front
    Service
    Score
    Service
    Student
    Service

    View full-size slide

  75. #sf_h6
    3.1 N+1問題にWebFluxで挑む(Ex)
    この章は、イベント後のフィードバックを受けて追加したものです。

    View full-size slide

  76. #sf_h6
    Fluxを引数とすることができる
     先の例ではリクエストの引数として「生徒のidをカンマ区切りにした文字列」
    をURLに渡していた
     URLにFluxを渡す方法はないが、POSTのボディとしてなら、Fluxを渡すことが
    できる

    View full-size slide

  77. #sf_h6
    BodyとしてFluxを受け取る(サーバ側)
    @PostMapping(value = "/flux", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public Flux getAsPost(@RequestBody Flux ids) {
    return ids.flatMapIterable(id -> scoreStore.get(id));
    }
    POSTのbodyとして
    Fluxを受け取るよう指定

    View full-size slide

  78. #sf_h6
    BodyにFluxを渡す(クライアント側 1/2)
    Flux students = webClient.get()
    .uri("localhost:8081/students/flux")
    .retrieve()
    .bodyToFlux(Student.class);
    .cache();

    View full-size slide

  79. #sf_h6
    BodyにFluxを渡す(クライアント側 1/2)
    Flux students = webClient.get()
    .uri("localhost:8081/students/flux")
    .retrieve()
    .bodyToFlux(Student.class);
    .cache();
    あとで2回、このFluxを利用するため
    取得したStudentをcacheしておく。
    cacheしなければ
    この Flux を利用するたびに
    /students/flux へのアクセスが発生する。

    View full-size slide

  80. #sf_h6
    Flux studentScore = webClient.post()
    .uri("localhost:8081/scores/flux")
    .contentType(MediaType.APPLICATION_STREAM_JSON)
    .body(students.map(s -> s.id), Integer.class)
    .retrieve()
    .bodyToFlux(Score.class)
    .collectList()
    .map(scores -> scores.stream().collect(Collectors.groupingBy(s -> s.id)))
    .flatMapMany(scoreMap ->
    students.map(student -> new StudentScore(student, scoreMap.get(student.id)))
    );
    BodyにFluxを渡す(クライアント側 2/2)

    View full-size slide

  81. #sf_h6
    Flux studentScore = webClient.post()
    .uri("localhost:8081/scores/flux")
    .contentType(MediaType.APPLICATION_STREAM_JSON)
    .body(students.map(s -> s.id), Integer.class)
    .retrieve()
    .bodyToFlux(Score.class)
    .collectList()
    .map(scores -> scores.stream().collect(Collectors.groupingBy(s -> s.id)))
    .flatMapMany(scoreMap ->
    students.map(student -> new StudentScore(student, scoreMap.get(student.id)))
    );
    postを指定
    Content-typeに
    application/stream+jsonを指定
    BodyにFluxを渡す
    BodyにFluxを渡す(クライアント側 2/2)

    View full-size slide

  82. #sf_h6
    性能の違い / 振る舞いの違い
     リクエストボディとしてFluxを利用することで、StudentServiceから受け取っ
    た値をそのままScoreServiceに渡して検索をする流れを組むことができた。
    Front
    Service
    Score
    Service
    Student
    Service

    View full-size slide

  83. #sf_h6
    4. まとめ

    View full-size slide

  84. #sf_h6
    振り返り
     Reactorはノンブロッキングプログラミングを行うためのライブラリ
     APIが膨大で、考え方も変える必要があるため、最初の敷居は少し高い
     Mono / Fluxを基本として、map / flatMap / subscribe / collectList /
    doOnComplete + CountDownLatch などが最初に覚えるべきメソッドたち
     WebFluxで利用できるようになったWebClientは、RestTemplateに比べて洗練
    されたAPIになったが、Mono / Fluxを中心としたリアクティブプログラミング
    が必須になることは要注意

    View full-size slide

  85. #sf_h6
    今後
     「業務で使いたい」というタイトルだったけど、今すぐ業務に使えるかという
    と微妙なところ
     一番の問題は、RDBMSなどのデータソースが非同期に対応していないこと
     本日の基調講演などでも紹介されたRSocketやR2DBCなどが利用できれば、利
    用機会が確実に増える
     その時代に向けて、いまのうちから準備しておきましょう!
     というか、いまそういう仕事があって焦ってるところ

    View full-size slide

  86. #sf_h6
    参考
     このスライドで紹介したWebFluxアプリケーションのソースコード
     https://github.com/cero-t/springfest2018
     Project Reactor - Learn
     https://projectreactor.io/learn
     https://github.com/reactor/lite-rx-api-hands-on/
     ReactorでN+1問題な処理を実装してみた話 – せろ部屋
     http://d.hatena.ne.jp/cero-t/20171215/1513290305
     ECサイトのサンプルをWebFluxなどで実装したもの
     https://github.com/cero-t/spring-cloud-kinoko-2017

    View full-size slide

  87. #sf_h6
    Try, Reactive Programming!!

    View full-size slide