CDI, 私を愛したSPI

この記事はCDI 2.0スペックリードであるAntoine Sabot-Durand氏の記事(Feb 20, 2017)の翻訳です。CDI SPIの概要をサンプルやCDI 1.1の新機能の紹介を交えて説明しています。私はプロデューサーにパラメーターを渡すために限定子を使う方法や、InjectionPointメタデータを使って実装を切り替える方法が面白いと思いました。ちなみに、この記事のタイトルは「007 私を愛したスパイ」をもじっています(この著者の他の記事も同様です)。

(原文)CDI, the SPI who loved me


legobricks

CDIユーザーは何度も私に尋ねます。なぜCDIを採用して、古いフレームワークや開発のやり方を止めるべきなのかを。この質問への答えは先進的なCDIの要素、すなわち拡張機能とCDI SPIに見い出せます。

そう、CDIの本当に優れた機能は最初から使えるものではなく、仕様書を深く検討してそのパワーを引き出す必要があります。残念な事に、仕様書での導入や説明のやり方は上手くいっているとは言えません。

この記事と次のポータブル・エクステンションの記事では、それを解決しようと思います。もしも初心者ユーザーがCDI SPIを学習する時間を割くのであれば、彼らがパワーの概要を理解できるように手助けをします。

私はCDI SPIのすべての側面を紹介し、あなたが日々の仕事でその一部を利用できる方法を紹介します。この記事では、用語「通常のコード」を使うことで、ポータブル・エクステンションのコードの対比させ、標準的な開発とCDIを拡張するための開発とを区別します。最後には、CI SPIが開発者をいかに愛しているのかを理解することでしょう 😉

このSPIって何?

CDI SPIは、開発者がCDIの概念(Bean、イベント、インジェクションポイント、インターセプター、など)に関するメタ情報にアクセスできるようにしたもので仕様書のイントロスペクションの部分になります。

読者の中にはAPI(Application Programming Interface) という用語に馴染みがある方がいると思いますが、CDI仕様書は主にSPI(Service Provider Interface) の概念の上に構築されています。両者は何が違うのでしょうか?

  • APIは、目標を達成するために呼び出したり使ったりするクラス/インタフェース/メソッド/などの記述です
  • SPIは、目標を達成するために拡張したり実装したりするクラス/インタフェース/メソッド/などの記述です

簡単に言うと、CDIは特定のタスクを実行するために利用者が実装する(または利用者のために仕様が実装する)インタフェースを提供します。これらの実装は通常はインジェクションまたはイベントの観察によっておこなわれますが、まれにはあなた自身の実装を生成する必要があるでしょう。

SPIの理解を容易にするために、4つのパートに分割したいと思います。

  • CDI エントリポイント
  • 型メタモデル
  • CDI メタモデル
  • 拡張専用のSPI

この分割はSPIの要素を導入するために普段つかっている個人的なアプローチで、CDIパッケージやドキュメントの構造を反映してはいません。

これらの異なるパートについて探索しましょう。

CDIエントリポイントを提供するSPI

通常、Java EEアプリケーションを開発するとき、CDI Beanグラフの中にわざわざ自分から「入る」必要はありません。それは(式言語を介して)UIやブート時に自動的に起動されるCDIイベント、またはEJB呼び出しから自動的におこなわれます。

しかしどきどき非CDIコードからCDIにアクセスしたり、非CDIコードをCDI Beanに実行時に組み込む必要があるかもしれません。SPIのこのパートはそれをするためのツールを提供します。

entry-points.png

BeanManagerCDI

CDI 1.0では、CDI Beanグラフにアクセスする唯一の解はJNDIからBeanManagerを取得することでした。

BeanManager bm = null;
try {
    InitialContext context = new InitialContext();
    bm = (BeanManager) context.lookup("java:comp/BeanManager");
} catch (Exception e) {
    e.printStackTrace();
}

BeanManagerはCDI SPIの中で中心的なインタフェースで、アプリケーション内のすべてのメタデータと生成済みのコンポーネントへのアクセスを提供します。

のセクション仕様書のセクション やそのjavadoc を調べると、それが持つすべての機能のまとまった概要を知ることができます。

非CDIコードからCDIをアクセスする開発者にとっての主な理由は、CDI Beanグラフに入るためのBeanインスタンスを要求することです。BeanManagerを使ってそれをすることは少し煩雑です。

BeanManager bm = null;
try {
    InitialContext context = new InitialContext();
    bm = (BeanManager) context.lookup("java:comp/BeanManager"); ❶
} catch (Exception e) {
    e.printStackTrace();
}
Set<Bean<?>> beans = bm.getBeans(MyService.class); ❷
Bean<?> bean =  bm.resolve(beans); ❸
CreationalContext ctx = bm.createCreationalContext(bean); ❹
MyService myService = (MyService) bm.getReference(bean, MyService.class, ctx);❺ 

❶ JNDIを介してBeanManagerを取り出す
❷ MyService型と@Default限定子を持つすべてのBeanを取り出す
❸ そのBeanの集合に対して曖昧な依存性解決を適用する
❹ 循環するような複雑なユースケースのためのコンテキスト依存インスタンスを生成するのを助けるためにCreationalContextを生成する
❺ そのインスタンスを取得する

この煩雑さはBeanManagerがCDIエコシステムに対して基本操作を可能にする先進的なCDIツールではないことを証明しています。あなたがインスタンスにアクセスしたいのであれば、それは明らかに最善のソリューションではありません。

それゆえにCDI 1.1ではJava Servie Loaderを使ってCDI実装から具象CDIクラスを取り出すような抽象CDIクラスを導入しました。

CDI<Object> cdi = CDI.current();

CDICDI.getBeanManager() メソッドを持つBeanManagerにすぐにアクセスできる手段を提供しますが、もっと興味深いことには、それはBeanManagerを使った奇妙なコードを使うことなくコンテキスト依存インスタンス(Contextual Instance)を要求するための便利な方法を提供します。

CDIInstance<Object>を拡張するので、それは自然にプログラムによるルックアップを使ってコンテキスト依存インスタンスを解決します。

あなたの非CDIコードでCDIにアクセスするのは次のCDIコード内のインジェクションと同等以上のサービスを提供します。

@Inject @Any Instance<Object> cdi;

インスタンスの取得は以下のように簡単です。

CDI<Object> cdi = CDI.current();
MyService service = cdi.select(MyService.class).get();

Unmanaged

CDI 1.1は他にもCDIを非CIコードに統合するの手助けする素晴らしい機能を導入しました。UnmanagedクラスはCDIの操作を非CDIクラスに適用することができるようにします。

それを使うことで、ライフサイクルコールバック (@Postconstruct@Predestroy) を呼び出してそのようなクラスインスタンスにインジェクションを実行することができます。サードバーティフレームワーク開発者は次にインジェクションポイント(@InjectはCDI仕様ではなく、Inject仕様に含まれることを思い出してください)を含むそれらの非CDIクラスを提供し、Unmanagedクラスはこのクラスのインスタンスを取得するために使うことができます。

例えば、このクラスが非CDIアーカイブに含まれることを想像してください。

public class NonCDI {

  @Inject
  SomeClass someInstance;

  @PostConstruct
  public void init()  {
  ...
  }

  @Predestroy
  public void cleanUp() {
  ...
  }
}

あなたはこのコードを使ってインジェクションが実行された後の状態のインスタンスを取得できます。

Unmanaged unmanaged = new Unmanaged(NonCDI.class);
UnmanagedInstance inst = unmanaged.newInstance();
NonCDI nonCdi = inst.produce().inject().postConstruct().get();

UnmanagedとUnManagedInstanceのコードのクラスをチェックすることによって、この機能を提供するために他のCDI SPIインタフェースがどのようにして使われているかを見ることができます。

型メタモデルのためのSPI

CDIにおけるすべての構成情報はアノテーションに基づいているので、既存の構成情報を生成したり修正したりするにはミュータブル(変更可能)なメタモデルが必要です。

型の表現やリフレクションはJDKに頼ることができたかもしれませんが、それは読み出し専用のためCDIでは我々自身の手によるモデルを作り出す必要がありました。

type-meta.png

AnnotatedTypeインタフェースは、このアノテーションを中心として型メタモデルの主要要素です。

AnnotatedTypeを定義して、型、フィールド、メソッド、メソッドパラメーターの上に必要なすべてのアノテーションをつけましょう。

AnnotatedTypeは主にポータブル・エクステンションで使用されます。それらは既存の型からコンテナーによって構築されます。

見てお判りのとおり、このモデルはCDI固有の機能は持たないので、もしもサードバーティ開発者が彼のフレーワークをCDIに結合すると決めたならば、彼のユーザーはフレームワークを構成するためにAnnotatedTypeを使って試すことができます。

CDIメタモデル専用のSPI

私の1つ前の記事でBeanメタモデルに関するインタフェースの概要について紹介しましたので、ここで再び詳細を説明するつもりはありません。

bean-meta.png

ただ1つ思い出して欲しいのは、このメタモデルは主にカスタムBeanを宣言するためのポータブル・エクステンションで使われるものではありますが、あなたのBeanにおいて、現在のBeanやインターセプター、デコレーター、現在インターセプトされているBeanやデコーレトされているBeanなどに関するイントロスペクション機能を得るために使うことができるということです(訳注:イントロスペクションの説明はこちら)。

残りのCDIメタデータSPIインタフェースは以下のとおりです。

cdi-meta.png

ObserverMethodEventMetadata

ObserverMethodインタフェースは、与えられたオブザーバーメソッドのためのメタデータを表現するもので、ポータブル・エクステンションの外部で使うことはありません。そこでエクステンションについては私の次の記事で扱うことにします。

EventMetadataも同様にイベントに関係するものですが反対のロジックで、それは通常のコードで使われるだけでエクステンションでは決して使われません。あなたはトリガーをかけたイベントについての情報を取得するためにそれをオブザーバーにインジェクトすることができます。

たとえば、オブザーバーを解決するためのより厳密なアプローチのためにそれを使うことができます。

私がの記事イベントの記事で書いたように、与えられた型と限定子の集合に対するオブザーバーの解決では限定子を持たないイベント型のサブタイプのオブザーバーも含みます。次のように効果的なイベント型と限定子をチェックすることによってこの規則に制限を加えるためにEventMetadataを使うことができます。

public class MyService {
  private void strictListen(@Observes @Qualified Payload evt, EventMetadata meta) {
    if(meta.getQualifiers().contains(new QualifiedLiteral())
       && meta.getType().equals(Payload.class))
         System.out.println("Do something") ❶
       else
         System.out.println("ignore")
  }
}

❶ このコードはイベント型が厳密にPayloadであり、かつその限定子が@Qualifiedを含むときに限って実行される

ProducerInjectionTargetとそれらのファクトリ

ProducerInjectionTargetもほとんどエクステンションで使われます。しかし、あなたが上に表示されたUnmanagedをちょっと見たなら、非CDIクラス上にインジェクションをおこなうライフサイクル操作を実行するためにInjectionTargetが通常のコードで使えるということがおわかりになったことでしょう。

Unmanagedが既存のオブジェクト上でインジェクションを実行できるようにしてくれるので、あなたはこのコードを使って自分自身でそれを実行できます。これはサードパーティによって提供されたオブジェクトでCDIらしくインジェクションを実行したいのなら役に立つでしょう。

AnnotatedType type = beanManager.createAnnotatedType(MyClass.class);
InjectionTarget injectionTarget = beanManager.getInjectionTargetFactory(MyClass.class).createInjectionTarget(null);
CreationalContext ctx = beanManager.createCreationalContext(null);

MyClass instance = new Myclass;
injectionTarget.inject(instance, ctx);
injectionTarget.postConstruct(instance);
CDI 1.1は、新しい種類のBeanを生成するためにProducerInjectionTargetをエクステンション内で使うときの循環依存の課題を解決するためProducerFactoryInjectionTargetFactoryを導入しました。これらについては次の記事で説明するつもりです。

InjectionPointメタデータ

このSPIファミリーで大事なことを言い忘れていました。それはInjectionPointです。この万能ナイフは通常のコードよりもむしろエクステンションでよく使われます。しかしこの場合、それは@DependentスコープのBeanに関連するインジェクションポイントの情報を取得するために使うことができるだけです。それはインジェクションポイントのユニークさを保証する唯一の方法です(たとえば、同じ@RequestScopedインスタンスは複数の場所にインジェクトされる可能性があります)。それがInjectionPointのパワーにアクセスするためのコストになります。ではInjectionPointを使う良い方法を調べてみましょう。

プロデューサにパラメーターを渡すために限定子を使う

InjectionPointはインジェクションされるものについての情報を取得するのに使われるので、限定子に含まれる情報はプロデューサー内から返却するものを決めるために使われます。
@Qualifier
@Retention(RetentionPolicy.RUNTIME)
public @interface HttpParam {
    @Nonbinding public String value(); ❶
}

❶ この限定子は、プロデューサーに情報を渡せるようにnon bindingメンバーを組み込んでいる

次は、インジェクションポイントでの情報を解析するDependent Beanのためのプロデューサーです。

@Produces
@HttpParam("") ❶
@Dependent
String getParamValue(InjectionPoint ip, HttpServletRequest req) { ❸
  return req.getParameter(ip.getAnnotated().getAnnotation(HttpParam.class).value());
}

❶ このプロデューサーは型としてStringを持ち、この限定子が付いているBeanを定義する
❷ あなたのBeanでインジェクションポイントを使うにはDependentスコープ内になければならないことを思い出してください
❸ このプロデューサーはInjectionPointメタデータと組み込みのHttpservletRequest Beanをインジェクトする

最後に、このプロデューサと一致するBean型と限定子をインジェクトすることによって、このプロデューサーを使うことができます。この限定子のパラメーターはプロデューサーで使われます。

@Inject
@HttpParam("productId")
String productId;

インジェクションポイントで要求される型を解析する

CDIは型イレーザーを上手に避けていて、強力なパラメーター化された型を保証しています。

以下の例では、インジェクションポイントでのMapの値の型に依存して異なる実装を使うジェネリックMapのためのプロデューサーを持ちます。

class MyMapProducer() {

    @Produces
    <K, V> Map<K, V> produceMap(InjectionPoint ip) {
        if (valueIsNumber(((ParameterizedType) ip.getType()))) ❶
            return new TreeMap<K, V>();
        return new HashMap<K, V>();
    }

    boolean valueIsNumber(ParameterizedType type) { 
        Class<?> valueClass = (Class<?>) type.getActualTypeArguments()[1];❷
        return Number.class.isAssignableFrom(valueClass)
    }
}

❶ このコードはインジェクションポイントで定義されたパラメーター化された型を取り出し、それをテスト関数に送る
❷ このテスト関数は第2の型パラメーターの効果的な型(Mapの値の型)をチェックし、この型がNumberを継承していたらtrueを返す

上のコードでは、@Inject Map<String,String> mapは内部ではHashMapを使い、@Inject Map<String,Integer> mapTreeMapを使います。これはビジネスコードに(実装の詳細が)漏れ出ることなく振る舞いの最適化や変更をするエレガントな方法です。

おわりに

InjectionPointを使って構築可能なたくさんの機能があります。そして、この記事では通常のコードでのいくつかの例を見てきたに過ぎないことを覚えておいてください。エクステンションで何ができるかを考えてみてください。

エクステンション専用のSPI

ようやくここでこのSPIのツアーを終わらせることにしましょう。

次のSPIクラスは全部丸ごとエクステンション開発専用です。

つまり、ポータブルエクステンションの魔法が起こる(主にブートストラップでの)コンテナーライフサイクルの各ステップごとのイベント型が定義されています。

spi-extensions.png

エクステンションに関する次の記事でこの魔法について知ることになるでしょう。

広告

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


DeltaSpike DataをJava EEアプリケーションサーバー上で動かす(2)

前回のブログに引き続きDeltaSpike Dataを使ったJava EEアプリケーションをArquillianを使ってテストする方法について説明します。DeltaSpike Data自身はJava EEサーバーに依存していませんが、今回のサンプルプログラムはJPAのpersistence.xmlがデータベースにアクセスするのにWildFlyのデータソースを使っているのでテストを実行するにはWildFlyが必要になります。このようなアプリサーバーに依存するテストはArqullianを使うことで可能になります。

Arquillianとは

Java EE アプリケーションは、REST/CDI/EJB/JMSなどサーバー上のコンテナに依存することが多いのでJUnitを使ったテストの自動化は簡単ではありません。

ArquillianはJava EEアプリケーションのためのテスティング・フレームワークです。アプリケーションをJava EEサーバー上で動作させてテストをしたいとき、以下のような処理をしたいことがあります。

  • サーバーを起動する
  • アプリケーションをサーバーにデプロイする
  • サーバー上でテストを実行する
  • アプリケーションをサーバーからアンデプロイする
  • サーバーを停止する

Arquillianは、これらサーバーの起動・停止やデプロイ・アンデプロイを代わりにやってくれます。テストの実行は使い慣れたJUnit(やTestNG)を使います。しかも、JUNitのテストプログラムはサーバー上で実行され、CDIやEJBなどのBeanはテスト内にインジェクトして参照することができます。これにより、通常のJUnitを使ったテストと同様にテストをアプリサーバー上で実行することができます。

Arquillianテストプログラムの構造

Arquillianのテストプログラムは以下のような構造になります。

@RunWith(Arquillian.class) // (1)
public class MemberRegistrationTest {
    @Deployment // (2)
    public static Archive<?> createTestArchive() {
    ...
    }
    @Test // (3)
    public void testRegister() throws Exception {
    ...
    }
}
  • (1) @RunWith()でTestRunnerとしてArquillian.classを指定します。
  • (2) @DeploymentでアプリサーバーにデプロイするモジュールをShrinkWrap APIで作成します。
  • (3) @Testでテストメソッドを指定します。

この@Deploymentの部分がArquillian固有の特徴のある部分です。Arquillianではアプリサーバー上でテストプログラムを実行するためのアーカイブをShrinkWrapというAPIを使ってプログラムで作成します。これはメモリ上でアーカイブを作る操作になります。このアーカイブにはアプリケーションのすべてのクラスを含めるのではなく、テストに必要なクラスだけ含めるようにします。このようにアーカイブを作ることでデプロイするアーカイブのサイズを小さくできますし、テストの用途に応じて、クラスやデプロイメントディスクリプタを差し替えることも可能です。

Arquillianのテストプログラム

前回のブログで作成したアプリケーションは、Mavenアーキタイプによって生成しましたが、それにはすでにArquillianに対応したテストプログラムが付いていました。それをベースにDeltaSpike Data用に修正します。修正のポイントは以下の通り。

  • DataSpike Dataの依存ライブラリを追加する
  • アーカイブにDeltaSpike Dataを使ったMemberRepository.classを追加する
  • アーカイブにMETA-INF/apache-deltaspike.propertiesを追加する

注意:以下のテストプログラムでは直接MemberRepositoryを参照していませんが、MemberRegistrationのクラスにおいてそれをインジェクトしているために、アーカイブに含めておく必要があります。以下のサンプルではpom.xmlを参照して必要な依存ライブラリを抽出しています。

@RunWith(Arquillian.class)
public class MemberRegistrationTest {
    @Deployment
    public static Archive<?> createTestArchive() {
    	// DeltaSpike Dataの依存ライブラリをアーカイブに追加する
    	File[] files = Maven.resolver()
                .loadPomFromFile("pom.xml")
                //.importRuntimeDependencies().resolve()
                .resolve("org.apache.deltaspike.modules:deltaspike-data-module-api", "org.apache.deltaspike.modules:deltaspike-data-module-impl")
                .withTransitivity()
                .asFile();
    	
    	// アーカイブにテストプログラムが参照するクラスを追加する
    	Archive<?> archive = ShrinkWrap.create(WebArchive.class, "test.war")
                .addClasses(Member.class, MemberRepository.class, MemberRegistration.class, Resources.class)
                .addAsLibraries(files)
                .addAsResource("META-INF/test-persistence.xml", "META-INF/persistence.xml")
                .addAsResource("META-INF/test-apache-deltaspike.properties", "META-INF/apache-deltaspike.properties")
                .addAsWebInfResource(EmptyAsset.INSTANCE, "beans.xml")
                // Deploy our test datasource
                .addAsWebInfResource("test-ds.xml");
    	
    	// デバッグ用にアーカイブの内容を標準出力に出力する
        System.out.println("archive=" + archive.toString(true));

        return archive;
    }

    @Inject
    MemberRegistration memberRegistration;

    @Inject
    Logger log;

    @Test
    public void testRegister() throws Exception {
	    Member newMember = new Member();
	    newMember.setName("Jane Doe");
	    newMember.setEmail("jane@mailinator.com");
	    newMember.setPhoneNumber("2125551234");
	    memberRegistration.register(newMember);
	    assertNotNull(newMember.getId());
	    log.info(newMember.getName() + " was persisted with id " + newMember.getId());  
    }
}

Arquillianの実行

Arquillianは内部でアプリサーバーと通信してテストケースを実行しますので、テストを実行するにはアプリサーバーを実行する必要があります。アプリサーバーの利用方法としては、remote/managed/embeddedの3種類が可能です。今回はテスト実行時にアプリサーバーの起動・停止をおこなうmanagedモードを選択します。

managedでWildFlyと通信可能にするため環境変数JBOSS_HOMEを設定してください(または、arquillian.xmlにWildFlyのパスを書くことで環境変数を設定しないことも可能です)。

テストの実行方法はmavenコマンドのプロファイルとしてarq-wildfly-managedを指定します。

mvn clean test -Parq-wildfly-managed

このテストを実行すると自動的にWildFlyが起動され、テストプログラムがWildFly上で実行されます。このプログラムではデバッグ用にアーカイブを標準出力に出していますが、その結果が以下にようになっています。DeltaSpike Dataが内部で使っているDeltaSpike Coreなどのjarがいくつも追加されている点に注意してください。

$ mvn clean test -Parq-wildfly-managed
[INFO] Scanning for projects…
[INFO]
[INFO] ————————————————————————
[INFO] Building WildFly Quickstarts: tanoseam-webapp 1.0.0-SNAPSHOT
[INFO] ————————————————————————
[INFO]
// (略)
00:12:57,148 INFO [org.jboss.as] (Controller Boot Thread) WFLYSRV0025: WildFly Full 10.0.0.CR4 (WildFly Core 2.0.0.CR8) started in 8679ms – Started 387 of 609 services (326 services are lazy, passive or on-demand)
archive=test.war:
/WEB-INF/
/WEB-INF/test-ds.xml
/WEB-INF/lib/
/WEB-INF/lib/deltaspike-partial-bean-module-impl-1.5.2.jar
/WEB-INF/lib/deltaspike-data-module-api-1.5.2.jar
/WEB-INF/lib/deltaspike-data-module-impl-1.5.2.jar
/WEB-INF/lib/deltaspike-core-impl-1.5.2.jar
/WEB-INF/lib/deltaspike-jpa-module-impl-1.5.2.jar
/WEB-INF/lib/deltaspike-proxy-module-api-1.5.2.jar
/WEB-INF/lib/deltaspike-proxy-module-impl-asm5-1.5.2.jar
/WEB-INF/lib/deltaspike-jpa-module-api-1.5.2.jar
/WEB-INF/lib/deltaspike-partial-bean-module-api-1.5.2.jar
/WEB-INF/lib/deltaspike-core-api-1.5.2.jar
/WEB-INF/classes/
/WEB-INF/classes/META-INF/
/WEB-INF/classes/META-INF/apache-deltaspike.properties
/WEB-INF/classes/META-INF/persistence.xml
/WEB-INF/classes/org/
/WEB-INF/classes/org/tanoseam/
/WEB-INF/classes/org/tanoseam/examples/
/WEB-INF/classes/org/tanoseam/examples/data/
/WEB-INF/classes/org/tanoseam/examples/data/MemberRepository.class
/WEB-INF/classes/org/tanoseam/examples/util/
/WEB-INF/classes/org/tanoseam/examples/util/Resources.class
/WEB-INF/classes/org/tanoseam/examples/service/
/WEB-INF/classes/org/tanoseam/examples/service/MemberRegistration.class
/WEB-INF/classes/org/tanoseam/examples/model/
/WEB-INF/classes/org/tanoseam/examples/model/Member.class
/WEB-INF/beans.xml// (略)

0:13:01,164 INFO [org.jboss.as] (MSC service thread 1-5) WFLYSRV0050: WildFly Full 10.0.0.CR4 (WildFly Core 2.0.0.CR8) stopped in 83ms

Results :

Tests run: 1, Failures: 0, Errors: 0, Skipped: 0

[INFO] ————————————————————————
[INFO] BUILD SUCCESS
[INFO] ————————————————————————
[INFO] Total time: 17.805 s
[INFO] Finished at: 2016-01-17T00:13:01+09:00
[INFO] Final Memory: 36M/395M
[INFO] ————————————————————————

DeltaSpike DataをJava EEアプリケーションサーバー上で動かす(1)

前回のブログで紹介したDeltaSpike DataモジュールをJava EEアプリケーションサーバー上で動かしてみましょう。どのアプリケーションサーバーを使ってもよいのですが、このブログではWildFlyを使った手順を紹介します。WildFlyにはCDIコンテナとしてWeldが組み込まれていますし、H2データベースが最初からインストールされているので、DeltaSpike Dataモジュールをすぐに試してみることができます。このページの最後にサンプルプログラムのリンクをつけていますので実際にプログラムを動かしながら確認してください。

WildFlyのインストール

WildFlyはJBoss.orgにおいてオープンソースで開発されているJava EE 7に対応したアプリケーションサーバーです(以前はJBossプロジェクトのアプリケーションサーバーはJBoss ASと呼ばれていましたが、JBoss ASはVersion 7までで、Version 8からWildFlyはという名前に変更されました)。

WildFlyの最新安定バージョンは9.0.1.Finalですが、今回は10.0.0.CR4を使うことにします。以下のサイトからwildfly-10.0.0.CR4.zipをダウンロードして、適当なディレクトリで解凍してください。

WildFlyの起動と停止

WildFlyの起動方法にはStandaloneモードとDomainモードの二つがあります。ここでは、単一サーバーで確認するためStandaloneモードで起動します。

$ cd wildfly-10.0.0.CR4/bin
$ ./standalone.sh

 

以後、WildFlyを起動したままの状態で作業を進めますが、サーバーの停止の際は起動したコンソール上でCntrl-Cを押してください。

ベースとなるアプリケーションの準備

DeltaSpike Dataモジュールのサンプルはスクラッチから作るのではなくて、ありもののJava EEアプリケーションを修正して作ることにします。WildFly用のJava EE 7 Webアプリケーションを作成するMavenアーキタイプを使うことにしましょう。以下のコマンドを叩くと、tanoseam-webappという名前のMavenプロジェクトが作成されます。

$ mvn archetype:generate -DarchetypeGroupId=org.wildfly.archetype -DarchetypeArtifactId=wildfly-javaee7-webapp-archetype -DarchetypeVersion=8.2.0.Final -DgroupId=org.tanoseam.examples -DartifactId=tanoseam-webapp -Dversion=1.0.0-SNAPSHOT -DinteractiveMode=false

 

このアプリケーションはkitchensinkと呼ばれるJSF/REST/EJB/JPAを使ったWebアプリケーションです。このJPAの部分をDeltaSpike Dataモジュールを使って置き換えます。

DeltaSpikeに対応させるためのアプリケーションの修正

アプリケーション内で直接EntityManagerを操作している箇所をDeltaSpike Dataを使うように修正します。このアプリケーションでは、メンバーの検索はMemberRepository、メンバー登録はMemberRegistrationというクラスが担当していますので、それぞれ修正が必要です。

1. アプリケーションのpom.xmlにDeltaSpike Dataの依存ライブラリを追加します。バージョンは最新版の1.5.2を指定しましょう。

		<dependency>
			<groupId>org.apache.deltaspike.modules</groupId>
			<artifactId>deltaspike-data-module-api</artifactId>
			<version>${version.deltaspike}</version>
		</dependency>

		<dependency>
			<groupId>org.apache.deltaspike.modules</groupId>
			<artifactId>deltaspike-data-module-impl</artifactId>
			<version>${version.deltaspike}</version>
		</dependency>

2. tanoseam-webappプロジェクトのソースを修正して、データベースの検索部分をDeltaSpike Dataを使うように変更します。修正箇所は3つのファイルです。

  • org.tanoseam.examples.data.data.MemberRepository.javaを(前回のブログで紹介した)DeltaSpike Dataモジュールを使ったリポジトリ実装で置き換えてください。
package org.tanoseam.examples.data;

import org.tanoseam.examples.model.Member;
import org.apache.deltaspike.data.api.EntityRepository;
import org.apache.deltaspike.data.api.Query;
import org.apache.deltaspike.data.api.Repository;
import java.util.List;

@Repository
public interface MemberRepository extends EntityRepository<Member, Long> {

    @Query("select m from Member m where m.email = ?1")
	public Member findByEmail(String email);

    @Query("select m from Member m order by name")
	public List<Member> findAllOrderedByName();
}
  • 次に、RESTサービスを上記リポジトリが提供するインタフェースに合わせて修正します。
    org.tanoseam.examples.rest.MemberResourceRESTService.javaのlookupMemberById()メソッドにおいてfindById(id)をfindBy(id)へ変更してください。
@GET
@Path("/{id:[0-9][0-9]*}")
@Produces(MediaType.APPLICATION_JSON)
public Member lookupMemberById(@PathParam("id") long id) {
    //Member member = repository.findById(id);
    Member member = repository.findBy(id);
    if (member == null) {
        throw new WebApplicationException(Response.Status.NOT_FOUND);
    }
    return member;
}
  • 最後に、org.tanoseam.examples.service.MemberRegistration.javaを修正します。MemberRepositoryのBeanを変数repositoryにインジェクトして、em.persist(member)をrepository.save(member)に置き換えてください。
@Inject
private MemberRepository repository;

public void register(Member member) throws Exception {
    log.info("Registering " + member.getName());
    //em.persist(member);
    repository.save(member);
    memberEventSrc.fire(member);
}

WildFlyへのデプロイ

ソースを修正したtanoseam-webappプロジェクトをビルドし、WildFlyへデプロイします。WildFlyへのデプロイには管理コンソールを使うことも可能ですが、WildFlyへのデプロイ用のMavenプラグインがありますので、今回はそれを使います。

$ cd tanoseam-webapp
$ mvn clean package wildfly:deploy

 

アプリケーションの実行

デプロイしたアプリケーションを実行するにはWebブラウザで以下を指定します。

このアプリケーションはメンバー情報(Name, Emai, Phone#)を入力する画面を表示します。各入力項目を入力してRegisterボタンを押下するとメンバーが登録されるはずです。しかし、今回ソースを修正したものを実行すると下図のようにボタンの右側に”A JTA EntityManager cannot use getTransaction()”というエラーメッセージが表示されてしまいます。

tanoseam-webapp

WildFlyのコンソールログを見ると、DeltaSpike Dataモジュールの処理の途中でIllegalStateException例外がスローされてEJB呼び出しが失敗しているようです。スタックトレースをよく見ると、この例外はResourceLocalTransactionStrategyというクラスのgetTransaction()メソッドで発生しています。

Caused by: java.lang.IllegalStateException: A JTA EntityManager cannot use getTransaction()
at org.hibernate.jpa.spi.AbstractEntityManagerImpl.getTransaction(AbstractEntityManagerImpl.java:1333)
at org.jboss.as.jpa.container.AbstractEntityManager.getTransaction(AbstractEntityManager.java:518)
at org.apache.deltaspike.jpa.impl.transaction.ResourceLocalTransactionStrategy.getTransaction(ResourceLocalTransactionStrategy.java:370)
at org.apache.deltaspike.jpa.impl.transaction.ResourceLocalTransactionStrategy.rollbackAllTransactions(ResourceLocalTransactionStrategy.java:336)
at org.apache.deltaspike.jpa.impl.transaction.ResourceLocalTransactionStrategy.execute(ResourceLocalTransactionStrategy.java:154)
at org.apache.deltaspike.data.impl.tx.TransactionalQueryRunner.executeTransactional(TransactionalQueryRunner.java:72)
at org.apache.deltaspike.data.impl.tx.TransactionalQueryRunner.executeQuery(TransactionalQueryRunner.java:54)
at org.apache.deltaspike.data.impl.handler.QueryHandler.process(QueryHandler.java:122)
… 127 more

 

実行時エラーの回避

DeltaSpike DataはDeltaSpike JPAモジュールを使ってトランザクションを制御しています。このエラーは、DeltaSpike JPAモジュールにおけるデフォルトのトランザクションタイプがRESOURCE_LOCALであることが原因です。JPAではRESOURCE_LOCALなトランザクションの場合はEntityManager APIを使ってEntityTransactionを取得し、明示的にトランザクションを開始する必要があります。

しかし、今回のサンプルではMemberRegistryというEJB内部でDeltaSpike Dataのリポジトリ呼び出していますので、リポジトリの処理を開始する前にEJBによってトランザクションはすでに開始されています。そこで、DeltaSpike JPAモジュールはRESOURCE_LOCALなトランザクションを開始することはできず、今回のエラーに至ったと考えられます。

このエラーを回避するには、DeltaSpike Dataにおけるトランザクションの振る舞いをCMT(Container Managed Transaction)に対応させるように変更する必要があります。これを行うには、META-INF/apache-deltaspike.propertiesに以下のようにTransactionStrategyとしてContainerManagedTransactionStrategyを設定します。

globalAlternatives.org.apache.deltaspike.jpa.spi.transaction.TransactionStrategy=org.apache.deltaspike.jpa.impl.transaction.ContainerManagedTransactionStrategy

 

動作確認

エラーを回避する設定をした後にアプリケーションを再デプロイして、いくつかメンバーを登録してみましょう。表のメンバーの表示はMemberListProducerによって行なわれ、その中でMemberRepositoryのfindAllOrderedByName()メソッドが呼び出されています。表のメンバーが名前の順(order by name)になっていることを確認してください。下図の例では(idから類推できるように)David, Bob, Andyの順に登録しましたが、表示上は名前でソートされてAndy, Bob, Davidの順になっています。

members

注意:今回のプログラムではH2データベースを使っていますが、これはデモやテスト用途に適したインメモリの設定になっているため、アプリケーションの再デプロイやサーバーの再起動によってDBに登録済みのデータは消えてしまいます。本番環境でDBを利用する場合は、H2の設定を変更するか、PostgreSQLやMySQLなどの他のデータベースを検討してみてください。

サンプルプログラム

この修正済みのプログラムはこちらからダウンロードできます。

少し手間がかかりましたが、WildFlyという実行環境が準備できるとDeltaSpike Dataのさまざまな機能を実機で試すことができるようになります。次回は、Arquillianを使ってDeltaSpike Dataの機能をテストする方法を説明します。

DeltaSpike Dataの紹介

DeltaSpike Dataモジュールは、Repositoryパターンを実現したモジュールです。JPAを使ったプログラミングでは同様なコードを何度も書く必要があります。Dataモジュールを使うとデータベースへのCRUD関連の機能やクエリ定義をリポジトリインタフェースに集約することができます。さらに、DTO (Data Transfer Object)とエンティティの間のマッピングを提供するなど、なかなか面白い機能も提供しています。

Dataモジュールを使ってリポジトリを作るには次のようなインタフェースを用意します。

例えば、Memberというエンティティクラスがあって、主キーの型がLongの場合、次のようにしてリポジトリを定義します。

@Repository
public interface MemberRepository extends EntityRepository<Member, Long> {}

たったこれだけで、JPAのAPIを使うことなく、Entityに簡単にアクセスできるようになります。EntityRepositoryインタフェースは、以下のようなメソッドを提供しています(雰囲気を伝えるため代表的なものをピックアップしています)。

  • E save(E entity)
  • void remove(E entity)
  • void refresh(E entity)
  • void flush()
  • E findBy(PK pk)
  • List<E> findAll()
  • Long count();

DeltaSpike Dataの簡単な使い方

リポジトリは@Injectで変数にインジェクトして使います。インジェクトのときに指定する型は@Repositoryをつけたインタフェースです。例えば、エンティティを保存するには次のようにします。

@Inject
private MemberRepository repository;

Member member = new Member();
member.setName("Neverbird");
member.setEmail("neverbird@mail.com");
member.setPhoneNumber("1234567890");

repository.save(member);

実装クラスがなくともリポジトリを利用できる理由

さて、ここまででリポジトリの実装クラスが登場していないのに気付きましたか? DeltaSpikeを使うと、インタフェースを宣言するだけでリポジトリを利用できるのです。実は、これは前回のブログ記事で紹介したPartialBeanモジュールを使っています。@Repositoryアノテーションのソースを確認すると、確かに@PartialBeanBindingを使って定義されています。

@Stereotype
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Inherited
@PartialBeanBinding
public @interface Repository
{
 Class forEntity() default Object.class;
 String methodPrefix() default "";
}

PartialBeanは、インタフェースへのアクセスからInvocationHandlerへの呼び出しを行う仕組みを提供します。DeltaSpike DataモジュールはEntityRepositoryインタフェースのJPAによる実装をInvocationHandlerとして提供しています。@Repositoryを使ってリポジトリのインタフェースを宣言することで、リポジトリインタフェースへの呼び出しが内部でInvocationHandlerの呼び出しに変換されているのです。

セットアップ

DeltaSpike Dataモジュールを使うためにはpom.xmlに以下の依存ライブラリを追加します。

<dependency>
 <groupId>org.apache.deltaspike.modules</groupId>
 <artifactId>deltaspike-data-module-api</artifactId>
 <version>${version.deltaspike}</version>
</dependency>

<dependency>
 <groupId>org.apache.deltaspike.modules</groupId>
 <artifactId>deltaspike-data-module-impl</artifactId>
 <version>${version.deltaspike}</version>
</dependency>

クエリの定義

EntityRepositoryインタフェースにはfindByとfindAllしか検索方法が提供されていません。これでは実際のアプリケーションでは使い物にならないのでEntity定義に合わせて必要な検索のためのメソッドをリポジトリに追加します。

@Repository
public interface MemberRepository extends EntityRepository {

 @Query("select m from Member m where m.email = ?1")
 public Member findByEmail(String email);
}

DeltaSpike Dataモジュールの最大の特徴はクエリの定義方法です。上の例は検索メソッドに@Queryというアノテーションを付加することでクエリを定義しています。@Queryで指定しているのはJPAでサポートしているJPQLの文字列です。findByEmailメソッドの例のように検索メソッドに引数を指定することもできます。一見してわかるように、この書き方を使うとJPQLの式と検索メソッドの結びつきを直感的に理解することができます。

リポジトリのユーザーは以下のようにリポジトリに定義されたクエリのメソッドを呼び出します。

@Inject
private MemberRepository repository;

Member member = repository.findByEmail("neverbird@email.com")

DeltaSpike Dataの最大の特徴はクエリ定義だと思います。Dataモジュールでは@Query以外にも豊富なクエリの定義方法を提供しています。次回は、WildFly上で実際にDeltaSpike Dataを動かしながら、その機能を確認していきましょう。

PartialBeanによるタイプセーフなハンドラー実装

DeltaSpike PartialBeanモジュールはインタフェース(または、抽象クラス)とハンドラーをバインドする仕組みを提供します。PartialBeanを使うとBeanの実装クラスを(Reflectionでお馴染みの)InvocationHandlerとして実装することができます。Partial(部分的な、不完全な)というのは、インタフェースを実装すればハンドラーが実装を提供するので、開発者がBeanの完全な実装をしなくとも良いという意味でしょう。

InvocationHandlerというと、それを聞いた人はきっとJavaのDynamic Proxyを想像されると思います。Dynamic ProxyではProxyへの呼び出しはターゲットのオブジェクトを呼び出します。しかし、このPartialBeanは実装上の仕組みとしてはDynamic Proxyを使ってはいるものの、ターゲットのBeanが別に存在するのではなく、ハンドラー自身がCDI Beanとして扱われます。ここが重要です。PartialBeanはCDIのインタセプターとは異なり、Beanの呼び出しに割り込んで何かを処理するものではなく、Beanそのものとしてインジェクションの対象になるのです。

そこで、PartialBeanではインタフェース部分の定義が重要になります。PartialBeanでは、インタフェースにおけるメソッド定義やメソッドに付加されたアノテーションの情報に基づいてハンドラーが処理を実行します。例えば、DeltaSpike DataモジュールではQueryの定義の実装にPartialBeanを使用しています。クエリの式をメソッドのアノテーションとして提供するのです。

@Query("select p from Person p where p.ssn = ?1")
Person findBySSN(String ssn);

これが意味することをよく考えてみましょう。ハンドラーはアノテーションの情報を使ってクエリを実行することができます。開発者はインタフェースからSQL式を理解することができ、しかもクエリを実行するときはメソッドを実行すれば良いのです。メソッドは引数の型や戻り値の型を提供しますから、呼び出し側が間違った呼び出しをしてもコンパイル時に型の不整合を検出することができます。つまり、タイプセーフです。PartialBeanは文字列の世界とタイプセーフな世界をつなぐ面白い仕組みと言えるでしょう。

PartialBeanのセットアップ

PartialBeanモジュールを使うにはMaven pom.xmlの依存ライブラリとして以下の設定を追加します。

		<dependency>
		    <groupId>org.apache.deltaspike.modules</groupId>
		    <artifactId>deltaspike-partial-bean-module-api</artifactId>
		    <version>${deltaspike.version}</version>
		    <scope>compile</scope>
		</dependency>
		
		<dependency>
		    <groupId>org.apache.deltaspike.modules</groupId>
		    <artifactId>deltaspike-partial-bean-module-impl</artifactId>
		    <version>${deltaspike.version}</version>
		    <scope>runtime</scope>
		</dependency>

@PartialBeanBindingの定義

インタフェースとハンドラーを結びつけるためには@PartialBeanBinding使って定義されたアノテーションが必要です。この例では@MyPartialBeanBindingというアノテーションを定義しています。

package org.tanoseam.examples;

import org.apache.deltaspike.partialbean.api.PartialBeanBinding;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.annotation.ElementType;

@PartialBeanBinding
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface MyPartialBeanBinding {}

インタフェース実装の例

次はPartialBeanのインタフェースの例です。@MyPartialBeanBindingというアノテーションをつけているので、これがPartialBeanであることがわかります。また、メソッド定義に@ValueHolderというアノテーションが付いているのに注意してください。このアノテーションは、後で、ハンドラー実装で参照します。

package org.tanoseam.examples;

import javax.enterprise.context.Dependent;

@Dependent
@MyPartialBeanBinding
public interface MyPartialBean {
    @ValueHolder(value="PartialBean")
    String getValue();
}

ハンドラーの実装

PartialBeanのハンドラーはInvocationHandlerのInvokeメソッドを実装します。
このサンプルプログラムでは、メソッドに付加されたアノテーションからvalueを取得しています。

package org.tanoseam.examples;

import java.lang.reflect.Method;
import javax.enterprise.context.Dependent;

@Dependent
@MyPartialBeanBinding
public class MyPartialBeanHandler implements java.lang.reflect.InvocationHandler {
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        ValueHolder holder = method.getAnnotation(ValueHolder.class);
        String value = holder.value();
        return value;
    }
}

テストコード

PartialBeanの使用方法は簡単で、通常のBeanと同様に@Injectで変数に設定することができます。

package org.tanoseam.examples;

import static org.junit.Assert.assertEquals;

import org.apache.deltaspike.testcontrol.api.junit.CdiTestRunner;
import org.junit.Test;
import org.junit.runner.RunWith;
import javax.inject.Inject;

@RunWith(CdiTestRunner.class)
public class PartialBeanTest {

    @Inject
    private MyPartialBean bean;

    @Test
    public void testPartialBean() {
          assertEquals("PartialBean", bean.getValue());
    }
}
%d人のブロガーが「いいね」をつけました。