まさかの時のCDIポータブル・エクステンション

この記事はCDI 2.0スペックリードであるAntoine Sabot-Durand氏の記事(Feb 6, 2017)の翻訳です。CDIコンテナーのライフサイクルとイベントについてフローチャートを使って内部動作をわかりやすく説明しています。エクステンションについてこれだけ詳細に解説した記事は他にはないと思います。

(原文)Nobody expects the CDI portable extensions


rainbow lego

ポータブル・エクステンションはCDIの中でおそらく最もクールな機能です。

残念なことに、この貴重な機能は仕様書の隠れたところにあって、それにまったく気づかない開発者がいますし、エクステンションを使うのは複雑過ぎると考える人もいます。

この記事では、誰でも自分の仕事で簡単な機能や先進的なインテグレーション機能を提供するためにエクステンションを使えるということを示します。

でも、まず最初に「なぜポータブル・エクステンションを開発する必要があるのか」という基本的な質問に答えましょう。

エクステンションを使って何ができるのか

CDIは構成情報とBeanグラフを生成するためにブート時にクラスパス内のほとんどのクラスをスキャンします。この瞬間に生成されるその構成情報とメタデータは静的な内容(クラスファイル)であり、もっと動的な内容が必要になることがあります。

ここがポータブル・エクステンションが役に立つところになります。

CDIポータブル・エクステンションは、ブート時に発生するCDIスキャンプロセスにフックし、CIコンテナーによって生成されるメタデータを修正したり情報を追加したりすることを可能にします。

それはBeanを追加したり、Beanになるべき型の集合からクラスを削除したり、CDIに存在するプロデューサーやオブザーバー、SPI elements のほとんどを追加したりすることを含みます。

要約すると、エクステンションとは、開発者がCDIを構成やクラスを読んで生成されるデフォルトの振る舞いを上書きすることを可能にする手段になります。

CDIエクステンションのはじめに

 CDIポータブル・エクステンションはJava SE service-providerに基づいています。

サービスインタフェースjavax.enterprise.inject.spi.Extensionで、エクステンションを追加するためにjavax.enterprise.inject.spi.Extensionインタフェースを実装し、このクラスの完全修飾名をMETA-INF/services/javax.enterprise.inject.spi.Extensionサービスプロバイダーのテキストファイルに追加する必要があります。

エクステンションの機能は、CDIコンテナーの特定のライフサイクルイベントに対するオブザーバーを追加することによって定義されます。ブート時に、CDIコンテナーは、すべてのエクステンションを発見してこれらのオブザーバーを登録するために、サービスプロバイダーのメカニズムを使います。

このアプローチはコンテナーの内部ライフサイクルのステップにフックしてそれらの結果を変更することを可能にします。

では、これらのステップをチェックしましょう。

エクステンションのステップ

エクステンションがどのように動作するかを理解するため、コンテナーのライフサイクルを4つの大きなステップに分割するところから始めましょう。

broaderlifecycle-jp.png

CDIコンテナーライフサイクルの主要なステップ

(「アプリケーション実行」以外の)各ステップは1つ以上のイベントを含み、各イベントに対してCDI要素の発見やメタデータの構築のために1つ以上のオブザーバーをエクステンション内で定義することができます。

これらの各ステップについて焦点を絞り、それぞれで使うことができるイベントを説明しましょう。

以下のサンプルのいくつかはCDI 2.0の新機能を使います。CDI 1.2でいかに同じ効果を得ることができるのかについて説明しましょう。

型を発見するフェーズ

型の発見はこのように図解することができます。

 

typediscovery-jp.png
型の発見

caution.png
この図(とこれ以降の図)では、黄色の箱はエクステンションがイベントを観察してアクションを実行する場所で、灰色の箱は内部でのコンテナーの振る舞いを簡単に示したものです。

このステップのゴールは、Beanの候補となるAnnotatedTypeの集合を作ることです。

この集合はBeforeTypeDiscoveryAfterDiscoveryオブザーバーによって明示的に追加することができます。

その集合には自動的にコンテナーによってクラスをスキャンする処理でも自動的に追加されます。その処理には、ProcessAnnotatedTypeオブザーバーを使って発見されたものを変更するために開発者がフックを置くことができます。

スキャン前の型の追加(BeforeBeanDiscoveryイベント)

CDIコンテナーがクラスパス上で型のスキャンを開始する前に、BeforeBeanDiscoveryイベントを発火します。

このイベントを観察することで、特定の型を発見された型の集合に追加したり、限定子、ステレオタイプやインターセプター・バインディングのような特定のCDIアノテーションを追加することが可能になります。

public interface BeforeBeanDiscovery {
  void addQualifier(Class<? extends Annotation> qualifier);❶ 
  void addQualifier(AnnotatedType<? extends Annotation> qualifier); ❶
  void addScope(Class<? extends Annotation> scopeType, boolean normal, boolean passivating); ❷
  void addStereotype(Class<? extends Annotation> stereotype, Annotation... stereotypeDef); ❸
  void addInterceptorBinding(AnnotatedType<? extends Annotation> bindingType); ❹
  void addInterceptorBinding(Class<? extends Annotation> bindingType, Annotation... bindingTypeDef); ❹
  void addAnnotatedType(AnnotatedType<? extends Annotation> type, String id); ❺

  /* New methods in CDI 2.0 */
  <T> AnnotatedTypeConfigurator<T> addAnnotatedType(Class<T> type, String id); ❺
  <T extends Annotation> AnnotatedTypeConfigurator<T> configureQualifier(Class<T> qualifier); ❶
  <T extends Annotation> AnnotatedTypeConfigurator<T> configureInterceptorBinding(Class<T> bt); ❹
}

AnnotationAnnotatedType、またはAnnotatedTypeConfiguratior(CDI 2.0の場合)を使って新しい限定子を追加する
❷ 新しいスコープのAnnotationを追加する
Annotationとそれが意味するAnnotationsコレクションを与えて新しいステレオタイプを定義する
Annotationとそのメタアノテーション、AnnotatedType、またはAnnotatedTypeConfigurator(CDI 2.0の場合)を使って新しいインターセプターバインディングを追加する
❺ カスタムAnnotatedTypeから、またはAnnotatedTypeConfigurator(CDI 2.0の場合)新しいAnnotatedTypeを追加する
次の例はこのイベントの使い方を説明します。
public class MetricsExtension implements Extension { ❶

    public void addMetricAsQual(@Observes BeforeBeanDiscovery bbd) { ❷
        bbd.addQualifier(Metric.class); ❸
    }
}

❶ エクステンションを定義する(クラスのFQDNをMETA-INF/services/javax.enterprise.inject.spi.Extensionテキストファイルに追加することも忘れずに)
BeforeBeanDiscoveryライフサイクルイベントのためのオブザーバー
❸ サードパーティーの非CDIフレームワークからのアノテーションを限定子として定義する

上の例はDropwizard Metrics CDI integration extensionの一部です。それはCDI限定子として標準アノテーション(@Metrics)を宣言します。

また、非CDIクラスをコンテナーから管理対象Beanとして発見されるように変換することも可能です。

public class MyLegacyFrameworkService { ❶

    private Configurator config;

    public MyLegacyFrameworkService(Configurator config) {
        this.config = config;
    }
}

...

public class LegacyIntegrationExtension implements Extension {

    public void addLegacyServiceAsBean(@Observes BeforeBeanDiscovery bbd) {
        bbd.addAnnotatedType(MyLegacyFrameworkService.class,MyLegacyFrameworkService.class.getName()) ❷
                .add(ApplicationScoped.Literal.INSTANCE) ❸
                .filterConstructors(c -> c.getParameters().size() == 1)
                .findFirst().get().add(InjectLiteral.INSTANCE); ❹
    }

❶ レガシーフレームワークからのクラス。そのコードを変更することなくCDIプログラミングモデルに統合したい。
❷ MyLegacyFrameworkServiceクラスに基づいてAnnotatedTypeConfigurator(CDI 2.0新機能)を使う
❸ AnnotatedTypeConfiguratorの上に@ApplicationScopedスコープを追加する
❹ ひとつのパラメーターを取る最初のコンストラクターを探し、そのパラメーターに@Injectを追加する

上の例はCDI 2.0からの新機能を使っています:
AnnotatedTypeConfiguratorBeforeBeanDiscoveryイベントのaddAnnotatedType()メソッドの1つによって返される。もしもあなたがCDI 1.1を使っているなら同じ事ができますが、より煩雑な方法で同じことをするのにあなた自身のAnnotatedTypeを実装しなければなりません。新しいAnnotatedTypeを構成するには、それにスコープを追加し、そのコンストラクタの一つに@Injectアノテーションを追加します。オブザーバーの呼び出しの最後では、コンテナーはこのconfiguratorから自動的に一致するAnnotatedTypeを構築し、それを発見された型の集合に追加します。

自動的な型スキャンの処理

この最初のイベントの後、コンテナーはアプリケーションクラスパス内で型を探す処理を開始します。

このスキャンの構成はクラスパス上の(jarやmoduleなどの)Beanアーカイブごとに異なるものにすることができます。

アプリケーションパス内の各jarは、beans.xmlを含む・含まないのどちらの構成も可能です。このbeans.xmlは、CDIコンテナーによって型を探索するためにBeanアーカイブをどのようにスキャンするかを定義します。

CDIはグローバルな構成ファイルを提供しないので、各Beanアーカイブ(他のアーカイブを内包するwarも対象として含む)ごとにdiscovery modeを定義しなければなりません。

次の3つのdiscovery modeが存在します。

  • none: このBeanアーカイブでは型は発見されない
  • annotated(デフォルトモード): 特定のアノテーション(bean defining annotation)を持つクラスだけが発見される
  • all: すべての型が発見される

discovery modeはBeanアーカイブのbeans.xmlファイルを解析することで推測されます。

表1 discovery modeは何か
beans.xml file state discovery mode

beans.xml が存在しない

annotated

空の beans.xml

all

beans.xml (CDI 1.0 xsdを使用)

all

beans.xml (CDI 1.1 xsdを使用)

bean-discovery-mode 属性の値

exclusion filtersを使って型の発見を細かく制御することもできます。
CDI 2.0ではJava SEを使うとき、beans.xmlファイルを含まないjarはデフォルトで無視されます。

ProcessAnnotatedTypeイベント

このスキャンのフェーズの後は、コンテナーはAnnotatedTypeを生成し、発見された型(アノテーションを除く)ごとにProcessAnnotatedTypeイベントを発火します。

public interface ProcessAnnotatedType<X> { ❶
    AnnotatedType<X> getAnnotatedType(); ❷
    void setAnnotatedType(AnnotatedType<X> type); ❸
    void veto(); ❹

    /* New in CDI 2.0 */
    AnnotatedTypeConfigurator configureAnnotatedType(); ❸
}

❶ このイベントは、ユーザーが与えられた元の型に基づくAnnotatedTypeだけを処理できるようにパラメーター化された型である
❷ 現在処理されているAnnotatedTypeを返す
❸ 処理されているAnnotatedTypeAnnotatedTypeインタフェースを実装すること、またはAnnotatedTypeConfigurator(CDI 2.0の新機能)で新しいものに置き換える
❹ 処理されているAnnotatedTypeを発見された型の集合から取り除く。この型はBeanになることはない

このイベントは既存の型の構成を上書きするのによく使われます。
例えば、以下の例はサードパーティライブラリのStandardService上にtransactionalアノテーションを追加します。

public class AddTranscationalToServiceExtension implements Extension {

    public void addTransactional(@Observes ProcessAnnotatedType<StandardService> pat) {❶ 
        pat.configureAnnotatedType().add(new AnnotationLiteral<Transactional>(){});
    }

❶ オブザーバーはStandardService型に基づいた任意のAnnotatedTypeだけがトリガーとなる

インタフェースを実装するか、指定されたアノテーションを持つ(@WithAnnotationsフィルターに感謝)ことで、型を拒否(veto)できます。

public class VetEntitiesExtension implements Extension {

    public void vetoEntities(@Observes @WithAnnotations(Entity.class) ProcessAnnotatedType<?> pat) { ❶
        pat.veto();
    }

❶ オブザーバーは@Entityアノテーションを持つ任意の型に基づくAnnotatedTypeがトリガーとなる
最後の例はアプリケーション内のすべてのJPAエンティティをCDI Beanとして扱わないようにするために拒否します。

AfterTypeDiscoveryイベント

このイベントは型を発見する処理を終了させます。

public interface AfterTypeDiscovery {
    List<Class<?>> getAlternatives(); ❶
    List<Class<?>> getInterceptors(); ❶
    List<Class<?>> getDecorators(); ❶
    void addAnnotatedType(AnnotatedType<?> type, String id); ❷

    /* New in CDI 2.0 */
    <T> AnnotatedTypeConfigurator<T> addAnnotatedType(Class<T> type, String id); ❷
}

❶ これらのメソッドはオルタナティブBean、インターセプター、デコレーターとして発見されたクラスのリストへのアクセスを可能にする。これらの棚卸しリストはここで必要となるすべてを調査したり、これらのリストは変更可能なので新しいクラスを追加したりすることも可能である。
BeforeBeanDiscoveryと同様にして、発見されたAnnotatedTypeの集合にカスタムのAnnotatedTypeを追加できる。

次のエクステンションは、もしもLastInterceptorクラスがインターセプターとして発見されたならば、これはすべての他のインターセプターの後に呼び出されます。

public class lastInteceptorExtension implements Extension {

public void lastInterceptorCheck (@Observes AfterTypeDiscovery atd) {
        List<Class<?>> interceptors = atd.getInterceptors();
        if(interceptors.indexOf(LastInterceptor.class) < interceptors.size()) {
            interceptors.remove(LastInterceptor.class);
            interceptors.add(LastInterceptor.class);
        }
    }
}

Beanを発見するフェーズ

このフェーズでは、発見された型がBeanになる資格があるかどうかを調べるために分析します。

もしもそれが該当するなら、今後そのBeanを修正できるように一連のイベントが発火されます。

もしもそのBeanがエクステンションによって拒否されなかったならば、コンテナーはプロデューサーとオブザーバーの処理を始めます。

このフェーズの最後では、エクステンションはAfterBeanDiscoveryイベントを使ってカスタムBeanやオブザーバーを登録する機会を得ます。

このフェーズは、AfterDeploymentValidationイベントを使ってコンテナーによってすべての要素の妥当性を検証して終了します。

次のスキーマはすべてのフェーズのステップを図解したものです。最初のうちは複雑に見えるかもしれませんが、この処理は理解するのはどちらかといえば簡単です。

 

beandiscovery-jp.png

Beanの発見

ProcessInjectionPointイベント

この処理の間に遭遇したインジェクションポイントごとに、コンテナーはProjessInjectionPointイベントを発火します。インジェクションポイントは、管理対象Bean、プロデューサーメソッド、オブザーバーメソッドのために発火されます。

public interface ProcessInjectionPoint<T, X> { ❶
    InjectionPoint getInjectionPoint(); ❷
    void setInjectionPoint(InjectionPoint injectionPoint); ❸
    void addDefinitionError(Throwable t); ❹

    /* New in CDI 2.0 */
    InjectionPointConfigurator configureInjectionPoint(); ❸
}

❶ イベントはパラメーター化された型で、オブザーバーが特定のクラスの型 T を対象にし、その型 T には型 X のインジェクションポイントを含む
❷ このイベントによって処理されたInjectionPointを返す
❸ カスタムInjectionPointを実装するか、InjectionPointConfigurator(CDI 2.0新機能)を使うことによって処理されたInjectionPointの置き換えを可能にする
❹ 定義エラーを追加することでオブザーバーがデプロイメントを中止できるようにする

エクステンションはこのイベントを複数の理由から観察することができます。たとえば、与えられた限定子のためのすべての型を収集し、これらのインジェクションポイントに一致するBeanを後から生成するために使用できます。

public class ConvertExtension implements Extension {

    Set<Type> convertTypes = new HashSet();

    public void captureConfigTypes(@Observes ProcessInjectionPoint<?, ?> pip) {
        InjectionPoint ip = pip.getInjectionPoint();
        if (ip.getQualifiers().contains(Convert.Literal.Instance)) {
            convertTypes.add(ip.getType());
        }
    }
}

上の例は@Convert限定子を持つアプリケーション内のすべてのインジェクションポイントのための型の集合を生成します。

後に、発見された型に一致するカスタムBeanを生成するためにこのコレクションを使うことができます。

ProcessInjectionTargetイベント

InjectionTargetは、一見、管理対象Beanではないように見えます。それは主に依存性注入メカニズムといくつかのコールバック機能を提供します。
このイベントはインジェクションをサポートするすべての要素のために発火します。

public interface ProcessInjectionTarget<X> { ❶
    public AnnotatedType<X> getAnnotatedType(); ❷
    public InjectionTarget<X> getInjectionTarget(); ❸
    public void setInjectionTarget(InjectionTarget<X> injectionTarget); ❹
    public void addDefinitionError(Throwable t); ❺
}

❶ そのイベントは、InjectionTargetの基底の型を指定するパラメーター化された型である。
❷ 処理されるInjectionTargetを定義していたAnnotatedTypeを返す
❸ このイベントによって処理されたInjectionTargetを返す
❹ 処理されるのInjectionTargetを置き換えられるようにする
❺ 定義エラーを追加することでオブザーバーがデプロイメントを中止できるようにする

このイベントを観測することでエクステンションがデフォルトのInjectionTargetの振舞いを上書きすることができ、サードパーティフレームワーク上の特定の機能を呼び出すなど、インジェクションの途中で特定のタスクを実行することができます。

ProcessBeanAttributesイベント

このイベントはコンテナー内で発見されたBeanを登録する前に発火されます。このイベントを観測することで属性を修正したり登録をキャンセルしたりすることができます。

このイベントは以下のすべての種類のBeanで発火されます。

  • 管理対象Bean
  • セッションBean
  • プロデューサーフィールド
  • プロデューサーメソッド
  • カスタムBean
public interface ProcessBeanAttributes<T> { ❶
    public Annotated getAnnotated(); ❷
    public BeanAttributes<T> getBeanAttributes(); ❸
    public void setBeanAttributes(BeanAttributes beanAttributes); ❹
    public void addDefinitionError(Throwable t); ❺
    public void veto(); ❻

    /* New in CDI 2.0 */
    public BeanAttributesConfigurator<T> configureBeanAttributes(); ❹
    public void ignoreFinalMethods(); ❼
}

❶ パラメーター化された型のイベントは、与えられた型に対してだけこのイベントを観測することができるようにする
❷ アノテーションが付加されたBeanを定義する型を返す(つまり、管理対象BeanやセッションBeanのためのAnnotatedType、プロデューサーのためのAnnotatedFieldまたはAnnotatedMethod、カスタムBeanのためのnull)
❸ 処理されるBeanAttributesを返す
BeanAttributesインタフェースを実装するか、BeanAttributesConfigurator(CDI 2.0新機能)を使うかのいずれかによって処理されるBeanAttributesを置き換えるようにする
❺ 定義エラーを追加することでオブザーバーがデプロイメントを中止できるようにする
❻ 一致するBeanを無視してその登録をスキップするようにコンテナーに要求する
❼ プロキシ生成に関して仕様書における制約を明示的にスキップするためのCDI 2.0の新しいメソッド

次のエクステンションは、開発者によって限定子が付いたSpecialClass型のBeanがひとつも追加されていないことをチェックします。なぜなら、それ用のカスタムBeanを登録するつもりがあるからです。(訳注:@Defaultは限定子が付いてないBeanに自動的に付加される)

public class CheckExtension implements Extension {

public void filterSpecialClassBean(@Observes ProcessBeanAttributes<SpecialClass> pba) {
        if(pba.getBeanAttributes().getQualifiers().contains(Default.Literal.INSTANCE))
            pba.veto();
    }
}

ProcessBeanイベント

このイベントはBeanがコンテナー内に登録されるときに発火されます。

public interface ProcessBean<X> { ❶ 
    public Annotated getAnnotated(); ❷
    public Bean<X> getBean(); ❸
    public void addDefinitionError(Throwable t); ❹
}
❶ より良いオブザーバーのフィルタリングのためのパラメーター化された型
❷ アノテーションが付加されたBeanを定義する型を返す(つまり、管理対象BeanやセッションBeanのためのAnnotatedType、プロデューサーのためのAnnotatedFieldまたはAnnotatedMethod、カスタムBeanのためのnull)
❸ 生成されたBeanを返す
❹ 定義エラーを追加することでオブザーバーがデプロイメントを中止できるようにする

このイベントは、主に指定されたBeanが生成されたことをチェックし、時には将来の使用のためにその定義を獲得します。ProcessBeanのオブザーバーはすべての種類のBeanを対象とします。もっと指定したい場合は、特定の種類のBeanのイベントだけを観測するためにこのイベントの子を使います。

processBean_hierarchy.png

ProcessProducerイベント

このイベントはアプリケーション内のすべてのプロデューサーに対して発火されます。プロデューサーは一種のBeanであることを思い出してください。でも、その定義と発見はそれを含むBeanに依存します。言いかえれば、Beanとして発見されなかったクラスで定義されたプロデューサーは無視されます。

それは主にコード生成の上書きを可能にします(つまり、エクステンションからアプリケーション内の特定のBeanインスタンスを生成するために書かれたコードを上書きできます)

public interface ProcessProducer<T, X> { 
    AnnotatedMember<T> getAnnotatedMember(); 
    Producer<T> getProducer(); 
    void addDefinitionError(Throwable t); 
    void setProducer(Producer<T> producer); 

    /* New in CDI 2.0 */
    ProducerConfigurator<X> configureProducer(); 
}
❶ より良いオブザーバーのフィルタリングのためのパラメーター化された型。T はそのプロデューサーを含むBeanのBeanクラスで、X はそのプロデューサーの型
❷ そのプロデューサーを定義するAnnotatedMemberを返す(つまり、フィールドプロデューサーにはAnnotatedFieldまたはメソッドプロデューサーにはAnnotatedMethod
❸ 処理されようとしているプロデューサーを返す
❹ 定義エラーを追加することでオブザーバーがデプロイメントを中止できるようにする
Producerインタフェースを実装するか、あるいはProducerConfiguratorヘルパー(CDI 2.0新機能)を使うかのいずれかによって処理されるプロデューサーを置き換えることを可能にする

次の例はMetrics-CDI extensionを参考にしたものです。ユーザーがアプリケーション内のメトリックのためのプロデューサーを宣言するとき、メトリックレジストリ内でそれがすでに存在するかどうかを調べます。もしも存在しているなら、新しいインスタンスを生成する代わりに、そのレジストリ内にあるものを返します。もしもそれが存在しなかったら、そのメトリックを生成するプロデューサーのコードを使用し、レジストリに追加し、アプリケーションに返します。

public class MetricsExtension implements Extension {

<T extends com.codahole.metrics.Metric> void processMetricProducer(@Observes ProcessProducer<?, T> pp, BeanManager bm) { ❶
        Metric m = pp.getAnnotatedMember().getAnnotation(Metric.class); ❷

        if (m != null) { ❸
            String name = m.name(); ❹
            Producer<T> prod = pp.getProducer(); ❺
            pp.configureProducer() ❻
                    .produceWith(ctx -> { ❼
                        MetricRegistry reg = bm.createInstance().select(MetricRegistry.class).get(); ❽
                        if (!reg.getMetrics().containsKey(name)) ❾
                            reg.register(name, prod.produce(ctx)); ➓
                        return (T) reg.getMetrics().get(name);⓫
                    });
        }
    }
}

❶ このオブザーバーはBeanManagerを必要とする。このヘルパーBeanはエクステンションにおける任意のオブザーバーに注入され得る
❷ プロデューサーに付加された@Metricアノテーションを取り出す
❸ もしもアノテーションが発見されなければ処理をスキップする
❹ アノテーションからメトリックの名前を取り出す
❺ コールバック処理で使えるように初期のプロデューサーを取得する
❻ 新しいProducerConfiguratorヘルパーを使う。CDI 1.2ではProducerの自分自身の実装を生成することもできる
❼ そのプロデューサーのインスタンスを生成するための関数コールバックを定義する
❽ レジストリBeanのインスタンスを取り出す
❾ 一致した名前のメトリックを探す
➓ もしも存在しなければオリジナルのプロデューサーコードを使って生成し、レジストリに格納する
⓫ レジストリから一致する名前のメトリックを返す

ProcessObserverMethodイベント

このイベントは有効化されたBean(Enabled Bean)で宣言されたすべてのオブザーバーに対して発火されます。
CDI 2.0以前は、それは主にオブザーバーメソッドの存在をチェックするためのイベントでした。CDI 2.0からは、これはObserverMethodの置き換えやそれを削除することが可能にすることでより制御ができるようになりました。

public interface ProcessObserverMethod<T, X> { 
    AnnotatedMethod<X> getAnnotatedMethod(); 
    ObserverMethod<T> getObserverMethod(); 
    void addDefinitionError(Throwable t); 

    /* new in CDI 2.0 */
    void setObserverMethod(ObserverMethod<T> observerMethod); 
    ObserverMethodConfigurator<T> configureObserverMethod(); 
    void veto(); 
}
❶ より良いオブザーバーフィルタリングのためのパラメータ化された型。Tはオブザーバーメソッドを含むBeanのBeanクラス、Xはそのイベントの型
ObserverMethodを定義するAnnotatedMethodを返す
ObserverMethodを返す
❹ 定義エラーを追加することでオブザーバーがデプロイメントを中止できるようにする
❺ カスタムObserverMethodインスタンスを提供するか、あるいはObserverMethodConfigurator(CDI 2.0新機能)を使うかのいずれかによってObserverMethodを削除するか、上書きすることを可能にする

以下の例はエクステンションがMyClassイベント型のすべての同期オブザーバーを非同期に切り替えることができます。

public class SwitchExtension implements Extension {

   public void switchToAsync(@Observes ProcessObserverMethod<?, MyClass> pom) {
       pom.configureObserverMethod().async(true);
   }
}

AfterBeanDiscoveryイベント

このイベントは、すべてのBean、プロデューサー、オブザーバーを発見した後で発火します。

それは発見されたメタデータを変更したり、改良したりする最後の機会になります。

public interface AfterBeanDiscovery {
    void addDefinitionError(Throwable t); ❶
    void addBean(Bean<?> bean); ❷
    void addObserverMethod(ObserverMethod<?> observerMethod); ❸
    void addContext(Context context); ❹
    <T> AnnotatedType<T> getAnnotatedType(Class<T> type, String id); ❺
    <T> Iterable<AnnotatedType<T>> getAnnotatedTypes(Class<T> type); ❻

    /* New in CDI 2.0 */
    <T> BeanConfigurator<T> addBean(); ❷
    <T> ObserverMethodConfigurator<T> addObserverMethod(); ❸
}
❶ 定義エラーを追加することでオブザーバーがデプロイメントを中止できるようにする
Beanインタフェースのカスタム実装を生成するか、あるいはBeanConfiguratorヘルパー(CDI 2.0新機能)を使うかのいずれかによってカスタムBeanを生成できるようになる
❸ カスタムObserverMethodインスタンスを提供するか、あるいはObserverMethodConfigurator(CDI 2.0新機能)を使うかのいずれかによってObserverMethodを削除するか、上書きすることを可能にする
❹ 新しいコンテキストをコンテナーに追加する
❺ 与えられたクラスとidを使って発見されたAnnotatedTypeを返す
❻ アプリケーション内のすべての発見されたAnnotatedTypeIterableを返す

 

AfterDeploymentValidationイベント

この最後のブートストラッピングイベントは、メタデータ内で期待されるすべてをチェックするためのフックです(オブザーバーはこれらのメタデーターを検査するためにBeanManagerを注入できることを思い出してください)。

このイベントが発火されるとき、コンテナー内のそのメタデータはもはやミュータブルではなく、アプリケーションは実行の準備ができています。

public interface AfterDeploymentValidation {
    void addDeploymentProblem(Throwable t); ❶
}

❶ 定義エラーを追加することでオブザーバーがデプロイメントを中止できるようにする

アプリケーションの生涯と死

ポータブルエクステンションの観点からは、そろそろ終わりに近づいています。

ブートストラッピングのたくさんのフェーズの後、アプリケーションはシャットダウンのイベントまで実行し続けます。それは最後のポータブルエクステンションイベントが発火されるときです。

BeforeShutdownイベント

このイベントはフックで、アプリケーションの生涯の間で生成された特定のリソースを掃除することができるようにするためのものです。

public interface BeforeShutdown {
}

おわりに

ポータブル・エクステンションはとてもパワフルなツールです。

それらを習得することは困難のように思えるかもしれませんが、一度、大半のSPIとこの記事で示したコンテナライフサイクルを理解すれば、アイディア次第で何でも作れる単なる大きなLEGOブロックの箱に過ぎないのです。

広告

異なる種類の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を使いこなすことを助けるし、ポータブル・エクステンションへのより明確な入り口となるでしょう。


%d人のブロガーが「いいね」をつけました。