異なる種類のBeanを認識する方法

この記事はCDI 2.0スペックリードであるAntoine Sabot-Durand氏の記事(Dec 14, 2015)の翻訳です。前半ではBeanとBeanインスタンスの違いや、タイプレゾリューションアルゴリズム、後半では各種CDI Beanの特徴について説明しています。仕様書を噛み砕いて説明している良い記事だと思いますので紹介します。

(原文)How to recognize different types of beans from quite a long way away


CDIにおいてはBeanはまさに中心的な概念です。でも、多くのユーザーにとっては、この概念は曖昧なままで、期待される結果を得るには実験が必要になります。この記事では、Bean関連の概念とBean定義とインジェクションの背後にある隠された詳細を明らかにします。

Bean,コンテキスト依存インスタンス、タイプセーフ・レゾリューション

ほとんどのCDIユーザーが以下のように書くとき、

@Inject
@MyQualifier
MyBean bean;

「私は@MyQualifier限定子を持つMyBean型のBeanをインジェクションさせる」と考えるでしょう。それは正しくないので、このインジェクションポイントの定義の背後にある正確なメカニズムを理解することが重要です。

Bean vs コンテキスト依存インスタンス

CDI特有の特徴の一つは、すべてのコンポーネント(限定子、Bean、プロデューサなど)はデプロイ時に発見されるということです。

それによってコンテナーは(実行前の)ごく初期にエラーを上げることができ、あなたが定義したすべてのインジェクションポイントが満たされ、曖昧にならないことを確信できます。

このディスカバリのプロセスはこの記事のトピックではないですが、あなたのアプリケーション内のすべてのクラスはBean(や他のコンポーネント)を発見するためにデプロイ時に解析されるということを理解すべきです。

このディスカバリのタスクの最後にはCDI SPIに含まれるほとんどの要素のためのメタデータの生成が終了しています。CDIコンテナーによって生成されるより中心的なメタデータは、デプロイ中に発見されるBeanのコレクションです。これらのメタデータは実際のアプリケーションのBeanであり、基本的なCDIの使い方では決して使用することはありません。

ですから、あなたのコードにインジェクションポイントを追加するときに、Beanと、コンテナーにリクエストするコンテキスト依存インスタンス(コンテキストのためのBeanのインスタンス)を混同しないでください。

Beanインタフェースの内容

Beanインタフェースは2つの主要な機能を持ちます。

  • コンテキスト依存インスタンスを生成・破棄するための”レシピ”を提供する(Contexualからのメソッド)
  • Bean定義から得られるBeanメタデータを格納する(BeanAttributeからのメソッド)
スクリーンショット 2017-12-31 14.52.02.png

Beanインタフェース階層。そう、InterceptorとDecoratorもBeanです。

Beanに格納されるメタデータはBeanを定義するユーザーコード(型やアノテーション)からくるものです。上のスキーマのBeanAttributesを見てみれば、これらのメタデータは型の集合(複数の型としてのBean)と限定子の集合(各Beanは少なくとも@Default@Anyという2つの限定子を持つ)を含むことがわかるでしょう。これら2つの集合はCDIタイプセーフ・レゾリューション・メカニズムで使用されます。

超初心者向けのタイプセーフ・レゾリューション

あなたのコードで@Injectを使うとき、あなたはコンテナーに対してある種のBeanを探すことを依頼することになります。この探索はBeanメタデータ内の情報を使って実施されます。

ほとんどのインジェクションポイントに対して、各インジェクションポイントが満たされていて曖昧でないかをチェックするための探索がデプロイ時に実施されます。唯一の例外は(Instanceを使った)プログラムによる参照の取得です。

一致するBeanが見つかると、インスタンスを提供するためにコンテナーは createメソッドを使います。

タイプセーフ・レゾリューションと呼ばれるこのプロセスは、次のように単純化できます: 与えられたインジェクションポイントのためのBeanを決定するとき、コンテナーは正しい候補を見つけるためにすべての利用可能なBeanの型と限定子の集合を検討します。

スクリーンショット 2017-12-31 14.40.29.png

単純化したタイプセーフ・レゾリューション

実際のプロセスはAlternativesと統合されるのでもっと複雑ですが、一般的な考えはここに書いてあることになります。

コンテナーが唯一の適格なBeanを見つけてインジェクションポイントを解決するのに成功したら、そのインスタンスを提供するためにこのBeanのcreate()メソッドが使われます。

では、私たちはいつBeanを参照するの?

基本的なCDIでは、その答えは”決してない”(あるいは、ほとんどない)になります。

Beanは、カスタムBeanを生成したりBeanメタデータを解析したりするために、ポータブル・エクステンションにおける90%の時間で使用されます。

CDI 1.1からはBeanをエクステンションの外部でも使うことができます。

リフレクションのために、今ではBeanメタデータをインタセプタまたはデコレーターのBeanクラスにインジェクトすることが許されます。

たとえば、このインターセプターは、実装内でのプロキシ生成の問題を避けるために、インターセプトされたBeanのメタデータを使います。

@Loggable
@Interceptor
public class LoggingInterceptor {

    @Inject
    private Logger logger;

    @Inject @Intercepted
    private Bean intercepted;

    @AroundInvoke
    private Object intercept(InvocationContext ic) throws Exception {
        logger.info(">> " + intercepted.getBeanClass().getName() + " - " + ic.getMethod().getName()); ❷ 
        try {
            return ic.proceed();
        } finally {
            logger.info("<< " + intercepted.getBeanClass().getName() + " - " + ic.getMethod().getName());
        }
    }
}

❶ @InterceptedはインターセプトされたBeanをインターセプター内にインジェクトするために予約された限定子である。
❷ ここで、プロキシのクラス実装ではなく、コンテキストで管理されたインスタンスの実際のクラスを取得するために使用される(訳注:引数のInvocationContextにはプロキシが渡されるので、そのクラスを調べるとプロキシ実装クラスとなる。@InterceptedでBeanを取得することで、Beanのクラスを簡単に取得できる)

異なる種類のCDI Bean

さて、ここまでBeanとBeanインスタンスの違いについて説明してきましたが、CDIに登場するすべてのBeanの種類とそれら特有の振る舞いをリストするときがきました。

管理対象Bean

管理対象BeanはCDIで利用可能なもっとも明白な種類のBeanです。それらはクラス宣言で定義できます。

仕様書(section 3.1.1 Which Java classes are managed beans?)によると以下のような定義になっています:

次の条件をすべて満たす場合、トップレベルJavaクラスは管理対象Beanです。

  • 非staticなインナークラスではない
  • 具象クラスまたは@Decoratorでアノテートされたクラス
  • javax.enterprise.inject.spi.Extensionを実装しない
  • @Vetoedがアノテートされていない、または@Vetoedがアノテートされたパッケージ内でない
  • 以下のいずれかの場合、適切なコンストラクタである
    • パラメーターなしのコンストラクタを持つクラス
    • @Injectによってアノテートされたコンストラクタを宣言するクラス

これらの条件を満たすすべてのJavaクラスは管理対象Beanなので、管理対象Beanを定義するために特別な宣言は必要ない。

 — CDI 1.2 仕様書

これらは一般な規則であり、それらを満たす妥当なクラスであっても、もしもBeanディスカバリモードがnoneまたはannotatedに設定されており、かつそのクラスがbean defining annotationを持たないならば無視されます。

まとめると、デフォルトBeanディスカバリ・モード(Annotated)を使っているなら、あなたのクラスは上の条件に従わなければならないし、CDI管理対象Beanになるためにはさらに以下の少なくとも1つを満たさなければなりません。

  • @ApplicationScoped@SessionScoped@ConversationScoped そして@RequestScopedアノテーション
  • すべてのほかのノーマルスコープ型
  • @Interceptor@Decoratorアノテーション
  • すべてのステレオタイプ・アノテーション (例えば、@Stereotypeのアノテーションが付加されたアノテーション)
  • @Dependentスコープアノテーション

別の制限はclient proxiesに関連します。多くの場合(インターセプターまたはデコレーター、パッシベーション、ノーマルスコープの使用、循環の可能性)に備えて、コンテナーはプロキシ内にラップされたコンテキスト依存インスタンスを提供する必要があるかもしれません。この理由で、管理対象Beanクラスはプロキシ可能であるべきで、さもないとコンテナは例外を上げることになります。

したがって、上記の規則に追加して、仕様書は管理対象Beanクラスに対して特定のサービスをサポートするようにする、言い換えると、ノーマルスコープであるという条件を課します。

そこで、可能であれば、あなたのBeanクラスがプロキシ可能であることを保証するために次の制限を避けるべきです。

  • 引数付きの非privateコンストラクタを持つべきである
  • finalにすべきではない
  • 非staticなfinalメソッドを持つべきではない

管理対象BeanのBean型

管理対象Beanのための(タイプセーフ・レソリューションで使われる)Bean型の集合は以下を含みます。

  • そのBeanクラス
  • Objectを含む)各スーパークラス
  • クラスが直接あるいは間接的に実装するすべてのインタフェース

@Typedアノテーションはこの集合を制限することができることを覚えておいてください。それが使用されたとき、Objectと一緒に、valueメンバーを使って明示的にリストされたクラスの型だけがそのBeanのBean型になります。

セッションBean

CDIセッションBeanはCDIの味付けをしたEJBです。@Vetoedアノテーションを付けずにBeanアーカイブ内でEJB 3.xクライアントビューのセッションBeanを定義したなら、あなたは実行時にセッションBeanを持つことになります。

ローカルなステートレス、シングルトン、ステートフルEJBは自動的にCDIセッションBeanとして扱われます。

それらはインジェクション、CDIスコープ、インターセプション、デコレーション、そしてすべての外のCDIサービスをサポートします。リモートEJBやMDBはCDI Beanとしては使用できません。

EJBやCDIスコープに関する次の制限について注意してください

  • ステートレスセッションBeanは@Dependentスコープに属さなければならない
  • シングルトンセッションBeanは@Dependentまたは@ApplicationScopedスコープに属さなければならない
  • ステートフルセッションBeanは任意のスコープに属すことが可能である

CDIにおいてEJBを使うとき、両方の仕様書の機能を使うことができます。たとえば、ひとつのBeanが非同期の振る舞いとobserverの機能を持ちます。

しかし、CDI実装はEJBコンテナをハックしているわけではないことを心に留めておいてください。それはEJBクライアントがしているようにEJBコンテナを使っているだけなのです。

このように、もしもあなたがセッションBeanをインジェクトするために、@Injectを使わずに@EJBを使ったとしたら、インジェクションポイントにプレーンなEJBを得るだけで、CDIセッションBeanを得るわけではないのです。

セッションBeanのBean型

(タイプセーフ・レゾリューションの間に使われる)CDIセッションBeanのためのBean型の集合は、その定義に依存します。

もしもセッションBeanがローカルインタフェースを持つならば、それは以下の型を含みます。

  • そのBeanのすべてのローカルインタフェース
  • これらのローカルインタフェースのすべてのスーパーインタフェース
  • Objectクラス

もしもセッションBeanが非インタフェース(non-interface)ビューを持つならば、それは以下の型を含みます。

  • そのBeanクラス
  • Objectを含む)すべてのスーパークラス

この集合も@Typedによって制限できます。

@ConversationScoped
@Stateful
public class ShoppingCart { ... } ❶

@Stateless
@Named("loginAction")
public class LoginActionImpl implements LoginAction { ... } ❷


@ApplicationScoped
@Singleton
@Startup
public class bootBean {
 @Inject
 MyBean bean;
}

❶ @ConversationScopedで定義された(非インタフェースビューの)ステートフルBeanで、Bean型としてShoppingCartObjectを持つ。
❷ ビューを持つ@DependentスコープのステートレスBean。loginActionという名前でELで使用可能。Bean型にLoginActionを持つ。
❸ シングルトンセッションBeanを定義するjavax.ejb.Singletonである。
❹ このEJBは起動時に生成され、MyBean CDI Beanを生成するトリガーになる。

プロデューサー

プロデューサーは標準的なPOJOをCDI Beanに変換する方法です。

プロデューサーは既存のBean内でフィールドやメソッドの定義を通してしか定義することがでません。

@Producesアノテーションをフィールドまたはvoidでないメソッドに追加することによって、あなたは新しいプロデューサー(すなわち新しいBean)を定義します。

プロデューサーを定義するフィールドやメソッドは任意のmofidierを持つことが許されます。staticにすることも可能です。

  • 限定子を持つ
  • スコープを持つ
  • 他のBeanをインジェクトできる。プロデューサーメソッドのパラメーターはインジェクションポイントである。コンテナがコンテキスト依存インスタンスを生成するためにそのメソッドを呼び出すときにコンテナはインジェクションを行う。このインジェクションポイントはデプロイ時にもチェックされる。

けれども、プロデューサーは管理対象BeanやセッションBeanと比べると、インターセプターやデコレーターをサポートしないという制限があります。Beanを作るときにこの制限は明らかではないですが、気に留めておくべきです。

あなたのプロデューサー(フィールドまたはメソッド)がnull値をとることができるなら、それを@Dependentスコープに置かなければなりません。

上で説明したBeanインタフェースを覚えていますか?あなたはプロデューサーメソッドのことを、ちょっと複雑ではありますが、Bean.create()メソッドを定義するための便利な方法であると理解できます。ではcreate()を定義できるなら、destroy()はどうでしょうか?それもディスポーザーメソッドで定義できるのです。

ディスポーザー

 プロデューサーの知られていない機能のひとつは、対応するディスポーザーメソッドを定義できることです。ディスポーザーメソッドはプロデューサーメソッドまたはプロデューサーフィールドによって返されたオブジェクトをカスタマイズされた方法でクリーンアップするような応用ができます。
プロデューサーのように、ディスポーザーメソッドはCDI Bean内で定義されなければなりません。それはどんな修飾子をつけることも、staticにすることも可能です。
プロデューサーとは異なり、ディスポーザーパラメーターと呼ばれ、@Disposesというアノテーションの付いた、たった1つのパラメーターだけを持つべきです。
コンテナーがプロデューサーメソッドまたはフィールドを見つけると、それに対応するディスポーザーメソッドを探します。複数のプロデューサーが一つのディスポーザーメソッドに対応させることができます。

プロデューサーのBean型

プロデューサー型(フィールド型またはメソッドリターン型)は以下に依存します。

  • それがインタフェースであれば、Bean型の集合はそのインタフェースが(直接または間接的)に拡張するすべてのインタフェースとそのObjectを含む。
  • それがプリテミティブまたは配列型であれば、Bean型の集合はその型とObjectを含む。
  • それがクラスであれば、Bean型の集合はそのクラスと、各スーパークラスとそれが(直接または間接的)に実装するすべてのインタフェースを含む。

繰り返しになりますが、@TypedはそのプロデューサーのBean型を制限できます。

public class ProducerBean {

  @Produces
  @ApplicationScoped
  private List<Integer> mapInt = new ArrayList(); ❶

  @Produces @RequestScoped @UserDatabase
  public EntityManager create(EntityManagerFactory emf) { ❷
    return emf.createEntityManager();
  }

  public void close(@Disposes @Any EntityManager em) {  ❸
    em.close();
  }

}

❶ このプロデューサーフィールドはBean型としてList, Collection, IterableそしてObjectを持つBeanを定義する。
❷ このプロデューサーメソッドはどこか別のところで生成されたEntityManagerFactory Beanから@RequestScopedをスコープとした@UserDatabase限定子の付いたEntityManagerを定義する。
❸ このディスポーザーは(@Any限定子のおかげで)すべての生成済みのEntityManagerを処分する。

リソース

プロデューサーのメカニズムのおかげでCDIはJava EEリソースをCDI Beanとして公開することができます。

そのようなリソースとしては以下があります。

  • 永続コンテキスト(@PersistenceContext)
  • 永続ユニット(@PersistenceUnit)
  • リモート EJB(@EJB)
  • Webサービス(@WebServiceRef)
  • 一般的なJava EEリソース(@Resource)
 リソースBeanを定義するためには、リソースBeanを定義する既存のCDI Bean内でプロデューサーフィールドを定義します。
@Produces
@WebServiceRef(lookup="java:app/service/PaymentService") ❶
PaymentService paymentService;

@Produces
@EJB(beanname="../their.jar#PaymentService") ❷
PaymentService paymentService;

@Produces
@CustomerDatabase
@PersistenceContext(unitName="CustomerDatabase") ❸
EntityManager customerDatabasePersistenceContext;

@Produces
@CustomerDatabase
@PersistenceUnit(unitName="CustomerDatabase") ❹
EntityManagerFactory customerDatabasePersistenceUnit;

@Produces
@CustomerDatabase
@Resource(lookup="java:global/env/jdbc/CustomerDatasource") ❺
Datasource customerDatabase;
❶ JNDI名からWebサービスを生成する
❷ Bean名からリモートEJBを生成する
❸ @CustomerDatabase限定子を持つ特定の永続ユニットから永続コンテキストを生成する
❹ @CustomerDatabase限定子から特定の永続ユニットを生成する
❺ JNDI名からJava EEリソースを生成する
もちろん、もっと複雑な方法でリソースを公開することもできます。
COMMITフラッシュモードでEntityManagerを生成するには次のようにします。
public class EntityManagerBeanProducer {

  @PersistenceContext
  private EntityManager em;

  @Produces
  EntityManager produceCommitEm() {
    em.setFlushMode(COMMIT);
    return em;
  }
}

宣言した後にはリソースBeanは他のBeanとしてインジェクションすることが可能です。

リソースのBean型

プロデューサーを使ってBeanとして公開されたリソースは、プロデューサーが型集合に関して従う規則とまったく同じものに従います。

組み込みBean

あなたが生成または公開するBean以外にも、CDIはあなたの開発を助けるようなたくさんの組み込みBeanを提供します。

最初に、コンテナは以下のインタフェースのために@Default限定子を持つ組み込みBeanを常に提供すべきです。

  • @DependentスコープのBeanManagerはBeanの中でBeanManagerを使ったインジェクションを可能にする
  • @RequestScopedConversationはconversationスコープの管理を可能する

イベントを使ったりプログラムによるルックアップを可能にするため、コンテナは以下の特徴を持った組み込みBeanを提供しなければなりません。

  • Bean型の集合にEventを含む。ただし、Javaの型Xには型変数を含んではならないこと。
  • 限定子の型の集合に各イベント限定子型を含む
  • @Dependentスコープ
  • Bean名はなし

プログラムによるルックアップのため、コンテナは以下の特徴をもった組み込みBeanを提供しなければなりません。

  • InstanceProvider。ただし、そのBean型の集合においてXは正当なBean型であること。
  • 限定子の型の集合に各限定子型を含む
  • @Dependentスコープ
  • Bean名はなし

Java EEまたは組み込みEJBコンテナは次の組み込みBeanを提供しなければなりません。どれも限定子@Defaultを持ちます。

  • Bean型としてjavax.transaction.UserTransactionを持つBeanで、JTA UserTransactionへの参照のインジェクションを可能にする。
  • Bean型としてjava.security.Principalを持つBeanで、現在の呼び出し元の識別子を表現するPrincipalのインジェクションを可能にする。

サーブレットコンテナは次の組み込みBeanを提供しなければなりません。どれも限定子@Defaultを持ちます。

  • Bean型としてjavax.servlet.http.HttpServletRequestを持つBeanで、HttpServletRequestへの参照のインジェクションを可能にする。
  • Bean型としてjavax.servlet.http.HttpSessionを持つBeanで、HttpSessionへの参照のインジェクションを可能にする。
  • Bean型としてjavax.servlet.ServletContextを持つBeanで、ServletContextへの参照のインジェクションを可能にする。

最後に依存性注入とAOPのイントロスペクションを可能にするために、コンテナは@Dependentスコープで次のインタフェースを持つ組み込みBeanを提供しなければなりません。

  • InjectionPoint@Default限定子を持ち@Dependent Beanのインジェクションポイントについて情報を取得する。
  • Bean@Default限定子を持ち、型集合にTを持つBeanのインジェクトを行う。
  • Bean@Intercepted@Decorated限定子を持ち、型集合にTを持つBeanのインターセプターやデコレーターに適用されるインターセプターやデコレーターのインジェクトを行う。

Beanインジェクションに関するすべての制限を知るには仕様書のbean metadataを調べてください。

カスタムBean

CDIはカスタムBeanについても多くの機能を提供します。ポータブル・エクステンションのおかげで、あなた自身の種類のBeanを追加でき、インスタンスの生成、インジェクション、破棄をフックすることができるようにします。

オブジェクトを生成する代わりに、与えられたインスタンスを探すためにレジストリを調べることができます。ポータブル・エクステンションにおいてそのようなBeanを作成する方法について今後の投稿で紹介します。

まとめ

ご覧いただいたように、@Injectの舞台の背後で多くのことが起こっています。それらを理解することは、CDIを使いこなすことを助けるし、ポータブル・エクステンションへのより明確な入り口となるでしょう。


コメントを残す