4
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Posted at

やること

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

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

やり方

この記事ではHandlerInterceptorRequestContextHolderに値を設定し、それを参照する方法を紹介します。

値を設定・参照する

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

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になるため、型にするためにはキャストが必要です。
また、リクエストに対する一連の処理内で当該のnamescopeへの登録がされていなければnullが返ってきます。

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()を登録しています。

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

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

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("/**") // 適用対象となるパス、ここでは全指定
    }
}

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

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

注意点

今回紹介したやり方では、リクエストを受けたスレッドと異なるスレッド(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.

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

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

参考にさせて頂いた内容

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

4
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?