JavaRush /Java Blog /Random-JA /依存関係の注入または「CDI とは他に何があるの?」についての短い説明です。
Viacheslav
レベル 3

依存関係の注入または「CDI とは他に何があるの?」についての短い説明です。

Random-JA グループに公開済み
現在、最も人気のあるフレームワークの構築の基礎となっているのは、依存関係の挿入です。これについて CDI 仕様に何が記載されているか、どのような基本機能があり、それらをどのように使用できるかを検討することをお勧めします。
依存関係の注入についての簡単な説明、または

導入

この短いレビューを CDI などについて取り上げたいと思います。これは何ですか?CDI は、コンテキストと依存性の注入の略です。これは、依存関係の挿入とコンテキストを説明する Java EE 仕様です。詳細については、Web サイトhttp://cdi-spec.orgを参照してください。CDI は仕様 (どのように機能するかの説明、一連のインターフェイス) であるため、CDI を使用するための実装も必要になります。そのような実装の 1 つは Weld です - http://weld.cdi-spec.org/ 依存関係を管理し、プロジェクトを作成するには、Maven を使用します - https://maven.apache.org それで、Maven がインストールされました。抽象的なことは理解できなくても、実際に理解していきます。これを行うには、Maven を使用してプロジェクトを作成します。コマンドラインを開き (Windows では、Win+R を使用して「ファイル名を指定して実行」ウィンドウを開いて cmd を実行できます)、Maven にすべてを実行してもらいます。このため、Maven にはアーキタイプと呼ばれる概念があります: Maven Archetype
依存関係の注入についての簡単な説明、または
その後、「数字を選択するかフィルターを適用してください」と「org.apache.maven.archetypes:maven-archetype-quickstart バージョンを選択してください」という質問で Enter キーを押すだけです。次に、プロジェクト識別子、いわゆる GAV を入力します ( 「命名規則ガイド」を参照)。
依存関係の注入についての簡単な説明、または
プロジェクトの作成が成功すると、「BUILD SUCCESS」という文字が表示されます。これで、お気に入りの IDE でプロジェクトを開くことができます。

プロジェクトへの CDI の追加

はじめに、CDI には興味深い Web サイトhttp://www.cdi-spec.org/があることがわかりました。ダウンロード セクションがあり、必要なデータを含むテーブルが含まれています。
依存関係の注入についての簡単な説明、または
ここでは、プロジェクトで CDI API を使用するという事実を Maven がどのように記述しているかを確認できます。API はアプリケーション プログラミング インターフェイス、つまり何らかのプログラミング インターフェイスです。私たちは、このインターフェイスの背後で何がどのように機能するかを気にすることなく、インターフェイスを操作します。API は、プロジェクトで使用を開始する jar アーカイブです。つまり、プロジェクトはこの jar に依存し始めます。したがって、このプロジェクトの CDI API は依存関係になります。Maven では、プロジェクトは POM.xml ファイル ( POM - プロジェクト オブジェクト モデル) に記述されます。依存関係は依存関係ブロックに記述されており、そこに新しいエントリを追加する必要があります。
<dependency>
	<groupId>javax.enterprise</groupId>
	<artifactId>cdi-api</artifactId>
	<version>2.0</version>
</dependency>
お気づきかと思いますが、指定された値でスコープを指定しません。なぜこのような違いがあるのでしょうか? このスコープは、誰かが依存関係を提供してくれることを意味します。アプリケーションが Java EE サーバー上で実行されるということは、サーバーがアプリケーションに必要なすべての JEE テクノロジーを提供することを意味します。このレビューを簡単にするために、Java SE 環境で作業することになるため、この依存関係を提供する人はいません。依存関係スコープの詳細については、「依存関係スコープ」を参照してください。これで、インターフェイスを操作できるようになりました。しかし、実装も必要です。覚えているように、Weld を使用します。随所に異なる依存関係が与えられているのが興味深い。ただし、ドキュメントに従っていきます。したがって、「18.4.5. クラスパスの設定」を読んで、そのとおりに実行しましょう。
<dependency>
	<groupId>org.jboss.weld.se</groupId>
	<artifactId>weld-se-core</artifactId>
	<version>3.0.5.Final</version>
</dependency>
Weld の 3 行目のバージョンが CDI 2.0 をサポートしていることが重要です。したがって、このバージョンの API は信頼できます。これでコードを書く準備が整いました。
依存関係の注入についての簡単な説明、または

CDIコンテナの初期化

CDI はメカニズムです。誰かがこのメカニズムを制御する必要があります。上ですでに説明したように、このようなマネージャーはコンテナーです。したがって、これを作成する必要がありますが、それ自体は SE 環境には表示されません。main メソッドに以下を追加しましょう。
public static void main(String[] args) {
	SeContainerInitializer initializer = SeContainerInitializer.newInstance();
	initializer.addPackages(App.class.getPackage());
	SeContainer container = initializer.initialize();
}
CDI コンテナを手動で作成した理由は次のとおりです。私たちはSE環境で働いています。一般的な戦闘プロジェクトでは、コードはサーバー上で実行され、サーバーがコードにさまざまなテクノロジーを提供します。したがって、サーバーが CDI を提供する場合、これはサーバーにすでに CDI コンテナーがあることを意味し、何も追加する必要はありません。ただし、このチュートリアルでは SE 環境を取り上げます。さらに、コンテナはここにあります。なぜコンテナが必要なのでしょうか? 中のコンテナには Bean (CDI Bean) が入っています。
依存関係の注入についての簡単な説明、または

CDI Bean

それで、豆。CDI ビンとは何ですか? これは、いくつかのルールに従う Java クラスです。これらのルールは仕様の「 2.2. Bean の種類は何ですか? 」の章で説明されています。CDI Bean を App クラスと同じパッケージに追加しましょう。
public class Logger {
    public void print(String message) {
        System.out.println(message);
    }
}
これで、メソッドからこの Bean を呼び出すことができますmain
Logger logger = container.select(Logger.class).get();
logger.print("Hello, World!");
ご覧のとおり、new キーワードを使用して Bean を作成したわけではありません。私たちは CDI コンテナに「CDI コンテナ。Logger クラスのインスタンスが本当に必要なので、それをください。」と尋ねました。この方法は「依存関係ルックアップ」、つまり依存関係を検索する方法と呼ばれます。次に、新しいクラスを作成しましょう。
public class DateSource {
    public String getDate() {
        return new Date().toString();
    }
}
日付のテキスト表現を返すプリミティブ クラス。次に、メッセージに日付出力を追加しましょう。
public class Logger {
    @Inject
    private DateSource dateSource;

    public void print(String message) {
        System.out.println(dateSource.getDate() + " : " + message);
    }
}
興味深い @Inject アノテーションが登場しました。cdi 溶接ドキュメントの「 4.1. 注入点」の章に記載されているように、この注釈を使用して注入点を定義します。ロシア語では、これは「実装ポイント」と読み取れます。これらは、Bean をインスタンス化するときに依存関係を注入するために CDI コンテナによって使用されます。ご覧のとおり、dateSource フィールドには値を割り当てていません。その理由は、CDI コンテナが CDI Bean 内部 (それ自体がインスタンス化した、つまり管理する Bean のみ) が「依存関係の注入」を使用できるようにしているという事実です。これは制御の反転のもう 1 つの方法であり、オブジェクトを明示的に作成するのではなく、他の誰かによって依存関係が制御されるアプローチです。依存関係の注入は、メソッド、コンストラクター、またはフィールドを通じて実行できます。詳細については、CDI 仕様の章「5.5. 依存関係の挿入」を参照してください。何を実装する必要があるかを決定する手順はタイプセーフ解決と呼ばれ、これについて説明する必要があります。
依存関係の注入についての簡単な説明、または

名前解決またはタイプセーフ解決

通常、インターフェイスは実装されるオブジェクトのタイプとして使用され、CDI コンテナ自体がどの実装を選択するかを決定します。これは多くの理由で役立ちますが、それについては後で説明します。したがって、ロガーインターフェイスがあります。
public interface Logger {
    void print(String message);
}
ロガーがあれば、それにメッセージを送信すれば、ログというタスクが完了する、と彼は言います。この場合、どこでどのように行うかは重要ではありません。次に、ロガーの実装を作成しましょう。
public class SystemOutLogger implements Logger {
    @Inject
    private DateSource dateSource;

    public void print(String message) {
        System.out.println(message);
    }
}
ご覧のとおり、これは System.out に書き込むロガーです。素晴らしい。これで、main メソッドは以前と同様に機能します。 Logger logger = container.select(Logger.class).get(); この行は引き続きロガーによって受信されます。そして利点は、インターフェースを知るだけで済み、CDI コンテナーがすでに実装を考えてくれることです。ログをリモート ストレージのどこかに送信する 2 番目の実装があるとします。
public class NetworkLogger implements Logger {
    @Override
    public void print(String message) {
        System.out.println("Send log message to remote log system");
    }
}
コードを変更せずに実行すると、エラーが発生します。CDI コンテナはインターフェイスの 2 つの実装を認識しますが、 org.jboss.weld.exceptions.AmbiguousResolutionException: WELD-001335: Ambiguous dependencies for type Logger どちらかを選択することはできません。いくつかのバリエーションが用意されています。最も単純なものは、 CDI Bean の@Vetoedアノテーションで、CDI コンテナーがこのクラスを CDI Bean として認識しないようにします。しかし、もっと興味深いアプローチがあります。CDI Bean は、 Weld CDI ドキュメントの@Alternative4.7. 代替」の章で説明されているアノテーションを使用して「代替」としてマークできます。それはどういう意味ですか?これは、明示的に使用すると言わない限り、選択されないことを意味します。これは Bean の代替バージョンです。NetworkLogger Bean を @Alternative としてマークしてみましょう。コードが再度実行され、SystemOutLogger によって使用されることがわかります。代替手段を有効にするには、 beans.xmlファイルが必要です。「 beans.xml、どこに入れればいいの?」という疑問が生じるかもしれません。したがって、ファイルを正しく配置しましょう。
依存関係の注入についての簡単な説明、または
このファイルを取得するとすぐに、コードを含むアーティファクトは「Explicit Bean archive」と呼ばれるようになります。これで、ソフトウェアと XML という 2 つの別個の構成ができました。問題は、同じデータをロードすることです。たとえば、DataSource Bean 定義は 2 回ロードされ、プログラムは実行時にクラッシュします。CDI コンテナはこれらを 2 つの別個の Bean として認識します (ただし、実際にはこれらは同じクラスであり、CDI コンテナは約 2 回学習しました)。これを回避するには、次の 2 つのオプションがあります。
  • この行を削除しinitializer.addPackages(App.class.getPackage())、代替の指示を XML ファイルに追加します。
<beans
    xmlns="http://xmlns.jcp.org/xml/ns/javaee"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="
        http://xmlns.jcp.org/xml/ns/javaee
        http://xmlns.jcp.org/xml/ns/javaee/beans_1_1.xsd">
    <alternatives>
        <class>ru.javarush.NetworkLogger</class>
    </alternatives>
</beans>
  • bean-discovery-mode値「none 」を持つ属性をBean のルート要素に追加し、プログラムで代替を指定します。
initializer.addPackages(App.class.getPackage());
initializer.selectAlternatives(NetworkLogger.class);
したがって、代替 CDI を使用すると、コンテナはどの Bean を選択するかを決定できます。興味深いことに、CDI コンテナが同じインターフェイスの複数の代替手段を知っている場合、アノテーションを使用して優先順位を示すことでそれを伝えることができます@Priority(CDI 1.1 以降)。
依存関係の注入についての簡単な説明、または

予選

それとは別に、修飾子などについて議論する価値があります。修飾子は Bean の上の注釈によって示され、Bean の検索を絞り込みます。さらに詳しく。興味深いことに、どの CDI Bean にも、どのような場合でも少なくとも 1 つの修飾子 - があります@Any。Bean の上に修飾子を何も指定しなかった場合、CDI コンテナ自体が@Any修飾子 - に別の修飾子を追加します@Default。何かを指定した場合 (たとえば、@Any を明示的に指定した場合)、@Default 修飾子は自動的に追加されません。しかし、予選の利点は、独自の予選を作成できることです。修飾子はアノテーションとほとんど変わりません。本質的に、これは特別な方法で書かれた単なる注釈です。たとえば、プロトコル タイプとして Enum を入力できます。
public enum ProtocolType {
    HTTP, HTTPS
}
次に、このタイプを考慮する修飾子を作成できます。
@Qualifier
@Retention(RUNTIME)
@Target({METHOD, FIELD, PARAMETER, TYPE})
public @interface Protocol {
    ProtocolType value();
    @Nonbinding String comment() default "";
}
@Nonbindingとしてマークされたフィールドは、修飾子の決定に影響を与えないこと に注意してください。次に、修飾子を指定する必要があります。これは、Bean タイプの上 (CDI がその定義方法を認識できるように) および注入ポイントの上 (@Inject アノテーションが付いているため、この場所で注入のためにどの Bean を探すべきかを理解できるように) が示されます。たとえば、修飾子を使用してクラスを追加できます。簡単にするために、この記事では NetworkLogger 内で実行します。
public interface Sender {
	void send(byte[] data);
}

@Protocol(ProtocolType.HTTP)
public static class HTTPSender implements Sender{
	public void send(byte[] data) {
		System.out.println("sended via HTTP");
	}
}

@Protocol(ProtocolType.HTTPS)
public static class HTTPSSender implements Sender{
	public void send(byte[] data) {
		System.out.println("sended via HTTPS");
	}
}
そして、Inject を実行するときに、どのクラスが使用されるかに影響を与える修飾子を指定します。
@Inject
@Protocol(ProtocolType.HTTPS)
private Sender sender;
すごいですよね?) 綺麗に見えますが、その理由は不明です。ここで次のことを想像してください。
Protocol protocol = new Protocol() {
	@Override
	public Class<? extends Annotation> annotationType() {
		return Protocol.class;
	}
	@Override
	public ProtocolType value() {
		String value = "HTTP";
		return ProtocolType.valueOf(value);
	}
};
container.select(NetworkLogger.Sender.class, protocol).get().send(null);
こうすることで、値の取得をオーバーライドして動的に計算できるようになります。たとえば、いくつかの設定から取得できます。そうすれば、プログラムやサーバーを再コンパイルしたり再起動したりせずに、その場で実装を変更できます。ますます面白くなりますね。)
依存関係の注入についての簡単な説明、または

プロデューサー

CDI のもう 1 つの便利な機能はプロデューサーです。これらは、一部の Bean が依存関係の注入を要求したときに呼び出される特別なメソッド (特別なアノテーションが付いています) です。詳細については、ドキュメントのセクション「2.2.3. プロデューサー メソッド」で説明されています。最も単純な例:
@Produces
public Integer getRandomNumber() {
	return new Random().nextInt(100);
}
ここで、整数型のフィールドに注入するときに、このメソッドが呼び出され、そこから値が取得されます。ここで、キーワード new を見たときに、これが CDI Bean ではないことをすぐに理解する必要があることを理解する必要があります。つまり、Random クラスのインスタンスは、CDI コンテナを制御するもの (この場合はプロデューサー) から派生したという理由だけで CDI Bean になるわけではありません。
依存関係の注入についての簡単な説明、または

インターセプター

インターセプターとは、業務に「干渉」するインターセプターのことです。CDI では、これは非常に明確に行われます。インタプリタ (またはインターセプタ) を使用してロギングを行う方法を見てみましょう。まず、インターセプターへのバインディングを記述する必要があります。多くのことと同様、これはアノテーションを使用して行われます。
@Inherited
@InterceptorBinding
@Target({TYPE, METHOD})
@Retention(RUNTIME)
public @interface ConsoleLog {
}
ここで重要なことは、これがインターセプター ( @InterceptorBinding) のバインディングであり、extends ( ) によって継承されることです@InterceptorBinding。次に、インターセプター自体を書いてみましょう。
@Interceptor
@ConsoleLog
public class LogInterceptor {
    @AroundInvoke
    public Object log(InvocationContext ic) throws Exception {
        System.out.println("Invocation method: " + ic.getMethod().getName());
        return ic.proceed();
    }
}
例でインターセプターがどのように記述されているかについては、仕様「1.3.6. インターセプターの例」を参照してください。さて、私たちがしなければならないのはイナーセプターをオンにすることだけです。これを行うには、実行されるメソッドの上にバインディング アノテーションを指定します。
@ConsoleLog
public void print(String message) {
そして、もう一つ非常に重要な詳細があります。インターセプタはデフォルトでは無効になっており、代替手段と同じ方法で有効にする必要があります。たとえば、beans.xmlファイルでは次のようになります。
<interceptors>
	<class>ru.javarush.LogInterceptor</class>
</interceptors>
ご覧のとおり、非常にシンプルです。
依存関係の注入についての簡単な説明、または

イベントとオブザーバー

CDI は、イベントとオブザーバーのモデルも提供します。ここでは、インターセプターほどすべてが明らかではありません。したがって、この場合のイベントは任意のクラスにすることができ、特別な説明は必要ありません。例えば:
public class LogEvent {
    Date date = new Date();
    public String getDate() {
        return date.toString();
    }
}
ここで、誰かがイベントを待つ必要があります。
public class LogEventListener {
    public void logEvent(@Observes LogEvent event){
        System.out.println("Message Date: " + event.getDate());
    }
}
ここで重要なのは、 @Observes アノテーションを指定することです。これは、これが単なるメソッドではなく、LogEvent タイプのイベントを監視した結果として呼び出されるメソッドであることを示します。さて、今度は監視してくれる人が必要です。
public class LogObserver {
    @Inject
    private Event<LogEvent> event;
    public void observe(LogEvent logEvent) {
        event.fire(logEvent);
    }
}
イベント タイプ LogEvent の Event イベントが発生したことをコンテナーに伝えるメソッドが 1 つあります。あとはオブザーバーを使用するだけです。たとえば、NetworkLogger では、オブザーバーの注入を追加できます。
@Inject
private LogObserver observer;
そして print メソッドでは、新しいイベントがあることをオブザーバーに通知できます。
public void print(String message) {
	observer.observe(new LogEvent());
イベントは 1 つのスレッドまたは複数のスレッドで処理できることを知っておくことが重要です。非同期処理の場合は、(.fire の代わりに) メソッドと(@Observes の代わりに).fireAsyncアノテーションを使用します。@ObservesAsyncたとえば、すべてのイベントが異なるスレッドで実行される場合、1 つのスレッドが例外をスローすると、他のスレッドは他のイベントの作業を実行できるようになります。CDI のイベントの詳細については、通常どおり仕様の「10. イベント」の章を参照してください。
依存関係の注入についての簡単な説明、または

デコレーター

上で見てきたように、さまざまなデザインパターンが CDI ウィングの下に集められています。そして、ここにもう 1 つ、デコレータがあります。これは非常に興味深いことです。このクラスを見てみましょう:
@Decorator
public abstract class LoggerDecorator implements Logger {
    public final static String ANSI_GREEN = "\u001B[32m";
    public static final String ANSI_RESET = "\u001B[0m";

    @Inject
    @Delegate
    private Logger delegate;

    @Override
    public void print(String message) {
        delegate.print(ANSI_GREEN + message + ANSI_RESET);
    }
}
これをデコレータとして宣言することで、Logger 実装が使用されるときに、この「アドオン」が使用されることになります。この「アドオン」は、実際の実装を認識しており、デリゲート フィールドに格納されます (アノテーションが付けられているため@Delegate)。デコレータは CDI Bean にのみ関連付けることができ、CDI Bean 自体はインターセプタでもデコレータでもありません。例は仕様書「1.3.7. デコレータの例」にも記載されています。デコレーターは、インターセプターと同様に、オンにする必要があります。たとえば、Beans.xmlでは次のようになります。
<decorators>
	<class>ru.javarush.LoggerDecorator</class>
</decorators>
詳細については、溶接リファレンス「第 10 章 デコレータ」を参照してください。

ライフサイクル

Bean には独自のライフサイクルがあります。次のようになります。
依存関係の注入についての簡単な説明、または
図からわかるように、いわゆるライフサイクル コールバックがあります。これらは、Bean のライフサイクルの特定の段階で特定のメソッドを呼び出すように CDI コンテナに指示するアノテーションです。例えば:
@PostConstruct
public void init() {
	System.out.println("Inited");
}
このメソッドは、CDI Bean がコンテナーによってインスタンス化されるときに呼び出されます。Bean が不要になったときに破棄される場合、@PreDestroy でも同じことが起こります。CDI という頭字語に C (Context) という文字が含まれているのは当然のことです。CDI の Bean はコンテキストに依存します。つまり、そのライフサイクルは、CDI コンテナ内で Bean が存在するコンテキストに依存します。これをよりよく理解するには、仕様セクション「7. コンテキスト インスタンスのライフサイクル」を読む必要があります。また、コンテナー自体にもライフサイクルがあることを知っておく価値があります。これについては「コンテナーのライフサイクル イベント」で読むことができます。
依存関係の注入についての簡単な説明、または

合計

上では、CDI と呼ばれる氷山の一角を見てきました。CDI は JEE 仕様の一部であり、JavaEE 環境で使用されます。Springを使っている人はCDIではなくDIを使うという、ちょっと仕様が違います。しかし、上記のことを知って理解すれば、簡単に考えを変えることができます。Spring が CDI ワールド (同じ Inject) からのアノテーションをサポートしていることを考慮します。追加資料: #ヴィアチェスラフ
コメント
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION