やること
現在時刻をリクエスト内の各所でLocalDateTime.now()
するような形で取得してしまうと、処理の最初と最後で値がズレてしまい、バグにつながる場合があります。
このような値は1度のみ生成して使い回すことが望ましいですが、素直に書いてしまうと各関数に値を渡す必要が出たり、値を使い回すことに設計が引っ張られてしまうことにもなり得ます。
そこで、リクエスト単位で一意な値を設定・参照できるようにすることでこの問題を解決します。
やり方
この記事ではHandlerInterceptor
でRequestContextHolder
に値を設定し、それを参照する方法を紹介します。
値を設定・参照する
リクエスト単位での値の保持は以下のように行うことができます。
例ではリクエスト単位の指定としていますが、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
になるため、型にするためにはキャストが必要です。
また、リクエストに対する一連の処理内で当該のname
・scope
への登録がされていなければ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("/**") // 適用対象となるパス、ここでは全指定
}
}
これによって、リクエストの全てで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.
この状況に対しては、マルチスレッド処理のスコープは限られていて値を渡すのは容易だと考えられるため、以下のように何故エラーが出ているかだけ分かるようにラップだけしておくのが良いと思っています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
経由で値を設定・参照することで、リクエスト単位で値を一意に扱う方法を紹介しました。
グローバル変数同様乱用は禁物ですが、層を横断して一意な値を扱いたいような場面では非常に強力なツールになりうると思います。
参考にさせて頂いた内容
- RequestContextHolder (Spring Framework 5.3.4 API)
- RequestAttributes (Spring Framework 5.3.4 API)
- SpringのContextHolderいろいろ - SIerだけど技術やりたいブログ
-
エラーメッセージの通り、
RequestContextListener
またはRequestContextFilter
で対処できるかもしれませんが、本文で説明した通りの理由で検証する気が起きなかったため、この記事では紹介しません。 ↩