Java EE7 CDI1.1のまとめ : その2 各種機能の解説
第一回はCDIの基本的な概念について解説したので、
第二回は具体的なCDIの各種機能について解説していきます。
依存関係の解決
CDIの最も基本的な機能になります。
CDIでは他のイネーブルド・ビーンへの参照を@Injectアノテーションで渡します。
/** * DIに使用されるイネーブルド・ビーン。 */ @Dependent public class DIBean { private static AtomicInteger counter = new AtomicInteger(); // インスタンスの生成毎に番号を払い出します。 private int count = counter.incrementAndGet(); public String getName() { return "bean" + count; } }
/** * インジェクト先のイネーブルド・ビーン。 */ @Stateless public class DIInjectee { /** * コンテナが生成したコンテキスチュアル・リファレンスをフィールド・インジェクションします。 * Springの@Autowired(required = "true")相当です。 * CDIの仕様では、曖昧な依存関係や、解決できない依存関係が存在する場合は、 * デプロイメント・エラーとして処理されます。 */ @Inject private DIBean bean1; private DIBean bean2; private DIBean bean3; /** * イニシャライザ・メソッド。 * Springのメソッドインジェクション相当です。 * * @param bean2 */ @Inject public void setBean2(DIBean bean2) { this.bean2 = bean2; } /** * コンストラクタ・インジェクション。 * Springと異なり、コンストラクタには必ず{@link javax.inject.Inject}を * 指定する必要があります。 * * @param diBean3 */ @Inject public DIInjectee(DIBean diBean3) { this.bean3 =diBean3; } /** * このクラスの全てのスーパークラス及び、 * このクラスのフィールド・インジェクション、 * イニシャライザメソッドの後に実行されます。 */ @PostConstruct public void initialize() { System.out.println("initilized!"); } public DIBean getBean1() { return bean1; } public DIBean getBean2() { return bean2; } public DIBean getBean3() { return bean3; } }
Assignableなイネーブルド・ビーンをインジェクトする
CDIでは、タイプセーフ・レゾリューションの仕組みにより、直接インジェクト先の型とマッチしなかった場合でも、代入可能(=サブクラス、実装)な場合は、その型のイネーブルド・ビーンがインジェクトされます。
/** * POJOのインターフェース。 */ public interface BeanInterface { String getName(); }
/** * {@link my.apps.cdi.sample.di.BeanInterface}の実装。 * CDIのタイプセーフ・レゾリューションにより、BeanInterfaceに * 代入可能なBeanImplがコンテキスチャル・リファレンスとして解決されます。 */ @Dependent public class BeanImpl implements BeanInterface { private static AtomicInteger counter = new AtomicInteger(); private int count = counter.incrementAndGet(); @Override public String getName() { return "bean" + count; } }
@Stateless public class AssignableInjectee { /** * CDIのタイプセーフ・レゾリューションにより、BeanInterfaceに * 代入可能なBeanImplがコンテキスチャル・リファレンスとして解決されます。 */ @Inject private BeanInterface bean; public BeanInterface getBean() { return bean; } }
クオリファイヤ(@Qualifier)によるインジェクトするインスタンスの選択
インジェクト対象のイネーブルド・ビーンが一意に定まらない場合に、@Qualifierアノテーションを使用することで、ビーンを一意に識別します。
/** * 独自のQualifierを定義します。 */ @Qualifier @Retention(RetentionPolicy.RUNTIME) // RUNTIMEで必要! デフォルトはCLASSです。 public @interface Impl1Qualifier { }
/** * 独自のQualifierを定義します。 */ @Qualifier @Retention(RetentionPolicy.RUNTIME) // RUNTIMEで必要! デフォルトはCLASSです。 public @interface Impl2Qualifier { }
/** * インジェクト対象のビーンのインターフェース。 */ public interface TargetBe
/** * アノテーションにより、特定の実装に依存せずに * オブジェクトの識別子を定義します。 */ @Impl1Qualifier @Singleton public class TargetBeanImpl1 implements TargetBean { }
/** * アノテーションにより、特定の実装に依存せずに * オブジェクトの識別子を定義します。 */ @Impl2Qualifier @Singleton public class TargetBeanImpl2 implements TargetBean { }
/** * {@link javax.inject.Named}を用いることで、 * 文字列ベースで独自のクオリファイヤを定義します。 */ @Named("impl3") @Singleton public class TargetBeanImpl3 implements TargetBean { }
/** * インジェクト先のセッション・ビーン。 * {@link javax.inject.Qualifier}を使用することで、 * 特定の実装への依存を排除しつつ、 * インジェクト対象のビーンを識別します。 */ @Stateless public class Injectee { /** * TargetBeanに代入可能なManaged Beanは2つ存在するが、 * Qualifierにより実装を一意に識別する。 */ @Impl1Qualifier @Inject private TargetBean targetBean1; @Impl2Qualifier @Inject private TargetBean targetBean2; /** * 文字列でインジェクト対象のビーンを選択します。 */ @Named("impl3") @Inject private TargetBean targetBean3; public TargetBean getTargetBean1() { return targetBean1; } public TargetBean getTargetBean2() { return targetBean2; } public TargetBean getTargetBean3() { return targetBean3; } }
インジェクト対象のイネービルド・ビーンの生成
CDIでは、Springのファクトリ・ビーン機能に相当するプロデューサという機能が存在します。プロデューサを使用することで、ビーンの生成をDIコンテナに任せず、ビーンの生成方法を独自にカスタマイズできます。
また、プロデューサを介して生成されたビーンは、CDIのアノテーション無しでイネーブルド・ビーンとなるため、
CDIを前提としないフレームワークのクラスをイネーブルド・ビーンとしてCDI環境に組み込むのにも活用できます。
注意点としては、プロデューサで生成されるビーンは、通常のマネージド・ビーンと同様に、
デフォルトのコンストラクタ、@Injectで修飾されたパラメータ付きコンストラクタの
いずれかを定義する必要があります。
生成対象ビーンが外部のフレームワークのもので、上記の要件を満たしていない場合は、
CDIのためだけにラップする必要があるかもしれません。
/** * ビーンのプロデューサ。 * * <p> * プロデューサのライフサイクル系アノテーションが * {@link javax.enterprise.context.Dependent}の場合、 * ビーンの生成毎にインスタンスが破棄され非効率なので、 * 特に理由が無い限りは、 * {@link javax.enterprise.context.ApplicationScoped}, * {@link javax.inject.Singleton}, * {@link javax.ejb.Singleton} * のうちの何れかで定義すべきです。 * </p> */ @ApplicationScoped public class BeanProducer { private static AtomicInteger counter = new AtomicInteger(); /** * {@link javax.enterprise.inject.Produces}で * プロデューサ・メソッドを定義します。 * デフォルトのスコープはDependentになります。 * また、Qualifierを指定可能です。 * @return */ @Named("count") @Produces private int incrementAndGet() { return counter.incrementAndGet(); } /** * {@link javax.enterprise.inject.Produces}で * プロデューサ・メソッドを定義します。 * メソッドにスコープ・アノテーションを定義することで、 * 生成されるオブジェクトのライフサイクルを決定します。 * また、パラメータはインジェクション・ポイントになります。 * @param count * @return */ @ApplicationScoped @Produces public ProducedBean produceBean(@Named("count") int count) { return new ProducedBean("ProducedBean: instance count=" + count); } }
/** * 生成対象のビーン。 * * <p> * どのスコープに属するかは、プロデューサが決定するため、 * 生成対象のBeanにスコープを指定する必要はない。 * 多様な使われ方があり得るライブラリの場合は、 * CDIへの依存をなるべく排除したほうが良いかもしれません。 * </p> */ public class ProducedBean { private final String name; public ProducedBean(String name) { this.name = name; } /** * プロデューサから生成するには、コンストラクタに@Injectを * 指定してインジェクション・ポイントとするか、 * 引数無しのコンストラクタを指定する必要があります。 * @Injectを指定する場合は、インジェクトする値を * 解決出来る必要があり、極めてややこしいので、 * なるべくさけるべきかと思います。 */ public ProducedBean() { this.name = null; } public String getName() { return name; } }
@Stateless public class ProducedBeanInjectee { /** * プロデューサが生成したビーンをインジェクトします。 */ @Inject private ProducedBean producedBean; public ProducedBean getProducedBean() { return producedBean; } }
また、CDIの特徴として、フィールドをビーンとして提供するプロデューサ・フィールドが存在します。
(プロデューサ・フィールドという名前ですが、戻り値を返すわけではないので、「提供」という方が適切?)
使い道に悩みそうですが、スレッドセーフでないJPAのEntityManagerを提供する等の用途が想定されます。
コンテナ管理のEntityManagerを取得するのには@PersistentContextを使用しますが、
@ApplicationScopedのサービスクラスに直接インジェクトすると、スレッドセーフ性が壊れる可能性があります。
かと言って、サービスクラスを@RequestScopedなどに変更すると、毎回インスタンスが生成されて無駄なので、
間にワンクッション挟むことになります。
(JSRのサンプルコードから察するに、JPAは元々ステートレス/ステートフル・セッションビーンからの
呼び出しのみを考慮していたのが、CDIの進化で前提が崩れて混乱を招いているようです・・・。)
この辺の話は非常にややこしいので、以下のstackoverflowのやりとりが参考になります。
http://stackoverflow.com/questions/11063567/java-cdi-persistencecontext-and-thread-safety
この記事はCDIに関するものなので、JPAには深入りしませんが、Stackoverflowを
参考にサンプルコードの断片を作成しました。厳密に検証しきれていないため、
APサーバのマニュアルやサンプルを参考にすべきです。
/** * リクエスト・スコープに属するビーンを作成します。 */ @RequestScoped public class EntityManagerProducer { @PersistenceContext @Produces private EntityManager em; }
@ApplicationScoped public class MyService { /** * アプリケーション・スコープのビーンからリクエスト・スコープのビーンを触ることで、 * EntityManagerを安全に使用できます。 */ @Inject private EntityManager em; }
インターセプター
AOPの代替となる、メソッド呼び出しの前後に処理を挟み込む機能です。インターセプターとインターセプト対象の双方に@InterceBindingを拡張したアノテーションを
指定することで、バインディングを実現します。
注意しなければならないのは、後述のデコレータと異なり、「JSR-318 Interceptors 1.2」として、
CDIとは別仕様であるということです。この辺りが混乱を招いているせいか、Oracleの公式チュートリアルですら、
何故かbeans.xmlにベタ書きする方法しか記載されていません。
(実際はアノテーションのみで有効化可能です)
/** * インターセプターを識別するためのバインディングを定義します。 */ @InterceptorBinding @Retention(RetentionPolicy.RUNTIME) // 実行時に必要なため、RUNTIMEとします。 public @interface MyInterceptorBinding { }
/** * インターセプタークラス。 * * <p> * バインディングのアノテーションにより、インターセプト対象を一意に識別します。 * インターセプターはCDIとは別仕様なので、詳細な有効化方法は * JSR-318 Interceptors 1.2に記載されています。 * </p> * * <p> * Oracleの公式チュートリアルでは、何故かbeans.xmlで定義していますが、 * 実際はXMLレスで使用可能です。その場合は、インターセプターを * コンテナに認識させるため、スコープ系アノテーションを定義する必要があります。 * インターセプターの場合、定義できるスコープは、Dependent固定です。 * CDI1.2では、この煩わしさが軽減されており、デフォルトでインターセプターが * スキャン対象となります。 * </p> */ @MyInterceptorBinding // インターセプターを一意に定義します。 @Priority(0) // インターセプターを有効化します。CDIのJSRに書いてない仕様なので要注意! @Interceptor @Dependent // インターセプターは標準ではスキャンされないため、明示的にスコープを指定します。 public class MyInterceptor { @AroundInvoke public Object invoke(InvocationContext context) throws Exception { return "Intercepted " + context.proceed(); } }
/** * インターセプターのバインド対象のセッション・ビーン。 */ @MyInterceptorBinding @Stateless public class Interceptee { public String invoke() { return "Invoked!"; } }
デコレータ
処理を挟み込む対象を意識しないインターセプターと異なり、対象を意識してメソッド単位で処理を指定できます。インターフェースの型でデコレート対象を識別できるため、前述のインターセプターと異なり、
特別なバインディング用のアノテーションは必要ありません。
また、デコレータを作成する場合は必ず@Delegateで修飾されたデコレート対象(へのプロキシ)を
インジェクトする必要があります。
/** * デコレート対象のインターフェース。 * デコレータは、実装するインターフェースでデコレート対象を * 一意に識別できるため、特別なアノテーションなどは必要ありません。 */ public interface Decoratee { String invoke(); }
/** * デコレート対象のクラス。インターフェースの実装である必要があります。 */ @Stateless public class DecorateeImpl implements Decoratee { @Override public String invoke() { return "Invoked!"; } }
/** * デコレータクラス。 * 対象のクラスの各メソッド呼び出しをデコレートします。 * * <p> * Oracleの公式チュートリアルでは、何故かbeans.xmlで定義していますが、 * 実際はXMLレスで使用可能です。その場合は、デコレータを * コンテナに認識させるため、スコープ系アノテーションを定義する必要があります。 * デコレータの場合、定義できるスコープは、Dependent固定です。 * CDI1.2では、この煩わしさが軽減されており、デフォルトでデコレータが * スキャン対象となります。 * </p> * * <p> * なお、大量のメソッドが定義されたクラスをデコレートするのに、冗長な実装をする手間を * 軽減するためか、デコレータは抽象クラスでも良いことになっています。 * (黒魔術をフル活用することを前提とした、豪快な仕様ですね・・・。) * </p> */ @Priority(0) // デコレータを有効化します。この仕様はCDIのJSRに記述されています。 @Decorator @Dependent // デコレータはスキャンされないので、明示的にスコープを指定する必要があります。 public class MyDecorator implements Decoratee { /** * デコレート対象(へのクライアント・プロキシ)。 * @Delegate付きのプロキシが定義されない場合、 * スキャン時にエラーとなります。 * * (余談ですが、IntelliJの有料ではこのあたりもサジェストしてくれます。) */ @Delegate @Inject private Decoratee delegate; @Override public String invoke() { return "Decorated " + delegate.invoke(); } }
イベント
DIコンテナをベースとしたイベント通知機能です。アノテーションの記述のみで済む他の機能と異なり、CDIが提供するインターフェースに依存するため、この機能を使う場合は完全にCDIが前提となります。
イベントの通知先は、クオリファイヤで識別します。
また、非常に高機能で、イベントの受信タイミングをトランザクション処理のタイミングと関連付けられます。
(送信はトランザクション処理中でも、実際の受信は、そのトランザクションの正常終了後まで遅延させる等)
/** * イベントの通知に使うオブジェクト。 */ public class EventArgument { private final Date eventDate; private final String message; public EventArgument(Date eventDate, String message) { this.eventDate = eventDate; this.message = message; } public Date getEventDate() { return eventDate; } public String getMessage() { return message; } @Override public String toString() { return "EventArgument{" + "eventDate=" + eventDate + ", message='" + message + '\'' + '}'; } }
/** * イベントを識別するためのQualifierを定義します。 * インターセプターと異なり、特別なアノテーションは必要ありません。 */ @Qualifier @Target({ElementType.PARAMETER, ElementType.FIELD}) @Retention(RetentionPolicy.RUNTIME) public @interface EventQualifier { }
@Qualifier @Target({ElementType.PARAMETER, ElementType.FIELD}) @Retention(RetentionPolicy.RUNTIME) public @interface TransactionalEventQualifier{ }
@Singleton public class EventNotifier { /** * イベントの通知に使うインターフェースをインジェクトします。 * 通知先などはQualifierで一意に識別します。 */ @EventQualifier @Inject private Event<EventArgument> event; @TransactionalEventQualifier @Inject private Event<EventArgument> transactionalEvent; public void fire(String msg) { event.fire(new EventArgument(new Date(), msg)); } /** * トランザクショナルにイベントを通知します。 * 通常のイベントと異なり、オブザーバが即時にイベントを受信せずに、 * トランザクションの動作とリンクします。 */ @Transactional public void fireTransactionally(String msg) { transactionalEvent.fire(new EventArgument(new Date(), msg)); // このメッセージの後にトランザクションの処理が完了してからイベントが通知されます。 System.out.println("end transactional method"); } }
@Singleton public class EventReceiver { private final List<EventArgument> receivedArgs = new ArrayList<EventArgument>(); private final List<EventArgument> transactionalReceivedArgs = new ArrayList<EventArgument>(); /** * イベントのオブザーバ・メソッド。 * EventQualifierで修飾されたNotifierからのイベントを受信します。 * @param arg */ public void receiveEvent(@Observes @EventQualifier EventArgument arg) { receivedArgs.add(arg); } /** * トランザクション完了時にイベントを受信するレシーバ。 * 受信のタイミングがトランザクション完了時なので、イベントの通知と、 * オブザーバ・メソッドの呼び出しタイミングが変わります。 * 成功時のみ、失敗時のみなど、細かく指定可能です。 * @param arg */ public void receiveTransactionalEvent( @Observes(during = TransactionPhase.AFTER_COMPLETION) @TransactionalEventQualifier EventArgument arg) { System.out.println("Transactional Event Received!: msg=" + arg.getMessage()); transactionalReceivedArgs.add(arg); } public List<EventArgument> getReceivedArgs() { return receivedArgs; } public List<EventArgument> getTransactionalReceivedArgs() { return transactionalReceivedArgs; } }
ここまでのテストケースは以下のGithubリポジトリからダウンロードできます。
https://github.com/zyake/cdi_sample
最後に
以上のように、CDIの各種機能を見ていきましたが、これだけ理解していれば、大抵の用途で使えるのではないかと思います。(実際はもっと色々あって、とてつもなくややこしいです・・・)
CDIは後発で、恐らく標準仕様の意地があるためか、
「Springなどに負けない、野心的なものを作ろう」
という意気込みは感じられるのですが、その結果として、仕様が膨大で複雑過ぎ、理解するのが困難なものになっています。
(JSRの図表の少なさは一体・・・。読むのが大変過ぎるorz 敷居を上げるためにわざとやっているのでは?と勘ぐりたくなります。)
また、仕様策定者の間ですら混乱しているのか、CDI1.2では多数のドキュメントバグ修正、
記述の明確化が実施されており、複雑なユースケースでは、実装依存がありそうな雰囲気です。
(グレーゾーンがとても多い)
しかし、CDIが強力なのは間違いなく、CDI2.0ではセキュリティ機構や、タイマー実行等の機能が予定されていますし、
単なるDIコンテナから、「Java EEサーバで利用できるビーン管理のインフラ」に進化してJava EE開発で
抑えておくのが必須の技術になりそうです。
次回は、もっと特定の実装に踏み込んで(RIのWeldを想定)、ソースコードを解析してみます。