CDIのイベントの操作

This tutorial needs a review. You can open a JIRA issue, or edit it in GitHub following these contribution guidelines.

執筆: Andy Gibson

コンテキストと依存性の注入

JSR-299で指定されているコンテキストと依存性の注入(CDI: Contexts and Dependency Injection)はJava EE 6の不可欠な部分であり、サーブレット、エンタープライズBean、JavaBeansなどのJava EEコンポーネントが、アプリケーションのライフサイクル内で明確なスコープを持って存在できるようにするためのアーキテクチャを提供します。また、CDIサービスによって、EJBセッションBeanやJSF (JavaServer Faces)管理対象BeanなどのJava EEコンポーネントが注入可能になり、イベントの起動や監視による疎結合方式の対話が可能になります。

このチュートリアルは、Andy Gibson氏によって投稿されたCDI入門パート3 - イベントというタイトルのブログをベースにしています。ここでは、Java EEの_イベント_の概念を活用する方法を示します。この方法は、アプリケーション内のイベントの生成およびイベントへのサブスクライブ(つまり_監視_)について、プロデューサとオブザーバの間でコードを分離して管理できます。javax.enterprise.event.Event`クラスを使用してイベントを作成し、CDIの@Observes`注釈を使用してイベントにサブスクライブします。

NetBeans IDEは、コンテキストと依存性の注入のサポートを組込みでサポートしています。これには、プロジェクト作成時に`beans.xml` CDI構成ファイルを生成するオプション、注釈のためのエディタおよびナビゲーション・サポート、一般的に使用されるCDIアーティファクトを作成するための各種ウィザードなどが含まれています。

このチュートリアルを完了するには、次のソフトウェアとリソースが必要です。

ソフトウェアまたはリソース 必須バージョン

NetBeans IDE

7.2、7.3、7.4、8.0、Java EEバージョン

Java Development Kit (JDK)

バージョン7または8

GlassFishサーバー

Open Source Edition 3.xまたは4x

cdiDemo3.zip

n/a

  • NetBeans IDEのJava EEバンドル版には、Java EE準拠のコンテナであるGlassFish Server Open Source Editionも含まれています。

  • このチュートリアルのサンプル・ソリューション・プロジェクトをダウンロードできます: cdiDemoComplete.zip

イベントの利用

前の@Alternative Beanおよびライフサイクル注釈の適用のチュートリアルでは、項目の一覧を取得してそれを検証し、無効な項目が見つかったら特定のアクションを起こすアプリケーションを作成しました。仮に、今後システムを拡大して、無効な項目が見つかった場合に発生するあらゆることを処理できるようにするとします。これには、電子メールの送信、他のデータの変更(注文の取消しなど)、またはファイルやデータベース表への却下リストの格納など、様々なものがあります。実装を完全に分離するために、Java EEの_イベント_を使用できます。イベントは、イベント・_プロデューサ_によって生成され、イベント・_オブザーバ_からサブスクライブされます。ほとんどのCDIと同様にイベントの生成およびサブスクライブは型保証であるため、修飾子は、監視するイベント・オブザーバを判別できます。

このシリーズで前のチュートリアルからビルドしているアプリケーションを使用した場合、それほど多くの変更をしなくてもこの実装が可能です。項目を処理するたびにイベントを生成する、もう1つの`ItemErrorHandler` (前のチュートリアルで作成)の実装を提供するのみです。このクラスに`EventItemHandler`という名前を付けて`ItemProcessor`に注入します。注入対象を選択するには、`Notify`修飾子を使用します。

cdi diagram events
Figure 1. アプリケーションの疎結合クラスへのCDI注入の使用
  1. まず、`cdiDemo3.zip`ファイル(上記の必要なリソースの一覧表を参照)からサンプルのスタート・プロジェクトを抽出します。「ファイル」>「プロジェクトを開く」([Ctrl]-[Shift]-[O]、Macの場合は[⌘]-[Shift]-[O])を選択してから、コンピュータ上のこのプロジェクトの場所を選択することで、IDEでプロジェクトを開きます。

  2. `EventItemHandler`という名前のクラスを作成します。「新規ファイル」(new file btn)ボタンをクリックするか、[Ctrl]-[N] (Macの場合は[⌘]-[N])を押してファイル・ウィザードを開きます。

  3. 「Java」カテゴリから「Javaクラス」を選択します。「次」をクリックします。

  4. クラス名として「EventItemHandler」、パッケージとして「exercise4」と入力します。

  5. 「終了」をクリックします。新しいクラスおよびパッケージが生成され、エディタでクラスが開きます。

  6. 次のようにして*EventItemHandler*を実装します。

public class EventItemHandler *implements ItemErrorHandler* {

    *@Inject
    private Event<Item> itemEvent;

    @Override
    public void handleItem(Item item) {
        System.out.println("Firing Event");
        itemEvent.fire(item);
    }*
}

イベント・ペイロードが`Item`になる`Event`のインスタンスを注入します。イベント・ペイロードとはイベント・プロデューサからイベント・オブザーバに渡される状態データのことで、この場合は却下されたItemが渡されます。無効な項目が処理されたら、イベントを起動して、受け取った無効な項目を渡します。このイベント・ベースの項目ハンドラは、他の項目ハンドラと同じように注入されるため、いつでも必要なときに交換したり、テスト中に取り換えたりできます。

  1. すべてのインポートを修正します。エディタを右クリックして「インポートを修正」を選択するか、[Ctrl]-[Shift]-[I] (Macの場合は[⌘]-[Shift]-[I])を押します。必ず`Event`クラスの完全修飾名として`javax.enterprise.event.Event`を選択するようにしてください。

fix all imports
Figure 2. エディタで右クリックして「インポートを修正」を選択し、「インポートを修正」ダイアログを呼び出す

`Event`の上で[Ctrl]-[Space]を押して、クラスのJavadoc定義を表示します。上記で使用した`fire()`メソッドも定義されています。

event javadoc
Figure 3. Ctrl-Space を押して、APIのクラスのJavadocドキュメントを表示する
  1. `Notify`という名前の修飾子を作成します。(修飾子についてはCDIの注入および修飾子の操作に記載。)

  2. 「新規ファイル」(new file btn)ボタンをクリックするか、[Ctrl]-[N] (Macの場合は[⌘]-[N])を押してファイル・ウィザードを開きます。

  3. 「コンテキストと依存性の注入」カテゴリから「修飾子タイプ」を選択します。「次」をクリックします。

  4. クラス名として「Notify」、パッケージとして「exercise4」と入力します。

  5. 「終了」をクリックします。新しい`Notify`修飾子がエディタで開きます。

@Qualifier
@Retention(RUNTIME)
@Target({METHOD, FIELD, PARAMETER, TYPE})
public @interface Notify {
}
  1. EventItemHandler`に@Notify`注釈を追加します。

*@Notify*
public class EventItemHandler implements ItemErrorHandler {

    ...
}

このエラー・ハンドラを注入のために識別し、ItemProcessor`で注入ポイントに追加して使用できる@Notify`修飾子注釈を作成しました。

  1. exercise2.ItemProcessor`で、`EventItemHandler`の注入ポイントに@Notify`注釈を追加します。

@Named
@RequestScoped
public class ItemProcessor {

    @Inject @Demo
    private ItemDao itemDao;

    @Inject
    private ItemValidator itemValidator;

    @Inject *@Notify*
    private ItemErrorHandler itemErrorHandler;

    public void execute() {
        List<Item> items = itemDao.fetchItems();
        for (Item item : items) {
            if (!itemValidator.isValid(item)) {
                itemErrorHandler.handleItem(item);
            }
        }
    }
}

(エディタのヒントを使用して`exercise4.Notify`のインポート文を追加します。)

  1. 「プロジェクトの実行」(run project btn)ボタンをクリックして、プロジェクトを実行します。

  2. ブラウザで「Execute」ボタンをクリックしてからIDEに戻り、「出力」ウィンドウ([Ctrl]-[4]、Macの場合は[⌘]-[4])でサーバー・ログを調べます。ビルドしてきたアプリケーションは、現時点で`DefaultItemDao`を使用して4つの`Item`を設定してから`Item`に`RelaxedItemValidator`を適用するため、`itemErrorHandler`が2度起動するのが確認できるはずです。

output window
Figure 4. 「出力」ウィンドウに表示されたGlassFishサーバー・ログの確認

しかし、現時点ではイベントを監視しているものはありません。これは、@Observes`注釈を使用して_オブザーバ_・メソッドを作成すれば修正できます。イベントを監視するために必要な手順はこれのみです。これを示すため、`FileErrorReporter (前のチュートリアルで作成)にこの`handleItem()`メソッドをコールするオブザーバ・メソッドを追加して、起動されたイベントに応答するように変更できます。

  1. `FileErrorReporter`がイベントに応答するようにするには、クラスに次のメソッドを追加します。

public class FileErrorReporter implements ItemErrorHandler {

    *public void eventFired(@Observes Item item) {
        handleItem(item);
    }*

    ...
}

(エディタのヒントを使用して`javax.enterprise.event.Observes`のインポート文を追加します。)

  1. 再びプロジェクトを実行([F6]、Macの場合は[fn]-[F6])し、「Execute」ボタンをクリックしてからIDEに戻り、「出力」ウィンドウでサーバー・ログを調べます。

output window2
Figure 5. 「出力」ウィンドウに表示されたGlassFishサーバー・ログの確認

先ほどと同じく無効なオブジェクトでイベントが起動されますが、今度は各イベントの起動時に項目の情報が保存されるようになったのが確認できます。また、起動されたイベントごとに`FileErrorReporter` Beanが作成されて閉じられているため、ライフサイクル・イベントが監視されていることもわかります。(@PostConstruct`や@PreDestroy`などのライフサイクル注釈については、@Alternative Beanおよびライフサイクル注釈の適用を参照。)

上記の手順で示したように、`@Observes`注釈はイベントを監視するための簡単な方法を提供します。

イベントおよびオブザーバは、修飾子を使用して注釈を付けることによって、オブザーバが項目の特定のイベントのみを監視するようにもできます。デモについては、CDI入門パート3 – イベントを参照してください。

スコープの処理

現状のアプリケーションでは、イベントが生成されるたびに`FileErrorReporter` Beanが作成されます。この場合、項目ごとにファイルを開いて閉じる必要はないため、毎回新しいBeanを作成することは望ましくありません。ただし、プロセスの開始時にファイルを開き、プロセスの完了時にファイルを閉じる必要があります。このために、FileErrorReporter Beanの_スコープ_について考慮する必要があります。

現時点では、FileErrorReporter Beanに定義されたスコープはありません。定義されたスコープがない場合、CDIはデフォルトの依存擬似スコープを使用します。これは実際のところ、Beanが非常に短い期間(通常はメソッド・コールの期間)で作成および破棄されることを意味します。現在のシナリオでは、起動されたイベントの期間でBeanが作成および破棄されます。これを修正するために、手動でスコープ注釈を追加してBeanのスコープを延ばすことができます。このBeanに`@RequestScoped`を指定して、最初のイベント起動時にBeanが作成されたら、リクエストの期間存在し続けるようにします。これはまた、このBeanを注入できるどの注入ポイントにおいても、同じBeanインスタンスが注入されることを意味します。

  1. FileErrorReporter`クラスに、@RequestScope`注釈および対応する`javax.enterprise.context.RequestScoped`のインポート文を追加します。

*import javax.enterprise.context.RequestScoped;*
...

*@RequestScoped*
public class FileErrorReporter implements ItemErrorHandler { ... }

入力中に[Ctrl]-[Space]を押すと、エディタのコード補完サポートを呼び出せます。コード補完で項目を選択すると、関連付けられたすべてのインポート文が自動的にクラスに追加されます。

code completion
Figure 6. 入力中に [Ctrl]-[Space]を押してコード補完の候補を呼び出す
  1. 再びプロジェクトを実行([F6]、Macの場合は[fn]-[F6])し、「Execute」ボタンをクリックしてからIDEに戻り、「出力」ウィンドウでサーバー・ログを調べます。

output window3
Figure 7. 「出力」ウィンドウに表示されたGlassFishサーバー・ログの確認

FileErrorReporter Beanが最初のイベントの起動時にのみ作成され、最後のイベントの起動後に閉じられます。

INFO: Firing Event
*INFO: Creating file error reporter*
INFO: Saving exercise2.Item@48ce88f6 [Value=34, Limit=7] to file
INFO: Firing Event
INFO: Saving exercise2.Item@3cae5788 [Value=89, Limit=32] to file
*INFO: Closing file error reporter*

システムの各部分をモジュール式で分離するには、イベントの使用をお薦めします。イベントを使用すると、イベントのオブザーバとプロデューサは互いのことを意識する必要がなくなり、そのための構成の必要もなくなります。イベントのプロデューサにオブザーバを意識させることなく、イベントにサブスクライブするコード部分を追加できます。(イベントを使用しない場合、通常は手動でイベントのプロデューサにオブザーバをコールさせる必要があります。)たとえば、だれかが注文ステータスを更新したら営業担当に電子メールを送るイベントや、技術サポートの問題が未解決のまま1週間を超えたらアカウント・マネージャに通知するイベントを追加できます。このような種類のルールはイベントを使用しなくても実装できますが、イベントを使用するとビジネス・ロジックを簡単に分離できるようになります。さらに、コンパイル時やビルド時の依存性がなくなります。ただアプリケーションにモジュールを追加するのみで、自動的にイベントの監視および生成が始まります。

関連項目