18
18

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Spring WebFluxを試す (主にRouter Functions)

Last updated at Posted at 2017-05-20

概要

Java Day Tokyo 2017でSpring Framework 5.0による Reactive Web Applicationを聞いて軽くテンション上がったのでSpring5から追加されるSpring WebFluxを試してみた。

参考資料はこちら。
はじめてのSpring WebFlux (その1 - Spring WebFluxを試す)
著者は登壇者と同じToshiaki Makiさん。
(というかセッションでご本人が「ブログ書いてるんで、それ読めば試せます」的なことを仰っていた)


とりあえずアプリを起動するまで

今回はSpring Bootの2.0.0 M1を使用。
(Spring Bootは2.0.0からSpring5に対応しているため)

Spring Initalizerでさっくり作成する。
Spring Bootのバージョンを2.0.0 M1にして、"Reactive Web"を追加。
image.png
(ちなみにM1時点ではactuatorは非対応。残念)

で、起動してNettyが立ち上がったのを確認したひとまずOK。
以下起動ログの抜粋。

2017-05-20 14:14:17.447  INFO 4880 --- [  restartedMain] o.s.w.r.r.m.a.ControllerMethodResolver   : Looking for @ControllerAdvice: org.springframework.boot.web.reactive.context.ReactiveWebServerApplicationContext@30361c29: startup date [Sat May 20 14:14:16 JST 2017]; root of context hierarchy
2017-05-20 14:14:17.916  INFO 4880 --- [  restartedMain] o.s.b.d.a.OptionalLiveReloadServer       : LiveReload server is running on port 35729
2017-05-20 14:14:17.944  INFO 4880 --- [  restartedMain] o.s.j.e.a.AnnotationMBeanExporter        : Registering beans for JMX exposure on startup
2017-05-20 14:14:18.241  INFO 4880 --- [  restartedMain] o.s.b.web.embedded.netty.NettyWebServer  : Netty started on port(s): 8080
2017-05-20 14:14:18.246  INFO 4880 --- [  restartedMain] itaka.demo.WebfluxSampleApplication      : Started WebfluxSampleApplication in 2.181 seconds (JVM running for 3.946)

Router Functionsでルーティング定義

WebFluxではRouterFunction<ServerResponse>@Beanがあればそれがルーティング定義として扱われる。
なので、Component Scan対象のクラスに書けばどこでも良さそう。

// いくつかstatic importしているので、詳細は参考資料のほうを参照
@Bean
public RouterFunction<ServerResponse> routes() {
    return route(GET("/"), req -> ok().body(Flux.just("Hello", "WebFlux"), String.class));
}

で、これで起動してhttp://localhost:8080/にアクセスしたら"HelloWebFlux"が返ってくる。
以上。

PathVariableどう扱うの?

  1. RequestPredicate(GETとかPOSTとか)の引数に今までどおり"{var}"のように記述
  2. ServerRequestの.pathVariable()で取得

の手順でいける。

@Bean
public RouterFunction<ServerResponse> routes() {
    return RouterFunctions
            .route(GET("/hello/{name}"), req ->
                    ok().body(Flux.just("Hello", req.pathVariable("name")), String.class));
}

Routingの優先順位

上記のPathVariableのroutingと"/hello/hoge"という固定パス定義によるroutingを両方定義した場合、何が起こるか。
結論から言うと、先に書いたほうが優先される。シンプル。

PathVariableのroutingを先に書いた場合

RouterFunctions
 .route(GET("/hello/{name}"), req -> ...)
 .AndRroute(GET("/hello/hoge"), req -> ...);

-> PathVariableが優先される。

固定パス定義のroutingを先に書いた場合

RouterFunctions
 .route(GET("/hello/hoge"), req -> ...)
 .AndRroute(GET("/hello/{name}"), req -> ...);

-> 固定パス定義が優先される。

Routing定義が増えてきたら

サンプル程度の規模であれば上述のようにダラダラroute定義をつなげていっても問題ないが、これは実際のアプリケーションで言えば__1つのControllerクラスに全Mappingを記述していることになる__ので、よろしくない。
そこで、以下のようにすることで多少すっきり書くことができる。

  1. RouterFunction<ServerResponse>を返却するメソッドを作って別クラスに置く
  2. 元の@Bean定義しているメソッド内で1のメソッドを呼び出すようにする
HelloRouter.java
@Component
public class HelloRouter {

    private static final String PATH = "/hello";

    public RouterFunction<ServerResponse> route() {
        return RouterFunctions
                .route(GET(PATH), this::hello)
                .andRoute(GET(PATH + "/hoge"), this::helloHoge)
                .andRoute(GET(PATH + "/{name}"), this::helloName);
    }

    private Mono<ServerResponse> hello(ServerRequest req) {
        return ok().body(Flux.just("Hello", "WebFlux"), String.class);
    }

    private Mono<ServerResponse> helloName(ServerRequest req) {
        return ok().body(Flux.just("Hello", req.pathVariable("name")), String.class);
    }

    private Mono<ServerResponse> helloHoge(ServerRequest req) {
        return ok().body(Flux.just("Hoge", "Hoge"), String.class);
    }
}
HelloRouterのroute()を呼び出す
@Bean
public RouterFunction<ServerResponse> routes(HelloRouter helloRouter) {
    return helloRouter.route();
}

更に、.andによるメソッドチェーンで複数のRouterFunction<ServerResponse>を繋げることができる。

.and()を使って複数のRouterFunctionをがっちゃんこする
// StreamRouterでは"/strem"のroutingを定義してあるものとする
@Bean
public RouterFunction<ServerResponse> routes(HelloRouter helloRouter, StreamRouter streamRouter) {
    return helloRouter.route()
            .and(streamRouter.route());
}

これで以下のroutingがセットされる。

  • /hello
  • /hello/hoge
  • /hello/{name}
  • /stream

(追記) RouterFunctions.nest()による改善

コメントをいただいたので、nest()を使って上のコードを更に書き換えてみる。
(Makiさんありがとうございます!)

RouterFunctions.nest()で階層状にrouting定義
public RouterFunction<ServerResponse> routes() {
    return nest(path("/hello"),
            route(GET("/"), this::hello)
                .andRoute(GET("/hoge"), this::helloHoge)
                .andRoute(GET("/{name}"), this::helloName));
}

ついでにRouterFunctions.route()もstatic importしたので、大分スッキリした!
RouterFunctionの部分だけ見てRouting内容を把握できるので、ここも@Controllerでの定義よりも良い所かな。


無限Streamで遊ぶ

ServerResponseの.body()でセットしているFluxというのはReactive StreamのPublisherを実装しているクラスで、イイ感じにReativeなStreamを実現できるそう。

具体例として「アクセスしたら延々と数字が返ってきて、その数字がカウントアップされていく」ものを実装してみる。
方法は以下の通り。

  1. 無限Streamを作る
  2. 1のStreamからFluxを作る
  3. 2のFluxをbodyとしてセット
@Bean
public RouterFunction<ServerResponse> routes() {
    Stream<Integer> stream = Stream.iterate(0, i -> i + 1);
    Flux<Integer> flux = Flux.fromStream(stream).delayElements(Duration.ofSeconds(1));

    return RouterFunctions
            .route(GET("/stream"), req ->
                    ok().contentType(MediaType.APPLICATION_STREAM_JSON).body(flux, Integer.class));
}

これで、http://localhost:8080/streamにアクセスすると延々と数字が流れてくる。
ただそのままだとものすごい勢いで流れてくるので、上記例では.delayElements()を使用してインターバルを持たせて流している。


まとめ

「WebFluxを試す」と言っておきながら、半分以上Router Functionsの実験になってしまった。
「WebFluxすげー」って体感するためには、もうちょい複雑というか「Non Blockingだから超速い」みたいなことが分かるようなアプリケーションを作ったほうが良さそう。

Router FunctiuonsはJava Day Tokyoのセッションを聞いていたときよりもテンション高く遊べた。
今回はGETしか使わなかったが、もちろん他のHttpMethodも使えるし、.and().or().negate()などを使って複雑なrouting条件を作ることもできるので楽しそう。
ただ、現時点(Spring Boot 2.0.0)では@Controllerとの共存ができないらしいので既存アプリケーションに適用するのはちょっとツライ(全部書き換えなくちゃいけない)・・・。
やるなら新規アプリケーションでやったほうが良いかな。

今回試したもののコードは以下の通り。
(書きながらリファクタリングしていったので、この記事のコードとは若干ズレがある)
IsaoTakahashi/webflux-sample - GitHub

18
18
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
18
18

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?