「ねえGoogle、WebFluxでAPIを実装してみて」
「Alexa、KotlinでAPIを実装してみて」
Spring 5.0のリリースで、WebFluxやKotlinのサポートが追加されました。
Spring WebFluxは、リアクティブプログラミングが出来る新しいフレームワークです。
Spring MVCはServlet APIベースでしたが、Spring WebFluxはReactive Streamベースの新しいHTTP API上に実装されています。
Kotlinは、Android開発言語として正式採用されたJVM言語です。
今日はKotlinを使ってSpring WebFluxでリアクティブなWeb APIでも作ってみようかと思います。
OS: macOS Sierra Version 10.12.6
Java: 1.8.0_151
Gradle: 4.2
Spring Boot: 2.0.0 M6
Kotlin: 1.1.51
プロジェクトは Spring Initializr を使ってサクッと作ります。
Gradle projectにして、言語は Kotlinに。Spring Boot は 2.0.0 M6 を選択。
Spring Boot は 2.0 から Spring5 に対応していますが、11月20日の執筆時点ではまだM6(pre)です。
Dependencies はとりあえず、Reactive Web を選んでおけばWebFluxが使えます。
build.gradle はこんな感じになります。
dependencies { |
compile('org.springframework.boot:spring-boot-starter-webflux') |
compile("org.jetbrains.kotlin:kotlin-stdlib-jre8") |
compile("org.jetbrains.kotlin:kotlin-reflect") |
testCompile('org.springframework.boot:spring-boot-starter-test') |
testCompile('io.projectreactor:reactor-test') |
} |
Spring WebFlux はアノテーションベースのプログラミングモデルと、ファンクショナルなプログラミングモデルの2パターンが用意されています。
前者は @RestController とか @GetMapping といった Spring MVCでおなじみのアノテーションを使って実装します。
後者は、ラムダベースの新しい実装方法になります。
今回は、後者の方で作ってみます。
リクエスト要求はRouter Function によって Handler Function にルーティングされます。
@Controllerクラスの@RequestMappingアノテーションのような感じですかね。
@Configuration |
class Router(private val demoHandler: DemoHandler, private val userHandler: UserHandler) { |
@Bean |
fun apiRouter() = router { |
accept(APPLICATION_JSON_UTF8).nest { |
GET("/", demoHandler::getDemo) |
GET("/users", userHandler::findAll) |
} |
accept(TEXT_EVENT_STREAM).nest { |
GET("/users", userHandler::streamOneSec) |
} |
} |
} |
Handler Functionはこんな感じ。
普通にJsonでレスポンス返すやつと、Stream感のあるレスポンスを返すやつを作ってみます。
FluxはReactive StreamのPublisherを実装したクラスでN要素のストリームを実現してます。
対してMonoは1 or 0要素のPublisherです。
@Component |
class UserHandler { |
private val users = Flux.just( |
User("一郎", "クレスコ", 20), |
User("二郎", "クレスコ", 30), |
User("三郎", "クレスコ", 40), |
User("四郎", "クレスコ", 50), |
User("五郎", "クレスコ", 60)) |
private val streamingUsers = Flux |
.zip(Flux.interval(Duration.ofSeconds(1)), users.repeat()) |
.map { it.t2 } |
fun findAll(req: ServerRequest): Mono<ServerResponse> { |
return ok() |
.contentType(APPLICATION_JSON_UTF8) |
.body(users, User::class.java) |
} |
fun streamOneSec(req: ServerRequest) = ok().bodyToServerSentEvents(streamingUsers) |
} |
以上でAPIの作成は終わり。とりあえず、今回はGETだけです。
それでは実行してみます。
$ ./gradlew bootRun
Starting a Gradle Daemon, 1 incompatible Daemon could not be reused, use --status for details
> Task :bootRun
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.0.0.M6)
2017-11-23 01:32:40.197 INFO 736 --- [ main] com.example.demo.DemoApplicationKt : Starting DemoApplicationKt on Js-MacBook-Pro.local with PID 736 (/Users/jtkwysnr/work/spring-boot-app/web-flux-kotlin/build/classes/kotlin/main started by jtkwysnr in /Users/jtkwysnr/work/spring-boot-app/web-flux-kotlin)
2017-11-23 01:32:40.202 INFO 736 --- [ main] com.example.demo.DemoApplicationKt : No active profile set, falling back to default profiles: default
2017-11-23 01:32:40.263 INFO 736 --- [ main] .r.c.ReactiveWebServerApplicationContext : Refreshing org.springframework.boot.web.reactive.context.ReactiveWebServerApplicationContext@1f0f1111: startup date [Thu Nov 23 01:32:40 JST 2017]; root of context hierarchy
2017-11-23 01:32:41.430 INFO 736 --- [ main] o.s.w.r.f.s.s.RouterFunctionMapping : Mapped Accept: [application/json;charset=UTF-8] => {
2017-11-23 01:32:41.444 INFO 736 --- [ main] o.s.w.r.handler.SimpleUrlHandlerMapping : Mapped URL path [/webjars/**] onto handler of type [class org.springframework.web.reactive.resource.ResourceWebHandler]
2017-11-23 01:32:41.445 INFO 736 --- [ main] o.s.w.r.handler.SimpleUrlHandlerMapping : Mapped URL path [/**] onto handler of type [class org.springframework.web.reactive.resource.ResourceWebHandler]
2017-11-23 01:32:41.499 INFO 736 --- [ main] o.s.w.r.r.m.a.ControllerMethodResolver : Looking for @ControllerAdvice: org.springframework.boot.web.reactive.context.ReactiveWebServerApplicationContext@1f0f1111: startup date [Thu Nov 23 01:32:40 JST 2017]; root of context hierarchy
2017-11-23 01:32:41.842 INFO 736 --- [ main] o.s.j.e.a.AnnotationMBeanExporter : Registering beans for JMX exposure on startup
2017-11-23 01:32:46.998 INFO 736 --- [ctor-http-nio-1] r.ipc.netty.tcp.BlockingNettyContext : Started HttpServer on /0:0:0:0:0:0:0:0:8080
2017-11-23 01:32:46.998 INFO 736 --- [ main] o.s.b.web.embedded.netty.NettyWebServer : Netty started on port(s): 8080
2017-11-23 01:32:47.002 INFO 736 --- [ main] com.example.demo.DemoApplicationKt : Started DemoApplicationKt in 17.086 seconds (JVM running for 17.404)
おお、いつものtomcatではなく Nettyが起動してますね。
curlでAPIを叩いて確認してみます。まずは、普通にJSON返すやつを。
$ curl -H "accept: application/json" http://localhost:8080/users |
[ |
{ |
"age": 20, |
"firstName": "一郎", |
"lastName": "クレスコ" |
}, { |
"age": 30, |
"firstName": "二郎", |
"lastName": "クレスコ" |
}, { |
"age": 40, |
"firstName": "三郎", |
"lastName": "クレスコ" |
}, { |
"age": 50, |
"firstName": "四郎", |
"lastName": "クレスコ" |
}, { |
"age": 60, |
"firstName": "五郎", |
"lastName": "クレスコ" |
} |
] |
普通にJSON Array返してます。
続いて、Stream感のあるやつ。
$ curl -H "accept: text/event-stream" http://localhost:8080/users |
data:{"firstName":"一郎","lastName":"クレスコ","age":20} |
data:{"firstName":"二郎","lastName":"クレスコ","age":30} |
data:{"firstName":"三郎","lastName":"クレスコ","age":40} |
data:{"firstName":"四郎","lastName":"クレスコ","age":50} |
data:{"firstName":"五郎","lastName":"クレスコ","age":60} |
data:{"firstName":"一郎","lastName":"クレスコ","age":20} |
data:{"firstName":"二郎","lastName":"クレスコ","age":30} |
わかりずらいですが、1秒おきに1件ずつ返してます。
以上、ざっと触ってみました。
今回のサンプルコードは、ココ。
今回は、Spring WebFlux を使って簡単なAPIを作ってみました。Reactor対応のHTTP Client とかも試してみたいですね。
Kotlinについては記事の中では全く触れてないですが、いいですね。
KotlinのテスティングフレームワークSpekも良さそうな感じ。
最後にSpring Bootのスプラッシュをクリスマスっぽくして、
それでは、また。