プロンプトとユースケース
Javaサービスでは、すべてのメソッドにパラメータを追加したり、再利用されるプールを通じて状態をリークさせたりすることなく、トレースID、テナント、またはセキュリティプリンシパルを深いコールバックや仮想スレッドに渡す必要があります。Java 25のScopedValueのレキシカルスコープ、バインディングとリバインディング、Carrier、仮想スレッド、および構造化並行性について説明し、ThreadLocalからの移行の境界を定義してください。
面接官がテストしていること
- 暗黙的コンテキストの可視範囲とライフサイクルの説明。
ScopedValueのバインディングが、run、call、またはwhereのスコープ内でのみ読み取り可能であることの理解。- イミュータブルなスナップショット伝播とミュータブルなスレッドローカル状態の区別。
- ネストされたバインディング、子タスク、例外、およびキャンセルの処理。
- セキュリティプリンシパル、ミュータブルなオブジェクト、およびプールの移行に伴うリスクの認識。
最初に明確にすべき質問
- コンテキストはリクエストスコープの読み取り専用の値ですか、それともスレッド間で書き戻す必要があるミュータブルな状態ですか?
- タスクは仮想スレッド、構造化並行性、または従来のスレッドプールを使用していますか?
- 子タスクはコンテキストを継承する必要がありますか?また、キーをオーバーライドすることは許可されますか?
- どのJDKバージョンがサポートされており、移行を段階的に行うことは可能ですか?
30秒での回答
リクエストコンテキストをイミュータブルな値としてモデル化し、ScopedValue.where(key, value).runまたはcallを使用してレキシカルスコープを作成します。深い階層のコードはキーを介して読み取りを行い、スコープを抜けるとバインディングは消滅します。ネストされたスコープでは一時的にリバインドできますが、Carrierはイミュータブルかつスレッドセーフです。これにより、仮想スレッドや構造化並行性のコードに、ミュータブルなThreadLocalよりも厳格なライフサイクルがもたらされますが、スコープをまたいで書き込む必要のある状態には適していません。移行中は明示的なパラメータと境界テストを維持してください。
詳細な解説、ステップバイステップ
1. コンテキストキーを定義する
privateなScopedValueキーを使用し、トレースID、テナントID、およびプリンシパルをイミュータブルなContextに配置します。ミュータブルなマップや書き込み可能なセッションオブジェクトを深い階層のコードに公開しないでください。
2. レキシカルスコープを作成する
static final ScopedValue<RequestContext> REQUEST = ScopedValue.newInstance();
ScopedValue.where(REQUEST, context).run(() -> handle(request));handle内の深い呼び出しは現在のバインディングを読み取ることができますが、スコープ終了後は可視ではなくなります。ライフサイクルはスレッドの回収ではなくコードブロックに従います。
3. 読み取りと非存在の処理
値が存在しないことが許容される場合はisBound()を使用し、明示的な内部デフォルト値にはorElseを使用します。セキュリティプリンシパルのような必須のコンテキストは、暗黙のうちに匿名として実行されるのを防ぐため、orElseThrowを使用する必要があります。
4. Carrierを説明する
ScopedValue.Carrierはイミュータブルでスレッドセーフなキー・バリューマッピングです。whereをチェーンすると新しいキャリアが返されます。複数のバインディングを組み立ててrunまたはcallを呼び出します。ミュータブルなキャリアを共有しないでください。
5. ネストされたリバインディングを処理する
内部のwhereは、同じキーに対して一時的に新しい値をバインドでき、その後は外部の値が復元されます。コールバック、例外ハンドラ、ログが誤ったテナントやプリンシパルを読み取らないよう、レビュー時にスコープツリーを描いて確認してください。
6. 仮想スレッドおよび構造化並行性と組み合わせる
レキシカルスコープ内で子タスクを作成し、意図したコンテキストのスナップショットを受け取れるようにします。キャンセルや例外による終了が発生しても、手動でのスレッドローカルのクリーンアップなしにスコープを終了できます。どの子タスクがキーをオーバーライドできるかをドキュメント化してください。
7. ThreadLocalと比較する
ThreadLocalは、スレッドの関連付けやミュータブルな書き込みが必要なレガシーAPIに適していますが、プールの再利用によりクリーンアップを忘れやすくなります。ScopedValueは読み取り専用で存続期間が短く、可視性が制限された用途に適しており、すべてのThreadLocal(特にコールバックをまたいで変更が必要な状態)を置き換えられるわけではありません。
8. 移行と可観測性を計画する
コンテキストスキーマ、許可されたリーダー、およびJDKマトリックスを定義し、古いコードを段階的に移行できるようにアダプタを使用します。暗黙的なコンテキストが見えないグローバル変数化しないよう、未バインドの障害、スレッド境界、ネストされたオーバーライド、およびリクエスト完了後に保持された参照を記録します。
トレードオフと境界
ScopedValueはバインディングの参照を渡します。値がミュータブルである場合、データ競合や権限の変更が発生する可能性があります。任意の新しいスレッドに対して自動的にビジネスセマンティクスを付与するわけではなく、明示的なパラメータ、認証ポリシー、またはキャンセルプロトコルを代替するものでもありません。明確なJDKベースラインと構造化並行性モデルを用いてJava 25 APIを採用し、古いリリースに対する互換性パスを維持してください。
ロールアウト計画と証拠
- 既存のThreadLocalキー、ライター、クリーンアップ、およびスレッド境界を棚卸しします。
- 読み取り専用のリクエストコンテキスト用にScopedValueキーとイミュータブルな型を作成します。
- リクエストのエントリポイントおよび構造化タスクの作成を
where(...).run/callでラップします。 - ネストされたリバインディング、例外、キャンセル、仮想スレッド、プールの再利用、および未バインドアクセスをテストします。
ScopedValue、Carrier、アクセス制御、および構造化並行性に関するOracleのJava 25 APIガイダンスをリリースの証拠として使用します。
よくある間違いとフォローアップ
間違い1:ScopedValueをミュータブルなグローバルとして扱う
キーは可視性を制御しますが、バインドされたオブジェクトは依然としてミュータブルである可能性があります。イミュータブルなコンテキストを使用し、書き込みを制限してください。
間違い2:スコープ境界を忘れる
スコープ終了後の読み取りは失敗するか、外部のバインディングを返します。エントリポイント、コールバック、および非同期タスクの作成を明示的に特定してください。
間違い3:すべてのThreadLocalを直接置き換える
スコープをまたぐ書き込みや古いJDKのサポートには異なる制約があります。使用状況を分類し、まずは読み取り専用のリクエストコンテキストから移行してください。
間違い4:すべてのスレッドが自動的に継承すると想定する
コンテキストの伝播は、タスク作成、エグゼキュータ、および構造化並行性を考慮して設計する必要があります。スレッド名は伝播の契約ではありません。
間違い5:プリンシパルのオーバーライドを無視する
ネストされたリバインディングによって監査IDが変わる可能性があります。プリンシパルをイミュータブルに保ち、必要な場合は必須とし、オーバーライドを監査してください。