コンテキストと依存性注入(CDI)の紹介
In this guide we’re going to describe the basic principles of the Quarkus programming model that is based on the Jakarta Contexts and Dependency Injection 4.1 specification. The CDI reference guide describes the bean discovery, non-standard features and configuration properties. The CDI integration guide has more detail on common CDI-related integration use cases, and example code for solutions.
1. OK、簡単なことから始めましょう。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 から来ています。関連するインターセプターは呼び出しをインターセプトし、関連するメトリクスを更新します。 インターセプターについては後述します。 |
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. セッターやコンストラクタのインジェクションは使えますか?
Yes, you can. In fact, in CDI the "setter injection" is superseded by more powerful initializer methods. Initializers may accept multiple parameters and don’t have to follow the JavaBean naming conventions.
@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. 修飾子の話をしましたか?
Qualifiers are annotations that help the container to distinguish beans that implement the same type.
As we already said a bean is assignable to an injection point if it has all the required qualifiers.
If you declare no qualifier at an injection point the @Default
qualifier is assumed.
修飾子型は、 @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 is a qualifier annotation. |
この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インスタンスがアプリケーションに使用され、すべてのインジェクションポイント間で共有されます。 インスタンス は遅延的に生成されます。 |
|
クライアントプロキシーを使用しないことを除いて、 |
|
Beanインスタンスは、現在の リクエスト (通常はHTTPリクエスト)に関連付けられています。 |
|
これは疑似スコープです。インスタンスは共有されておらず、すべての注入ポイントは依存Beanの新しいインスタンスをスポーンします。依存Beanのライフサイクルは、それを注入するBeanに拘束されています。 |
|
このスコープは |
Quarkusのエクステンションによって提供される他のカスタムスコープもあります。例えば、 quarkus-narayana-jta は jakarta.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. クライアントプロキシーの概念が理解できません。
Indeed, the client proxies could be hard to grasp, but they provide some useful functionality.
A client proxy is basically an object that delegates all method invocations to a target bean instance.
It’s a container construct that implements io.quarkus.arc.ClientProxy
and extends the bean class.
クライアントプロキシーはメソッドの呼び出しをデリゲートするだけです。そのため、通常のスコープされた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 | The Translator_ClientProxy instance is always injected instead of a direct reference to a contextual instance of the Translator bean. |
クライアントプロキシーは、以下のことを可能にします。
-
遅延インスタンス化 - メソッドがプロキシーに呼び出されるとインスタンスが生成されます。
-
「狭い」スコープの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型のセットを構築するために使用されます。この場合、 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. まとめ
In this guide, we’ve covered some basic topics of the Quarkus programming model that is based on the Jakarta Contexts and Dependency Injection 4.1 specification. Quarkus implements the CDI Lite specification, but not CDI Full. See also the list of supported features and limitations. There are also quite a few non-standard features and Quarkus-specific APIs.
If you wish to learn more about Quarkus-specific features and limitations there is a Quarkus CDI Reference Guide. We also recommend you to read the CDI specification and the Weld documentation (Weld is a CDI Reference Implementation) to get acquainted with more complex topics. |