Kotlin + Spring boot2 + Webflux + RouterFunctionsのセッション話

# はじめに
[前回の記事](https://qiita.com/tasogarei/items/a786d47cab6fff3162bc)でRouterFunctionsを使ってThymeleafの描画をした後にJavaで作ってたWebアプリケーションを移植していたら、セッション周りで大いにはまってしまったので記事にしました。

2018/5/26
この後もちょこちょこ勉強していますが、本記事の取り方だとまずいことに今さら気づきました。
Monoに包まれた世界から包まれてない世界に旅立っているのに処理させすぎです。
修正案はある程度見えているのですが、検証している時間が中々取れてないためしばしセッションを取るアンチパターンぐらいで見ておいてください。

2018/5/29
Monoに包まれた世界で修正してみました。
最初のコード書き換えるのもあれなので、追記という形をとっています。

# 環境
IntelliJ IDEA
Kotlin 1.2.20
Java 8
Spring Boot 2.0.0.RELEASE

# 本題
セッションはHandlerで使うこととなるかと思いますが、アノテーションでHandlerを実装する際はWebで調べていると引数として受け取ることが可能となっています。
しかし、RouterFunctionsから呼ばれるHandlerを呼ぶ際に指定可能な引数は`ServerRequest`だけのようで、セッション受け取ろうと`WebSession`を指定するとエラーとなります。
そのため、どうにかして引数以外から`WebSession`を受け取る必要があることが判明しました。

``` kotlin
OK fun getIndex(req: ServerRequest): Mono<ServerResponse>
NG fun getIndex(req: ServerRequest, session : WebSession): Mono<ServerResponse>
```

とりあえずリクエストからセッションは`session()`で取り出すことが可能であるのは簡単に分かったのですが、この戻り値が`Mono<WebSession>`になっており直接`WebSession`を取得することが出来ませんでした。

``` kotlin
var session : Mono<WebSession> = req.session()
```

`Mono<T>`はこの中にあります。

``` kotlin
import reactor.core.publisher.Mono
```

このMonoクラスの`<T>`部分取る関数名が`block()`となっているのがはまりどころでした。
JavaDocなんかも見ていたのですが、はっはっはご冗談という感じで他の方法を探しまくってました。
色々と探しているうちに[こちらの記事](https://qiita.com/sugibuchi/items/e933ae4b5ec512054de2)が見つかり、本当に`block()`が正しいことが分かったため事なきを得ました。
ここらへんは知識不足でのはまりだったので、勉強が必要ですね・・・

そんな訳で`block()`で`WebSession`が取れることは分かってので`attribute`の取得と設定をすればセッションに登録可能となります。

``` kotlin
// 取得
val value : String? = req.session().block()?.getAttribute("key")
```

セッションが存在しないもしくはkeyが存在しない場合はnullが返ってくるので、nullを想定するべきです。
`WebSession`には`getAttributeOrDefault`と存在しない場合の値を決める関数がありますが、ここはKotlinのエルビス演算子を使用した方が`block()`の対応もまとめて出来るので良いと思います。

``` kotlin
// こっちじゃなくて
val value : String? = req.session().block()?.getAttributeOrDefault("key", "")
// こっちを使う
val value = req.session().block()?.getAttribute("key") ?: ""
```

nullで判定させたい場合は、nullを返させてエルビス演算子で判定させればキレイな感じの処理になるかなと思います。

``` kotlin
val value = req.session().block()?.getAttribute("key")
value ?: {
    return
}

print(value)

// これでもいけるかと思ったらコードの意味が変わるのでダメですね
req.session().block()?.getAttribute("key") ?: {
    return
}
```

こんな感じです。

``` kotlin
// 設定
req.session().block()!!.attributes["key"] = value
```

設定時はセッションがnullだと入れる場所がなくて困るので、`?`は使えません。
ビルドエラーになります。

``` kotlin
// 破棄
req.session().block()!!.attributes["key"] = null
```

破棄はたぶんkeyに対してnull入れれば良いかとは思いますが、Kotlinの作法や`Mono<T>`的に正しいかはよく分かってません。

# ちなみに
`WebSession`のSessionはCookieで管理されています。
これをHeaderに入れれるにすることも可能のようですが、やり方はまだ分かってません。
分かり次第、ここに追記したいと思います。

追記：
色々試したのですが、うまくSessionがHeaderに入ってくれません。
理由がさっぱり分からないのですが、こればっかりに時間を使う訳にいかないので調査を打ち切ります。
分かったら再度追記しますが、いったんコードだけのせます。

``` kotlin
package com.tasogarei.application.config

import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.session.MapSession
import org.springframework.session.ReactiveMapSessionRepository
import org.springframework.session.ReactiveSessionRepository
import org.springframework.session.config.annotation.web.server.EnableSpringWebSession
import org.springframework.web.server.session.HeaderWebSessionIdResolver
import org.springframework.web.server.session.WebSessionIdResolver
import java.util.concurrent.ConcurrentHashMap

@Configuration
@EnableSpringWebSession
class WebConfigSession {
    @Bean
    fun reactorSessionRepository(): ReactiveSessionRepository<MapSession> {
        return ReactiveMapSessionRepository(ConcurrentHashMap())
    }

    @Bean
    fun headerWebSessionIdResolver(): WebSessionIdResolver {
        val resolver = HeaderWebSessionIdResolver()
        resolver.headerName = "X-SESSION-ID"
        return resolver
    }
}
```

# 最後に
`block()`という名前に翻弄させられてしまいましたが、なんとかセッションを扱うことが出来ました。
これで移植は完了したのですが、このケースはどうやるんだろう？がぽろぽろ出てきているのでもう少し勉強する必要がありますね。
また勉強して記事にしていきたいと思います。

# Blockしないセッションの扱い()
勉強不足という問題でnon-blocking処理とblockin処理が混ざったコードとなってしまいました。
混ぜるな危険ということが言われており、混ぜないコードも考えてみました。

これが正解かといわれるとまだよくわかってないので、分かる人いたら教えてほしいです・・・

## セッション取得
取得は`map`や`flatMap`で取得しながら変換するコードが真っ先に思い浮かびました。
`spring-webflux`の戻り値は`Mono<ServerResponse>`となっているため、セッションから値を取り出してそのままResponseを作成するみたいなことも簡単に出来ます。

ここでは値を取り出すところだけサンプルとして記載しておきます。

``` kotlin
val value : Mono<String?> = request.session().map {
    it.getAttribute("key") as? String
}
```

本題とは関係ないのですが、sessionの値がない場合とある場合の処理を組んだ際に`when`ってこんな感じで組めることを発見しました。
普通にnullもいけちゃうんですね。一つ勉強になりました。

``` kotlin
when(attribute) {
    null -> "nullの処理"
    else -> "not nullの処理"
}
```

## セッション設定
設定はsubscribeを使います。


``` kotlin
request.session().subscribe {
    it.attributes["key"] = value
}
```

# 最後に
難しくてまだよく分かってない部分も多いので、ちまちま勉強は続けたいと思います。
もっと良いやり方が分かったらまた更新します。