Quarkusを使っていると、次のようなエラーに遭遇することがある。
jakarta.enterprise.context.ContextNotActiveException: RequestScoped context was not active when trying to obtain a bean instance for a client proxy of CLASS bean
これは@RequestScopedなBeanをRequest Contextが開始されていない実行パス(非同期処理・別スレッド・HTTP以外のエントリポイントなど)で使ったときに発生する。
@RequestScopedの前提を整理
Quarkusの@RequestScopedはCDI(Jakarta Contexts and Dependency Injection)の仕様に基づいている。
@RequestScoped class RequestInfo { val requestId: String = UUID.randomUUID().toString() }
@RequestScopedはスレッド単位ではない。- Request Context単位で管理される。
Request Contextはいつ有効か
Quarkusでは、QuarkusがHTTPリクエスト処理として管理している同期処理(フィルタ / インターセプタを含む)の間だけ、Request Contextが自動的に有効になる。
@Path("/hello") class HelloResource { @Inject lateinit var requestInfo: RequestInfo @GET fun hello(): String { // Request Context は有効 return requestInfo.requestId } }
例外発生ケース:非同期処理
CompletableFutureを使った例。
@ApplicationScoped class AsyncService { @Inject lateinit var requestInfo: RequestInfo fun runAsync() { CompletableFuture.runAsync { // ここで例外が発生 println(requestInfo.requestId) } } }
- CompletableFuture.runAsyncはQuarkusが管理していないスレッドプール(ForkJoinPool)で実行される。
- そのスレッドではRequest Contextが開始されていない。
- にもかかわらず
@RequestScopedBeanにアクセスしたため、例外が発生する。
@ActivateRequestContextとは何か
@ActivateRequestContextは「このメソッドの実行中、同期的に実行される範囲のみRequest Contextを有効化する」ためのアノテーションである。
@ApplicationScoped class ContextAwareService { @Inject lateinit var requestInfo: RequestInfo @ActivateRequestContext fun doSomething() { println(requestInfo.requestId) } }
@ActivateRequestContextが必要になる代表的ケース
(1) HTTP以外のエントリポイント
- Scheduler
- Message Consumer
- Batch / Job
- 一部のテストコード
@ApplicationScoped class ScheduledJob { @Inject lateinit var requestInfo: RequestInfo @Scheduled(every = "10s") @ActivateRequestContext fun execute() { println("requestId=${requestInfo.requestId}") } }
HTTPリクエストが存在しないため、明示的にRequest Contextを開始する必要がある。
(2) RequestScoped前提の既存設計を利用したい場合
- インターセプタやフィルタと共通利用したい。
- API設計を変えたくない。
- RequestScopedな依存が多い。
import jakarta.enterprise.context.ApplicationScoped import jakarta.inject.Inject @ApplicationScoped class MessageHandler { @Inject lateinit var processor: RequestScopedProcessor @ActivateRequestContext fun handle(message: String) { processor.process(message) } }
@ActivateRequestContextが効かないパターン
(1) CompletableFuture
@ActivateRequestContextが有効なのは、アノテーションが付いたメソッドを実行しているスレッドのみなので、別スレッドに処理を渡した瞬間Request Contextは伝播しない。
@ActivateRequestContext fun runAsync() { CompletableFuture.runAsync { // Request Context は有効にならない println(requestInfo.requestId) } }
(2) Reactive Routes
quarkus-reactive-routes はJAX-RSよりも低レイヤー(Vert.xの RoutingContextを直接操作できる)で、非同期・リアクティブなHTTPルーティングを簡単に書くための仕組みだが、非同期境界(イベントループ -> ワーカースレッドなど)を越えるとRequest Contextが失われることがあるため、@RequestScopedや@ActivateRequestContextが意図通りに効かなくなる。
@ApplicationScoped class ReactiveHandler { @Inject lateinit var requestInfo: RequestInfo @Route(path = "/reactive") @ActivateRequestContext fun handle(rc: RoutingContext) { Uni.createFrom().item { // ❌ ContextNotActiveException が発生することがある requestInfo.requestId }.subscribe().with { rc.response().end(it) } } }
非同期・リアクティブな処理が前提の場合、@RequestScopedに依存した設計自体を見直し、必要な情報を明示的に引き回す設計を検討することも重要。