【SpringBoot】リクエスト/セッション単位で一意な値を設定・参照する

# やること
現在時刻をリクエスト内の各所で`LocalDateTime.now()`するような形で取得してしまうと、処理の最初と最後で値がズレてしまい、バグにつながる場合があります。
このような値は1度のみ生成して使い回すことが望ましいですが、素直に書いてしまうと各関数に値を渡す必要が出たり、値を使い回すことに設計が引っ張られてしまうことにもなり得ます。

そこで、リクエスト単位で一意な値を設定・参照できるようにすることでこの問題を解決します。

# やり方
この記事では`HandlerInterceptor`で`RequestContextHolder`に値を設定し、それを参照する方法を紹介します。

## 値を設定・参照する
リクエスト単位での値の保持は以下のように行うことができます。
例ではリクエスト単位の指定としていますが、`setAttribute`の第三引数（`scope`）に`RequestAttributes.SCOPE_SESSION`を指定すればセッション単位で保持することもできます。

```kotlin
import org.springframework.web.context.request.RequestAttributes
import org.springframework.web.context.request.RequestContextHolder
import java.time.LocalDateTime

val now: LocalDateTime = LocalDateTime.now()
val currentAttributes: RequestAttributes = RequestContextHolder.currentRequestAttributes()

currentAttributes.setAttribute("now", now, RequestAttributes.SCOPE_REQUEST)
```

参照は以下のように行います。
取れるのは`Object`になるため、型にするためにはキャストが必要です。
また、リクエストに対する一連の処理内で当該の`name`・`scope`への登録がされていなければ`null`が返ってきます。

```kotlin
import org.springframework.web.context.request.RequestContextHolder
import java.time.LocalDateTime

val currentAttributes: RequestAttributes = RequestContextHolder.currentRequestAttributes()
val now: LocalDateTime =
        currentAttributes.getAttribute("now", RequestAttributes.SCOPE_REQUEST) as LocalDateTime
```

### リクエストごとに値を設定する
リクエストごとに値を設定する方法はいくつか考えられますが、ここでは`HandlerInterceptor`を作成し、コンフィグで全パスに適用する方法を紹介します。

まず以下のように現在日時をセットするインターセプターを定義します。
サンプルでは`now`という名前で`LocalDateTime.now()`を登録しています。

```kotlin
import org.springframework.stereotype.Component
import org.springframework.web.context.request.RequestAttributes
import org.springframework.web.context.request.RequestContextHolder
import org.springframework.web.servlet.HandlerInterceptor
import java.time.LocalDateTime
import javax.servlet.http.HttpServletRequest
import javax.servlet.http.HttpServletResponse

@Component
class SetCurrentTimeInterceptor : HandlerInterceptor {
    override fun preHandle(request: HttpServletRequest, response: HttpServletResponse, handler: Any): Boolean {
        // リクエスト毎に一意な日時をセット
        RequestContextHolder
                .currentRequestAttributes()
                .setAttribute("now", LocalDateTime.now(), RequestAttributes.SCOPE_REQUEST)

        return true
    }
}
```

次に、以下のようにインターセプターを登録するコンフィグを追加します。

```kotlin
import org.springframework.context.annotation.Configuration
import org.springframework.web.servlet.config.annotation.InterceptorRegistry
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer

@Configuration
class SetCurrentTimeInterceptorConfig(
        private val setCurrentTimeInterceptor: SetCurrentTimeInterceptor
) : WebMvcConfigurer {
    override fun addInterceptors(registry: InterceptorRegistry) {
        registry.addInterceptor(setCurrentTimeInterceptor)
                .addPathPatterns("/**") // 適用対象となるパス、ここでは全指定
    }
}
```

これによって、リクエストの全てで`name`と`scope`から一意な日時の読み出しが行えるようになります。

実際のアプリケーションでは、既存のインターセプター/コンフィグに設定を追加する形でも構わないでしょう。

## 注意点
今回紹介したやり方では、リクエストを受けたスレッドと異なるスレッド（`pallarelStream`の内部や、`@Async`を付与したメソッド）から呼び出した場合、以下のように`IllegalStateException`が発生します。

```
java.lang.IllegalStateException: No thread-bound request found: Are you referring to request attributes outside of an actual web request, or processing a request outside of the originally receiving thread? If you are actually operating within a web request and still receive this message, your code is probably running outside of DispatcherServlet: In this case, use RequestContextListener or RequestContextFilter to expose the current request.
```

この状況に対しては、マルチスレッド処理のスコープは限られていて値を渡すのは容易だと考えられるため、以下のように何故エラーが出ているかだけ分かるようにラップだけしておくのが良いと思っています[^mt]。

[^mt]: エラーメッセージの通り、`RequestContextListener`または`RequestContextFilter`で対処できるかもしれませんが、本文で説明した通りの理由で検証する気が起きなかったため、この記事では紹介しません。

```kotlin
import org.springframework.web.context.request.RequestAttributes
import org.springframework.web.context.request.RequestContextHolder
import java.time.LocalDateTime

/**
 * @return リクエストごとに一意な現在日時
 */
val currentTime: LocalDateTime get() = try {
    RequestContextHolder
            .currentRequestAttributes()
            .getAttribute("now", RequestAttributes.SCOPE_REQUEST) as LocalDateTime
} catch (e: IllegalStateException) {
    throw IllegalStateException("リクエストを処理するスレッド以外から呼び出さないで下さい。", e)
}
```

# 終わりに
この記事では、`RequestContextHolder`経由で値を設定・参照することで、リクエスト単位で値を一意に扱う方法を紹介しました。
グローバル変数同様乱用は禁物ですが、層を横断して一意な値を扱いたいような場面では非常に強力なツールになりうると思います。

# 参考にさせて頂いた内容
- [RequestContextHolder \(Spring Framework 5\.3\.4 API\)](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/context/request/RequestContextHolder.html)
- [RequestAttributes \(Spring Framework 5\.3\.4 API\)](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/context/request/RequestAttributes.html)
- [SpringのContextHolderいろいろ \- SIerだけど技術やりたいブログ](https://www.kimullaa.com/entry/2017/02/18/211448)