CDI を使用した Quarkus での依存性インジェクション: ArC、スコープ、修飾子、ビルドタイム インジェクション
依存関係の注入 (DI) は、最新のエンタープライズ Java アプリケーション開発の基盤です。長年にわたり、Java 開発者は Spring IoC (制御の反転) または Hibernate Weld や OpenWebBeans などの標準の Jakarta EE CDI (コンテキストと依存関係の挿入) コンテナーに依存して、コンポーネントのライフサイクルを分離し、サービスの依存関係を管理し、構成プロパティを挿入してきました。
しかし、エンタープライズ アプリケーションがクラウドネイティブ マイクロサービス、Docker と Kubernetes によるコンテナ化、およびサーバーレス実行モデル (AWS Lambda、Knative) に移行するにつれて、従来の依存関係注入コンテナーには、重いランタイム リフレクションと高いメモリ オーバーヘッドという重大な欠陥が露呈しました。
Quarkus とその軽量 CDI エンジン ArC を登場させます。 Kubernetes と GraalVM のネイティブ コンパイル用にゼロから構築された Quarkus は、リフレクション、クラスパス スキャン、Bean グラフのワイヤリングをアプリケーションの実行時からビルド時に移行することで、依存関係の注入を再定義します。
この詳細ガイドでは、CDI を使用した Quarkus での依存性注入について詳しく説明し、ArC コンテナの内部機構、CDI スコープ、カスタム修飾子、プロデューサー、インターセプター、イベント駆動型オブザーバー、プログラムによるルックアップ、および高パフォーマンスのクラウドネイティブ Java アプリケーションを作成するためのベスト プラクティスについて説明します。
1. ArC を理解する: Quarkus ビルドタイム CDI エンジン
標準の Jakarta EE CDI コンテナは、アプリケーションの起動時に完全に動作します。レガシー アプリケーションが起動すると、CDI コンテナはクラスパス内のすべての JAR をスキャンし、メタデータを解析し、Java Reflection (java.lang.reflect) を使用して注釈を検査し、メモリ内に動的プロキシを生成し、依存関係グラフを構築します。この動的な反映により、大量の RAM (メタスペース) が消費され、コールド スタート ブート時間が増加します。
Quarkus は、ArC (Jakarta CDI 4.0 仕様に基づくビルドタイム CDI 実装) を使用してこのボトルネックを解決します。
ArC と標準 CDI の違い:
- ビルド時の拡張:
mvn packageまたは./gradlew build中に、ArC はすべてのアプリケーション Bean をスキャンし、注入ポイントを検証し、依存関係グラフを事前構築します。 - Ahead-Of-Time (AOT) バイトコード生成: ArC は、インスタンス化、Bean プロキシ、インターセプター用の直接 Java バイトコードを前もって生成し、実行時のリフレクションを排除します。
- 未使用の Bean プルーニング (デッド コードの除去): 動的に挿入または参照されない CDI Bean はコンパイル中に識別され、最終バイナリから自動的に削除され、メモリ フットプリントが削減されます。
- GraalVM ネイティブ イメージの最適化: Bean ワイヤリングはビルド時に解決されるため、Quarkus アプリケーションはリフレクション構成ヒントなしでネイティブ実行可能ファイルにシームレスにコンパイルされます。
2. CDI Bean のスコープとライフサイクル管理
CDI では、スコープ によって Bean インスタンスのライフサイクルと可視性が決まります。 Quarkus ArC は、ビルド時の最適化を備えた標準の Jakarta CDI スコープをサポートします。
Quarkus のプライマリ CDI スコープ:
| スコープの注釈 | ライフサイクルと動作 | プロキシの作成 | ベストユースケース |
|---|---|---|---|
@ApplicationScoped |
アプリケーションごとに 1 つのインスタンス。最初の呼び出し時の遅延初期化。すべてのスレッド間で共有される状態。 | 生成されたクライアント プロキシ | ステートレス サービス、リポジトリ、構成。 |
@RequestScoped |
単一の HTTP リクエストまたはイベント呼び出しにバインドされたインスタンス。リクエストが完了すると破棄されます。 | 生成されたクライアント プロキシ | ユーザーセッション、リクエストコンテキストデータ、セキュリティトークン。 |
@依存 |
何も指定されていない場合のデフォルトのスコープ。インスタンスは、注入元の Bean の存続期間に直接関連付けられます。 | プロキシなし (直接インスタンス) | ヘルパー ユーティリティ クラス、一時的な状態ホルダー。 |
@シングルトン |
アプリケーションごとに 1 つのインスタンス。積極的または遅延的にインスタンス化されます。クライアント プロキシ ラッパーはありません。 | プロキシなし (直接インスタンス) | クライアント プロキシのオーバーヘッドを回避する必要がある高パフォーマンスのシングルトン。 |
@取り外し不可 |
ビルド時の最適化中に Bean を削除しないよう ArC に指示するマーカー アノテーション。 | スコープに依存 | 動的検索ターゲットまたはリフレクション参照 Bean。 |
コード例: スコープ付き Bean の宣言
package com.ghaznix.services;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.context.RequestScoped;
import java.util.UUID;
@ApplicationScoped
public class OrderProcessingService {
public String processOrder(String orderId) {
return "Order " + orderId + " processed successfully.";
}
}
@RequestScoped
public class RequestContextHolder {
private final String requestId = UUID.randomUUID().toString();
public String getRequestId() {
return requestId;
}
}
3. Quarkus での依存性注入パターン
Quarkus は、標準の jakarta.inject.Inject アノテーションを使用して複数の注入スタイルをサポートします。
A. コンストラクターのインジェクション (推奨されるベスト プラクティス)
コンストラクター インジェクションは不変性 (final フィールド) を保証し、CDI コンテナーを必要とせずに単体テストを簡素化し、依存関係を明示します。
package com.ghaznix.resources;
import com.ghaznix.services.OrderProcessingService;
import jakarta.inject.Inject;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
@Path("/orders")
public class OrderResource {
private final OrderProcessingService orderService;
// In Quarkus, if a class has a single constructor, @Inject is OPTIONAL!
@Inject
public OrderResource(OrderProcessingService orderService) {
this.orderService = orderService;
}
@POST
@Path("/{id}")
public String checkout(@PathParam("id") String id) {
return orderService.processOrder(id);
}
}
プロのヒント: Quarkus ArC では、Bean に 1 つのコンストラクターしかない場合、
@Injectアノテーションを完全に省略できます。 ArC はこれをインジェクション コンストラクターとして自動的に認識します。
B. フィールド注入
フィールド挿入は簡潔ですが、フィールドが変更可能になり、スタンドアロンの単体テストが複雑になります。
@ApplicationScoped
public class NotificationService {
@Inject
EmailClient emailClient; // Field injection
}
4. 修飾子と複数の実装の曖昧さの解消
複数の Bean が同じインターフェイスを実装している場合、CDI はビルド時にあいまいな依存関係例外をスローします。 Quarkus は 修飾子 を使用して、ターゲット Bean 実装を安全に解決します。
カスタム修飾子の定義
package com.ghaznix.payment;
import jakarta.inject.Qualifier;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
@Qualifier
@Retention(RUNTIME)
@Target({TYPE, METHOD, FIELD, PARAMETER})
public @interface PaymentType {
Mode value();
enum Mode {
CREDIT_CARD,
PAYPAL,
CRYPTO
}
}
修飾された Bean の実装
public interface PaymentProcessor {
void process(double amount);
}
@ApplicationScoped
@PaymentType(PaymentType.Mode.CREDIT_CARD)
public class CreditCardProcessor implements PaymentProcessor {
@Override
public void process(double amount) {
System.out.println("Processing Credit Card payment: $" + amount);
}
}
@ApplicationScoped
@PaymentType(PaymentType.Mode.PAYPAL)
public class PayPalProcessor implements PaymentProcessor {
@Override
public void process(double amount) {
System.out.println("Processing PayPal payment: $" + amount);
}
}
修飾された Bean の注入
@ApplicationScoped
public class CheckoutService {
private final PaymentProcessor creditCardProcessor;
private final PaymentProcessor payPalProcessor;
@Inject
public CheckoutService(
@PaymentType(PaymentType.Mode.CREDIT_CARD) PaymentProcessor creditCardProcessor,
@PaymentType(PaymentType.Mode.PAYPAL) PaymentProcessor payPalProcessor) {
this.creditCardProcessor = creditCardProcessor;
this.payPalProcessor = payPalProcessor;
}
}
5. プロデューサーとディスポーザー: サードパーティのオブジェクトの挿入
場合によっては、@ApplicationScoped アノテーションが付けられていないサードパーティ ライブラリ (AWS SDK クライアント、Redis 接続、カスタム構成 Bean など) からクラスを注入する必要があることがあります。 Quarkus は、プロデューサー メソッド と ディスポーザー を使用してこれを処理します。
package com.ghaznix.config;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.inject.Disposes;
import jakarta.enterprise.inject.Produces;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
@ApplicationScoped
public class DatabaseConnectionProducer {
@Produces
@ApplicationScoped
public Connection createDatabaseConnection() throws SQLException {
return DriverManager.getConnection("jdbc:postgresql://localhost:5432/mydb", "user", "pass");
}
// Disposer method called automatically when bean is destroyed
public void closeConnection(@Disposes Connection connection) throws SQLException {
if (connection != null && !connection.isClosed()) {
connection.close();
}
}
}
6. CDI インターセプター: アスペクト指向プログラミング (AOP)
インターセプタを使用すると、ビジネス ロジックを汚染することなく、横断的な懸念事項 (ロギング、監査証跡、メトリクス、レート制限) を Bean メソッドに付加できます。
ステップ 1: インターセプター バインディングを定義する
package com.ghaznix.interceptors;
import jakarta.interceptor.InterceptorBinding;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
@InterceptorBinding
@Retention(RUNTIME)
@Target({TYPE, METHOD})
public @interface LogExecutionTime {
}
ステップ 2: インターセプター ロジックの実装
package com.ghaznix.interceptors;
import jakarta.annotation.Priority;
import jakarta.interceptor.AroundInvoke;
import jakarta.interceptor.Interceptor;
import jakarta.interceptor.InvocationContext;
@LogExecutionTime
@Interceptor
@Priority(Interceptor.Priority.APPLICATION)
public class LogExecutionTimeInterceptor {
@AroundInvoke
public Object logInvocation(InvocationContext context) throws Exception {
long startTime = System.currentTimeMillis();
try {
return context.proceed();
} finally {
long duration = System.currentTimeMillis() - startTime;
System.out.printf("[AUDIT] Method %s executed in %d ms%n",
context.getMethod().getName(), duration);
}
}
}
ステップ 3: ターゲット Bean にアノテーションを付ける
@ApplicationScoped
public class AnalyticsService {
@LogExecutionTime
public void computeAggregates() {
// Business computation
}
}
Quarkus はビルド時に静的インターセプター プロキシを生成するため、Spring Boot の動的 CGLIB ランタイム プロキシと比較して、メソッド呼び出しのオーバーヘッドは事実上ゼロです。
7. CDI イベントを使用したイベント駆動型アーキテクチャ
CDI は、分離されたインプロセス通信のための組み込みの同期および非同期イベント発行を提供します。
イベントの公開と監視
// 1. Define Event Payload
public record OrderCreatedEvent(String orderId, double totalAmount) {}
// 2. Publish Event
@ApplicationScoped
public class OrderService {
@Inject
Event<OrderCreatedEvent> orderEventPublisher;
public void createOrder(String orderId, double amount) {
// Create order logic...
orderEventPublisher.fire(new OrderCreatedEvent(orderId, amount));
}
}
// 3. Observe Event Synchronously or Asynchronously
@ApplicationScoped
public class InventoryObserver {
public void onOrderCreated(@Observes OrderCreatedEvent event) {
System.out.println("Updating inventory for order: " + event.orderId());
}
public void onOrderCreatedAsync(@ObservesAsync OrderCreatedEvent event) {
System.out.println("Async audit log for order: " + event.orderId());
}
}
アプリケーションのライフサイクル イベント
Quarkus は、アプリケーションの起動時または終了時に初期化コードを実行するための起動イベントとシャットダウン イベントを公開します。
@ApplicationScoped
public class ApplicationLifecycleBean {
void onStart(@Observes StartupEvent ev) {
System.out.println("Quarkus Application starting up...");
}
void onStop(@Observes ShutdownEvent ev) {
System.out.println("Quarkus Application shutting down...");
}
}
8. Instance<T> および Arc.container() を使用したプログラムによる Bean ルックアップ
ターゲット実装を実行時 (または従来の非 CDI インスタンス内) で動的に選択する必要がある場合は、Instance<T> または Quarkus Arc.container() 静的ルックアップを使用します。
@ApplicationScoped
public class DynamicPluginRegistry {
@Inject
Instance<PaymentProcessor> processors;
public void executeAll(double amount) {
for (PaymentProcessor processor : processors) {
processor.process(amount);
}
}
}
9. アーキテクチャの比較: Quarkus ArC、Spring IoC、Weld
| 特徴 | クォーカスArC | Spring IoCコンテナ | 標準ジャカルタ溶接 |
|---|---|---|---|
| 依存関係の解決 | ビルド時間 (AOT) | ランタイムリフレクション | ランタイムリフレクション |
| メタスペース RAM オーバーヘッド | 非常に低い (メタデータの場合は約 0MB) | 高 (動的プロキシ クラス キャッシュ) | 中程度から高程度 |
| 起動時間 | ミリ秒 (< 0.05 秒) | 秒 (2.5秒~8秒) | 秒(1.5秒~4秒) |
| GraalVM ネイティブ サポート | すぐに使えるネイティブ | Spring ネイティブ ヒントが必要 | 複雑な構成 |
| 未使用豆の剪定 | ビルド時の自動プルーニング | クラスパスに保持される | クラスパスに保持される |
| 単一コンストラクターのインジェクション | 暗黙的 (注釈は必要ありません) | サポートされています | @Inject が必要です |
10. @QuarkusTest および @InjectMock を使用した CDI Bean の単体テスト
Quarkus では、JUnit 5 および RestAssured を使用して CDI Bean をシームレスにテストできます。 @InjectMock を使用して、テスト中に依存関係 Bean を Mockito モックに自動的に置き換えます。
package com.ghaznix;
import com.ghaznix.services.OrderProcessingService;
import io.quarkus.test.InjectMock;
import io.quarkus.test.junit.QuarkusTest;
import jakarta.inject.Inject;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
@QuarkusTest
public class OrderServiceTest {
@Inject
OrderService orderService;
@InjectMock
OrderProcessingService orderProcessingService;
@Test
public void testOrderCheckout() {
Mockito.when(orderProcessingService.processOrder("ORD-101"))
.thenReturn("Mocked Success");
String result = orderService.checkout("ORD-101");
Assertions.assertEquals("Mocked Success", result);
}
}
##11.結論
CDI (ArC) を使用した Quarkus の依存性注入は、エンタープライズ Java 設計における画期的な変化を示します。 Quarkus は、Bean メタデータのスキャン、プロキシ生成、および注入グラフの解決をコンパイル時に移すことで、超音速の起動速度、亜原子的なメモリ フットプリント、シームレスな GraalVM ネイティブ バイナリ サポートを実現します。
Spring Boot から移行する場合でも、Kubernetes 上でグリーンフィールドのクラウドネイティブ マイクロサービスを構築する場合でも、Quarkus CDI をマスターすることで、最新のソフトウェア アーキテクチャに必要なパフォーマンスと開発者の生産性を確保できます。
Tags
Empower Your Digital Presence & Workflows
Explore top-tier tools built by Ghaznix to streamline your links, surveys, and brand growth.