この記事は 『CRESCO Advent Calendar 2017』 10日目の記事です。

 

VPA関連を書こうと思ったけれど、他の人と被りまくりそうな気がするので回避。

「ねえGoogle、WebFluxでAPIを実装してみて」
「Alexa、KotlinでAPIを実装してみて」

Spring 5.0のリリースで、WebFluxやKotlinのサポートが追加されました。
Spring WebFluxは、リアクティブプログラミングが出来る新しいフレームワークです。
Spring MVCはServlet APIベースでしたが、Spring WebFluxはReactive Streamベースの新しいHTTP API上に実装されています。

https://spring.io/
拡大
https://spring.io/

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')
}

Web API 作成

Annotation-based Programming Model と Functional Programming Model

Spring WebFlux はアノテーションベースのプログラミングモデルと、ファンクショナルなプログラミングモデルの2パターンが用意されています。

前者は @RestController とか @GetMapping といった Spring MVCでおなじみのアノテーションを使って実装します。
後者は、ラムダベースの新しい実装方法になります。
今回は、後者の方で作ってみます。

RouterFunctions

リクエスト要求は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)
}
}
}

HandlerFunctions

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だけです。

API実行

それでは実行してみます。

$ ./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のスプラッシュをクリスマスっぽくして、

それでは、また。