ELW株式会社 テックブログ

リアルなログをそのままお届けします。

【Quarkus】RequestScoped備忘録

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の@RequestScopedCDI(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が開始されていない
  • にもかかわらず @RequestScoped Beanにアクセスしたため、例外が発生する。

@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-routesJAX-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に依存した設計自体を見直し、必要な情報を明示的に引き回す設計を検討することも重要。