コンテキストと依存性注入(CDI)の紹介
このガイドでは、 https://jakarta.ee/specifications/cdi/4.1/jakarta -cdi-spec-4.1.html[Jakarta コンテキストと依存性の注入 4.1、window="_blank"] 仕様に基づいた Quarkus プログラミングモデルの基本原則について説明します。 CDI リファレンスガイド では、Bean の検出、非標準機能、および設定プロパティーについて説明します。 CDI 統合ガイド には、一般的な CDI 関連の統合ユースケースとソリューションのサンプルコードの詳細が記載されています。
1. まず、シンプルなことから始めます。Beanとは何でしょうか?
Beanは コンテナーで管理された オブジェクトです。依存性の注入、ライフサイクルコールバック、インターセプターなどの基本的なサービスのセットをサポートしています。
2. ちょっと待ってください。「コンテナーで管理された」とはどういう意味ですか?
簡単に言えば、オブジェクトインスタンスのライフサイクルを直接制御することはできません。その代わりに、アノテーションや設定などの宣言的な手段でライフサイクルに影響を与えることができます。コンテナーはアプリケーションが動作する 環境 です。コンテナーは、Beanのインスタンスを作成したり破棄したり、指定されたコンテキストにインスタンスを関連付けたり、他のBeanに注入したりします。
3. 何に使うと適切ですか?
アプリケーション開発者は、「どこで、どのように」ではなく、ビジネスロジックに集中して、すべての依存関係を持つ完全に初期化されたコンポーネントを得ることができます。
制御の反転 (Inversion of Control, IoC )というプログラミングの原理を聞いたことがあると思います。依存性注入はIoCの実装技術の一つです。 |
4. 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 | これはフィールド注入ポイントです。 Translator が Dictionary Beanに依存していることをコンテナーに伝えます。マッチするBeanがない場合、ビルドは失敗します。 |
3 | これはインターセプター結合アノテーションです。この場合、アノテーションは MicroProfile Metrics から来ています。関連するインターセプターは呼び出しをインターセプトし、関連するメトリクスを更新します。<<interceptors,interceptors> については後述します。 |
5. いいですね。依存関係の解決方法はどのように動作しますか?名前も識別子も見当たりません。
良い質問ですね。CDIでは、Beanをインジェクションポイントにマッチングするプロセスは タイプセーフ です。各Beanは、Beanタイプのセットを宣言します。上の例では、 Translator
Beanには、 Translator
と java.lang.Object
の 2 つのBeanタイプがあります。その後、Beanが 必要な型 にマッチするBean型を持ち、 必要な すべての 修飾子を 持っている場合、Beanはインジェクションポイントに代入可能です。この後、修飾子について説明します。今のところ、上記のBeanが Translator
と java.lang.Object
のタイプのインジェクションポイントに代入可能であることを知っていれば十分です。
6. ふむ、ちょっと待ってください。複数のBeanが同じ型を宣言した場合はどうなるのでしょうか?
シンプルなルールがあります: 正確に1つのBeanがインジェクションポイントに割り当て可能でなければならず、そうでなければビルドは失敗します。 割り当て可能なBeanがない場合、ビルドは UnsatisfiedResolutionException
で失敗します。複数のBeanが割り当て可能な場合、ビルドは AmbiguousResolutionException
で失敗します。これは非常に便利です。コンテナーがどのインジェクションポイントに対しても明確な依存関係を見つけることができない場合、アプリケーションは早く失敗するからです。
実行時に曖昧さを解決するために、
|
7. セッターやコンストラクタのインジェクションは使えますか?
はい、できます。 実際、CDI では、セッターインジェクションは、より強力な https://jakarta.ee/specifications/cdi/4.1/jakarta -cdi-spec-4.1.html#initializer_methods[initializer methods、window="_blank"] に置き換えられています。 初期化子は複数のパラメーターを使用でき、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. 修飾子の話をしましたか?
https://jakarta.ee/specifications/cdi/4.1/jakarta -cdi-spec-4.1.html#qualifiers[修飾子、window="_blank"] は、コンテナーが同じタイプを実装する Bean を区別するのに役立つアノテーションです。
すでに述べたように、Bean は、必要な修飾子がすべて揃っている場合に注入ポイントに割り当てることができます。
注入ポイントで修飾子を宣言しない場合は、 @Default
修飾子が想定されます。
修飾子型は、 @Retention(RUNTIME)
として定義され、 @jakarta.inject.Qualifier
メタアノテーションでアノテーションされた Java アノテーションです:
@Qualifier
@Retention(RUNTIME)
@Target({METHOD, FIELD, PARAMETER, TYPE})
public @interface Superior {}
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だけが代入可能です。
10. 実際にQuarkusアプリケーションで使用できるスコープは何ですか?
jakarta.enterprise.context.ConversationScoped
以外の仕様で言及されているすべてのビルトインスコープを使用することができます。
アノテーション | 説明 |
---|---|
|
単一のBeanインスタンスがアプリケーションに使用され、すべてのインジェクションポイント間で共有されます。client proxy は遅延的に生成されます。 |
|
クライアントプロキシーを使用しないことを除いて、 |
|
Beanインスタンスは、現在の リクエスト (通常はHTTPリクエスト)に関連付けられています。 |
|
これは疑似スコープです。インスタンスは共有されておらず、すべての注入ポイントは依存Beanの新しいインスタンスをスポーンします。依存Beanのライフサイクルは、それを注入するBeanに拘束されています。 |
|
このスコープは |
Quarkusのエクステンションによって提供される他のカスタムスコープもあります。例えば、 quarkus-narayana-jta は jakarta.transaction.TransactionScoped を提供します。
|
11. @ApplicationScoped
と @Singleton
は非常に似ているように見えます。Quarkusアプリケーションにはどれを選べばいいのでしょうか?
それは場合によりけりです ;-)。
@Singleton
Bean には client proxy がないため、Bean が注入されるとインスタンスは 即座に作成 されます。対照的に、 @ApplicationScoped
Bean のインスタンスは 遅延作成 (つまり、
注入されたインスタンスに対してメソッドが初めて呼び出されるとき) されます。
さらに、クライアントプロキシーはメソッドの呼び出しを委譲するだけなので、注入された @ApplicationScoped
Bean のフィールドを直接読み書きしてはいけません。注入された @Singleton
のフィールドは安全に読み書きすることができます。
@Singleton
は少しだけ良いパフォーマンスを発揮します。というのも、回り道がないからです(コンテキストから現在のインスタンスに委譲するプロキシがない)。
一方、QuarkusMock を使用して @Singleton
Bean を模倣できません。
@ApplicationScoped
Beanは、実行時に破棄して再作成することもできます。既存のインジェクションポイントは、インジェクションされたプロキシーが現在のインスタンスにデリゲートするので、単に機能します。
したがって、 @Singleton
を使用する正当な理由がない限り、デフォルトで @ApplicationScoped
を使用することをお勧めします。
12. クライアントプロキシーの概念が理解できません。
確かに、 https://jakarta.ee/specifications/cdi/4.1/jakarta -cdi-spec-4.1.html#client_proxies[クライアントプロキシー、window="_blank"] は理解しにくいかもしれませんが、いくつかの便利な機能を提供します。
クライアントプロキシーは基本的に、ターゲット 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_ClientProxy インスタンスは、 Translator Bean の https://jakarta.ee/specifications/cdi/4.1/jakarta -cdi-spec-4.1.html#contextual_instance[contextual instance、window="_blank"] への直接参照の代わりに常に挿入されます。 |
クライアントプロキシーは、以下のことを可能にします。
-
遅延インスタンス化 - メソッドがプロキシーに呼び出されるとインスタンスが生成されます。
-
「狭い」スコープのBeanを「広い」スコープのBeanに注入する機能、すなわち、
@RequestScoped
Beanを@ApplicationScoped
Beanに注入することができます。 -
依存関係グラフの円形の依存関係。循環的な依存関係を持つことは、しばしば再設計を検討すべきであることを示していますが、時には避けられないこともあります。
-
まれなケースでは、手動でBeanを破棄するのが現実的です。直接参照を注入すると、古くなったBeanのインスタンスになってしまいます。
13. そうですか。Beanは何種類かあるんですよね?
はい、一般的には以下に区別しています:
-
クラスBean
-
プロデューサーメソッド
-
プロデューサーフィールド
-
合成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の型の集合を構築するために使用されます。この場合、 double と java.lang.Object .スコープアノテーションは宣言されていないので、デフォルトは @Dependent になります。 |
2 | コンテナーは、Beanのインスタンスを作成するときにこのフィールドを読みます。 |
3 | コンテナーはメソッドアノテーションを分析して Bean メタデータを構築します。
戻り値の型 は、Bean 型のセットを構築するために使用されます。
この場合はリスト<String>`、 コレクション<String> 、 反復可能<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
@Decorated
Bean<Account> delegateInfo; (5)
@Inject
LogService logService; (6)
void withdraw(BigDecimal amount) {
delegate.withdraw(amount); (7)
if (amount.compareTo(1000) > 0) {
logService.logWithdrawal(delegate, amount);
}
}
}
1 | @Priority はデコレーターを有効にします。優先度の値が小さいインターセプターが最初に呼び出されます。 |
2 | @Decorator はデコレーターコンポーネントをマークします。 |
3 | 装飾された型のセットには、 java.io.Serializable を除く、Java インターフェースであるすべての Bean 型が含まれます。 |
4 | 各デコレーターは、正確に1つの デリゲート・インジェクション・ポイント を宣言する必要があります。デコレーターは、このデリゲート・インジェクション・ポイントに割り当て可能なBeanに適用されます。 |
5 | @Decorated 修飾子を使用すると、装飾された Bean に関する情報を取得できます。 |
6 | デコレーターは、他のBeanを注入することができます。 |
7 | デコレーターは、デリゲートオブジェクトの任意のメソッドを呼び出すことができます。そして、コンテナは、チェーンの次のデコレーターか、インターセプトされたインスタンスのビジネスメソッドを呼び出します。 |
デコレーターのインスタンスは、インターセプトする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. まとめ
このガイドでは、 https://jakarta.ee/specifications/cdi/4.1/jakarta -cdi-spec-4.1.html[Jakarta コンテキストと依存性の注入 4.1、window="_blank"] 仕様に基づいた Quarkus プログラミングモデルの基本的なトピックについて説明しました。 Quarkus は CDI Lite 仕様を実装していますが、CDI Full は実装していません。 the list of supported features and limitations も参照してください。 non-standard features と Quarkus-specific APIs などもあります。
Quarkus 固有の機能と制限に関する詳細は、Quarkus CDI リファレンスガイド を参照してください。 より複雑なトピックを理解するために、 https://jakarta.ee/specifications/cdi/4.1/jakarta -cdi-spec-4.1.html[CDI 仕様] と Weld ドキュメント (Weld は CDI リファレンス実装です) を参照することを推奨します。 |