The English version of quarkus.io is the official project site. Translated sites are community supported on a best-effort basis.

コンテキストと依存性注入(CDI)の紹介

このガイドでは、 Jakarta Contexts and Dependency Injection 4.0 仕様に基づくQuarkusプログラミングモデルの基本原則について説明します。

1. OK、簡単なことから始めましょう。Beanとは何でしょうか?

Beanは コンテナーで管理された オブジェクトです。依存性の注入、ライフサイクルコールバック、インターセプターなどの基本的なサービスのセットをサポートしています。

2. ちょっと待ってください。「コンテナーで管理された」とはどういう意味ですか?

簡単に言えば、オブジェクトインスタンスのライフサイクルを直接制御することはできません。その代わりに、アノテーションや設定などの宣言的な手段でライフサイクルに影響を与えることができます。コンテナーはアプリケーションが動作する 環境 です。コンテナーは、Beanのインスタンスを作成したり破棄したり、指定されたコンテキストにインスタンスを関連付けたり、他のBeanに注入したりします。

3. 何に使うと適切ですか?

アプリケーション開発者は、「どこで、どのように」ではなく、ビジネスロジックに集中して、すべての依存関係を持つ完全に初期化されたコンポーネントを得ることができます。

制御の反転 (Inversion of Control, IoC )というプログラミングの原理を聞いたことがあると思います。依存性注入はIoCの実装技術の一つです。

4. Beanはどんな形をしているのでしょうか?

Beanにはいくつかの種類があります。一番多いのは、クラスベースのBeanです。

シンプルなBeanの例
import jakarta.inject.Inject;
import jakarta.enterprise.context.ApplicationScoped;
import org.eclipse.microprofile.metrics.annotation.Counted;

@ApplicationScoped (1)
public class Translator {

    @Inject
    Dictionary dictionary; (2)

    @Counted  (3)
    String translate(String sentence) {
      // ...
    }
}
1 これはスコープアノテーションです。これはコンテナーに、Beanのインスタンスをどのコンテキストに関連付けるかを伝えます。この特定のケースでは、 単一のBeanインスタンス がアプリケーション用に作成され、 Translator の注入を行う他の全てのBeanによって使用されます。
2 これはフィールド注入ポイントです。 TranslatorDictionary Beanに依存していることをコンテナーに伝えます。マッチするBeanがない場合、ビルドは失敗します。
3 これはインターセプター結合アノテーションです。この場合、アノテーションは MicroProfile Metrics から来ています。関連するインターセプターは呼び出しをインターセプトし、関連するメトリクスを更新します。 インターセプターについては後述します。

5. いいですね。依存関係の解決方法はどのように動作しますか?名前も識別子も見当たりません。

良い質問ですね。CDIでは、Beanをインジェクションポイントにマッチングするプロセスは タイプセーフ です。各Beanは、Beanタイプのセットを宣言します。上の例では、 Translator Beanには、 Translatorjava.lang.Object の 2 つのBeanタイプがあります。その後、Beanが 必要な型 にマッチするBean型を持ち、 必要な すべての 修飾子を 持っている場合、Beanはインジェクションポイントに代入可能です。この後、修飾子について説明します。今のところ、上記のBeanが Translatorjava.lang.Object のタイプのインジェクションポイントに代入可能であることを知っていれば十分です。

6. ふむ、ちょっと待ってください。複数のBeanが同じ型を宣言した場合はどうなるのでしょうか?

シンプルなルールがあります: 正確に1つのBeanがインジェクションポイントに割り当て可能でなければならず、そうでなければビルドは失敗します。 割り当て可能なBeanがない場合、ビルドは UnsatisfiedResolutionException で失敗します。複数のBeanが割り当て可能な場合、ビルドは AmbiguousResolutionException で失敗します。これは非常に便利です。コンテナーがどのインジェクションポイントに対しても明確な依存関係を見つけることができない場合、アプリケーションは早く失敗するからです。

実行時に曖昧さを解決するために、 jakarta.enterprise.inject.Instance を介してプログラム的な検索を使用することができ、さらに、指定された型を実装するすべてのBeanを反復処理することができます:

public class Translator {

    @Inject
    Instance<Dictionary> dictionaries; (1)

    String translate(String sentence) {
      for (Dictionary dict : dictionaries) { (2)
         // ...
      }
    }
}
1 このインジェクションポイントは、 Dictionary 型を実装した複数のBeanがあっても、曖昧な依存関係にはなりません。
2 jakarta.enterprise.inject.Instance は、 Iterable を拡張します。

7. セッターやコンストラクタのインジェクションは使えますか?

はい、使用できます。実際、CDIでは "セッター・インジェクション "は、より強力な イニシャライザー・メソッド に取って代わられています。 イニシャライザは複数のパラメータを受け取ることができ、JavaBeanの命名規則に従う必要はありません。

初期化とコンストラクタのインジェクション例
@ApplicationScoped
public class Translator {

    private final TranslatorHelper helper;

    Translator(TranslatorHelper helper) { (1)
       this.helper = helper;
    }

    @Inject (2)
    void setDeps(Dictionary dic, LocalizationService locService) { (3)
      / ...
    }
}
1 これはコンストラクタのインジェクションです。実際には、このコードは通常のCDI実装では動作しません。通常のスコープを持つBeanは常にno-argsコンストラクタを宣言しなければならず、Beanのコンストラクタは @Inject でアノテーションされなければなりません。しかし、Quarkusでは、no-argsコンストラクタが存在しないことを検出し、バイトコードに直接「追加」します。また、コンストラクタが1つしかない場合は、 @Inject を追加する必要はありません。
2 イニシャライザメソッドには @Inject をアノテーションしなければなりません。
3 イニシャライザは複数のパラメーターを受け付けることができ、それぞれがインジェクションポイントとなります。

8. 修飾子の話をしましたか?

修飾子 は,コンテナが同じ型を実装するBeanを区別するのに役立つアノテーションです。 すでに述べたように、Beanは、必要な修飾子がすべてあれば注入ポイントに割り当て可能です。注入ポイントで修飾子を宣言しない場合、 @Default 修飾子が仮定されます。

修飾子型は、 @Retention(RUNTIME) として定義され、 @jakarta.inject.Qualifier メタアノテーションでアノテーションされた Java アノテーションです:

修飾子の例
@Qualifier
@Retention(RUNTIME)
@Target({METHOD, FIELD, PARAMETER, TYPE})
public @interface Superior {}

Beanの修飾子は、Beanクラスやプロデューサのメソッドやフィールドに修飾子タイプをアノテーションすることで宣言されます。

カスタム修飾子を持つ Bean の例
@Superior (1)
@ApplicationScoped
public class SuperiorTranslator extends Translator {

    String translate(String sentence) {
      // ...
    }
}
1 @Superior修飾子アノテーション です。

このBeanは @Inject @Superior Translator@Inject @Superior SuperiorTranslator には割り当てられますが、 @Inject Translator には割り当てられません。その理由は、 @Inject Translator はタイプセーフ解決の際に自動的に @Inject @Default Translator に変換されるからです。また、私たちの SuperiorTranslator@Default を宣言していないので、元の Translator Beanだけが代入可能です。

9. 良いですね。Beanスコープとは何ですか?

Beanのスコープはインスタンスのライフサイクルを決定します。

すべてのBeanは、正確に1つのスコープを持っています。

10. 実際にQuarkusアプリケーションで使用できるスコープは何ですか?

jakarta.enterprise.context.ConversationScoped 以外の仕様で言及されているすべてのビルトインスコープを使用することができます。

アノテーション 説明

@jakarta.enterprise.context.ApplicationScoped

単一のBeanインスタンスがアプリケーションに使用され、すべてのインジェクションポイント間で共有されます。 インスタンス は遅延的に生成されます。

@jakarta.inject.Singleton

クライアントプロキシーを使用しないことを除いて、 @ApplicationScoped と同じです。インスタンスは、@Singleton Beanに解決するインジェクションポイントがインジェクションされたときに生成されます。

@jakarta.enterprise.context.RequestScoped

Beanインスタンスは、現在の リクエスト (通常はHTTPリクエスト)に関連付けられています。

@jakarta.enterprise.context.Dependent

これは疑似スコープです。インスタンスは共有されておらず、すべての注入ポイントは依存Beanの新しいインスタンスをスポーンします。依存Beanのライフサイクルは、それを注入するBeanに拘束されています。

@jakarta.enterprise.context.SessionScoped

このスコープは jakarta.servlet.http.HttpSession オブジェクトによってバックアップされています。 quarkus-undertow エクステンションを使用している場合のみ利用可能です。

Quarkusのエクステンションによって提供される他のカスタムスコープもあります。例えば、 quarkus-narayana-jtajakarta.transaction.TransactionScoped を提供します。

11. @ApplicationScoped@Singleton は非常に似ているように見えます。Quarkusアプリケーションにはどれを選べばいいのでしょうか?

それは場合によりけりです ;-)。

@Singleton Beanは クライアントプロキシー を持たないので、Beanがインジェクトされるとインスタンスは 熱心に(eagerly)生成され ます。対照的に、 @ApplicationScoped Beanのインスタンスは 怠惰(lazily)に生成されます。

さらに、クライアントプロキシーはメソッドの呼び出しを委譲するだけなので、注入された @ApplicationScoped Bean のフィールドを直接読み書きしてはいけません。注入された @Singleton のフィールドは安全に読み書きすることができます。

@Singleton は少しだけ良いパフォーマンスを発揮します。というのも、回り道がないからです(コンテキストから現在のインスタンスに委譲するプロキシがない)。

一方、 QuarkusMock を使って @Singleton Beanをモックすることはできません。

@ApplicationScoped Beanは、実行時に破棄して再作成することもできます。既存のインジェクションポイントは、インジェクションされたプロキシーが現在のインスタンスにデリゲートするので、単に機能します。

したがって、 @Singleton を使用する正当な理由がない限り、デフォルトで @ApplicationScoped を使用することをお勧めします。

12. クライアントプロキシーの概念が理解できません。

実際、 クライアントプロキシ は把握しにくいかもしれませんが、いくつかの便利な機能を提供します。 クライアントプロキシは,基本的に,すべてのメソッド呼び出しをターゲットBeanインスタンスに委譲するオブジェクトです。 これは, io.quarkus.arc.ClientProxy を実装し,Beanクラスを拡張するコンテナ構成体です。

クライアントプロキシーはメソッドの呼び出しをデリゲートするだけです。そのため、通常のスコープされたBeanのフィールドを読み書きしてはいけません。
生成されたクライアントプロキシーの例
@ApplicationScoped
class Translator {

    String translate(String sentence) {
      // ...
    }
}

// The client proxy class is generated and looks like...
class Translator_ClientProxy extends Translator { (1)

    String translate(String sentence) {
      // Find the correct translator instance...
      Translator translator = getTranslatorInstanceFromTheApplicationContext();
      // And delegate the method invocation...
      return translator.translate(sentence);
    }
}
1 Translator Beanの コンテキストインスタンス への直接参照ではなく, Translator_ClientProxy インスタンスが常に注入される。

クライアントプロキシーは、以下のことを可能にします。

  • 遅延インスタンス化 - メソッドがプロキシーに呼び出されるとインスタンスが生成されます。

  • 「狭い」スコープのBeanを「広い」スコープのBeanに注入する機能、すなわち、 @RequestScoped Beanを @ApplicationScoped Beanに注入することができます。

  • 依存関係グラフの円形の依存関係。循環的な依存関係を持つことは、しばしば再設計を検討すべきであることを示していますが、時には避けられないこともあります。

  • まれなケースでは、手動でBeanを破棄するのが現実的です。直接参照を注入すると、古くなったBeanのインスタンスになってしまいます。

13. そうですか。Beanは何種類かあるんですよね?

はい、一般的には以下に区別しています:

  1. クラスBean

  2. プロデューサーメソッド

  3. プロデューサーフィールド

  4. 合成Bean

合成Beanは通常、エクステンションによって提供されます。そのため、このガイドではそれらを取り上げません。

プロデューサ・メソッドとフィールドは、Beanのインスタンス化を追加で制御する必要がある場合に便利です。また、サードパーティのライブラリを統合する際に、クラスソースを制御できず、追加のアノテーションなどを追加できない場合にも便利です。

プロデューサーの例
@ApplicationScoped
public class Producers {

    @Produces (1)
    double pi = Math.PI; (2)

    @Produces (3)
    List<String> names() {
       List<String> names = new ArrayList<>();
       names.add("Andy");
       names.add("Adalbert");
       names.add("Joachim");
       return names; (4)
    }
}

@ApplicationScoped
public class Consumer {

   @Inject
   double pi;

   @Inject
   List<String> names;

   // ...
}
1 コンテナーは,フィールドアノテーションを分析して,Beanのメタデータを構築します。 は,Beanの型の集合を構築するために使用されます。この場合、 doublejava.lang.Object .スコープアノテーションは宣言されていないので、デフォルトは @Dependent になります。
2 コンテナーは、Beanのインスタンスを作成するときにこのフィールドを読みます。
3 コンテナーは,メソッドのアノテーションを解析して,Beanのメタデータを構築します。戻り値の は、Bean型のセットを構築するために使用されます。この場合、 List<String>Collection<String>Iterable<String>java.lang.Object となります。スコープアノテーションは宣言されていないので、デフォルトは @Dependent となります。
4 コンテナーは、Beanのインスタンスを作成する際にこのメソッドを呼び出します。

プロデューサーについては他にもあります。修飾子を宣言したり、プロデューサーメソッドのパラメーターに依存性を注入したりすることができます。プロデューサについては、例えば Weld のドキュメントを参照してください。

14. OK、インジェクションは便利ですね。他にはどんなサービスが提供されていますか?

14.1. ライフサイクルコールバック

Beanクラスは、ライフサイクル @PostConstruct@PreDestroy コールバックを宣言することができます。

ライフサイクルコールバックの例
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;

@ApplicationScoped
public class Translator {

    @PostConstruct (1)
    void init() {
       // ...
    }

    @PreDestroy (2)
    void destroy() {
      // ...
    }
}
1 このコールバックは、Beanインスタンスがサービスに投入される前に呼び出されます。ここでいくつかの初期化を行うのが安全です。
2 このコールバックは、Beanインスタンスが破棄される前に呼び出されます。ここでいくつかのクリーンアップタスクを実行しても安全です。
コールバック内のロジックを「副作用なし」に保つこと、つまり、コールバック内で他のBeanを呼び出すことは避けるべきです。

14.2. インターセプター

インターセプターは、横断的な問題をビジネス・ロジックから分離するために使用されます。基本的なプログラミングモデルとセマンティクスを定義した Java Interceptors という別の仕様があります。

シンプルなインターセプターバインディングの例
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import jakarta.interceptor.InterceptorBinding;

@InterceptorBinding (1)
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD, ElementType.CONSTRUCTOR}) (2)
@Inherited (3)
public @interface Logged {
}
1 これは、インターセプター バインディング アノテーションです。 使用方法については、次の例を参照してください。
2 インターセプター バインディング アノテーションは常にインターセプターの型に付けられ、ターゲットの型またはメソッドに付けることもできます。
3 インターセプター バインディングはしばしば @Inherited ですが、そうでなければならない訳ではありません。
シンプルなインターセプターの例
import jakarta.annotation.Priority;
import jakarta.interceptor.AroundInvoke;
import jakarta.interceptor.Interceptor;
import jakarta.interceptor.InvocationContext;

@Logged (1)
@Priority(2020) (2)
@Interceptor (3)
public class LoggingInterceptor {

   @Inject (4)
   Logger logger;

   @AroundInvoke (5)
   Object logInvocation(InvocationContext context) {
      // ...log before
      Object ret = context.proceed(); (6)
      // ...log after
      return ret;
   }

}
1 インターセプターバインディングアノテーションは、インターセプターをBeanにバインドするために使用されます。次の例のように Beanクラスに @Logged をアノテーションするだけです。
2 Priority はインターセプターを有効にし、インターセプターの順序に影響を与えます。優先度の値が小さいインターセプターが最初に呼び出されます。
3 インターセプターコンポーネントをマークします。
4 インターセプターは、依存性注入の対象となる場合があります。
5 AroundInvoke とは、ビジネスの方法に口出しする方法を指します。
6 インターセプターチェーンの次のインターセプターに進むか、インターセプターされたビジネスメソッドを呼び出します。
インターセプタのインスタンスは、インターセプトするBeanのインスタンスに依存するオブジェクトです。
シンプルなインターセプターの使用例
import jakarta.enterprise.context.ApplicationScoped;

@Logged (1) (2)
@ApplicationScoped
public class MyService {
   void doSomething() {
       ...
   }
}
1 インターセプター・バインディング・アノテーションをBeanクラスに付けると、すべてのビジネス・メソッドがインターセプトされるようになります。 アノテーションは個々のメソッドに付けることもでき、その場合、アノテーションされたメソッドのみがインターセプトされます。
2 @Logged アノテーションは @Inherited であることを思い出してください。 MyService を継承する Bean クラスがあれば、LoggingInterceptor も適用されます。

14.3. デコレーター

デコレーターはインターセプターと似ていますが、ビジネスセマンティクスを持つインターフェイスを実装しているため、ビジネスロジックを実装することができます。

シンプルなデコレーターの例
import jakarta.decorator.Decorator;
import jakarta.decorator.Delegate;
import jakarta.annotation.Priority;
import jakarta.inject.Inject;
import jakarta.enterprise.inject.Any;

public interface Account {
   void withdraw(BigDecimal amount);
}

@Priority(10) (1)
@Decorator (2)
public class LargeTxAccount implements Account { (3)

   @Inject
   @Any
   @Delegate
   Account delegate; (4)

   @Inject
   LogService logService; (5)

   void withdraw(BigDecimal amount) {
      delegate.withdraw(amount); (6)
      if (amount.compareTo(1000) > 0) {
         logService.logWithdrawal(delegate, amount);
      }
   }

}
1 @Priority はデコレーターを有効にします。優先度の値が小さいインターセプターが最初に呼び出されます。
2 @Decorator はデコレーターコンポーネントをマークします。
3 装飾された型のセットには、 java.io.Serializable を除く、Java インターフェースであるすべての Bean 型が含まれます。
4 各デコレーターは、正確に1つの デリゲート・インジェクション・ポイント を宣言する必要があります。デコレーターは、このデリゲート・インジェクション・ポイントに割り当て可能なBeanに適用されます。
5 デコレーターは、他のBeanを注入することができます。
6 デコレーターは、デリゲートオブジェクトの任意のメソッドを呼び出すことができます。そして、コンテナは、チェーンの次のデコレーターか、インターセプトされたインスタンスのビジネスメソッドを呼び出します。
デコレーターのインスタンスは、インターセプトするBeanのインスタンスに依存するオブジェクトです。つまり、インターセプトされたBeanごとに新しいデコレーターインスタンスが作成されます。

14.4. イベントとオブザーバー

Beanは、完全に分離された方法で相互作用するために、イベントを生成したり消費したりすることもできます。任意の Java オブジェクトをイベントのペイロードとして使用できます。オプションの修飾子は、トピックセレクタとして機能します。

シンプルなイベントの例
class TaskCompleted {
  // ...
}

@ApplicationScoped
class ComplicatedService {

   @Inject
   Event<TaskCompleted> event; (1)

   void doSomething() {
      // ...
      event.fire(new TaskCompleted()); (2)
   }

}

@ApplicationScoped
class Logger {

   void onTaskCompleted(@Observes TaskCompleted task) { (3)
      // ...log the task
   }

}
1 jakarta.enterprise.event.Event は、イベントの発行に使われています。
2 イベントを同期的に発生させます。
3 このメソッドは、 TaskCompleted イベントが発生したときに通知されます。
イベント/オブザーバーの詳細については、 Weld ドキュメントをご覧ください。

15. まとめ

このガイドでは、 Jakarta Contexts and Dependency Injection 4.0 仕様に基づく Quarkus プログラミングモデルの基本的なトピックについて説明しました。 QuarkusはCDI Lite仕様を実装していますが、CDI Full仕様は実装していません。 サポートされる機能と制限事項のリスト も参照してください。 また、 非標準 の機能や Quarkus固有のAPI も数多くあります。

Quarkus固有の機能や制限について詳しく知りたい場合は、Quarkus CDIリファレンスガイド があります。また、より複雑なトピックに精通するために、 CDI仕様書Weldドキュメント (WeldはCDIリファレンス実装です)を読むことをお勧めします。

関連コンテンツ