概要
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"を追加。
(ちなみに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どう扱うの?
- RequestPredicate(
GET
とかPOST
とか)の引数に今までどおり"{var}"のように記述 - 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を記述していることになる__ので、よろしくない。
そこで、以下のようにすることで多少すっきり書くことができる。
-
RouterFunction<ServerResponse>
を返却するメソッドを作って別クラスに置く - 元の
@Bean
定義しているメソッド内で1のメソッドを呼び出すようにする
@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);
}
}
@Bean
public RouterFunction<ServerResponse> routes(HelloRouter helloRouter) {
return helloRouter.route();
}
更に、.and
によるメソッドチェーンで複数のRouterFunction<ServerResponse>
を繋げることができる。
// 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さんありがとうございます!)
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を実現できるそう。
具体例として「アクセスしたら延々と数字が返ってきて、その数字がカウントアップされていく」ものを実装してみる。
方法は以下の通り。
- 無限Streamを作る
- 1のStreamからFluxを作る
- 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