CDI, 私を愛したSPI
2018/02/18 コメントを残す
この記事はCDI 2.0スペックリードであるAntoine Sabot-Durand氏の記事(Feb 20, 2017)の翻訳です。CDI SPIの概要をサンプルやCDI 1.1の新機能の紹介を交えて説明しています。私はプロデューサーにパラメーターを渡すために限定子を使う方法や、InjectionPointメタデータを使って実装を切り替える方法が面白いと思いました。ちなみに、この記事のタイトルは「007 私を愛したスパイ」をもじっています(この著者の他の記事も同様です)。
(原文)CDI, the SPI who loved me
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のこのパートはそれをするためのツールを提供します。
BeanManager と CDI
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の中で中心的なインタフェースで、アプリケーション内のすべてのメタデータと生成済みのコンポーネントへのアクセスを提供します。
非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();
CDIはCDI.getBeanManager() メソッドを持つBeanManagerにすぐにアクセスできる手段を提供しますが、もっと興味深いことには、それはBeanManagerを使った奇妙なコードを使うことなくコンテキスト依存インスタンス(Contextual Instance)を要求するための便利な方法を提供します。
CDIはInstance<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では我々自身の手によるモデルを作り出す必要がありました。
AnnotatedTypeインタフェースは、このアノテーションを中心として型メタモデルの主要要素です。
AnnotatedTypeを定義して、型、フィールド、メソッド、メソッドパラメーターの上に必要なすべてのアノテーションをつけましょう。
AnnotatedTypeは主にポータブル・エクステンションで使用されます。それらは既存の型からコンテナーによって構築されます。
見てお判りのとおり、このモデルはCDI固有の機能は持たないので、もしもサードバーティ開発者が彼のフレーワークをCDIに結合すると決めたならば、彼のユーザーはフレームワークを構成するためにAnnotatedTypeを使って試すことができます。
CDIメタモデル専用のSPI
私の1つ前の記事でBeanメタモデルに関するインタフェースの概要について紹介しましたので、ここで再び詳細を説明するつもりはありません。
ただ1つ思い出して欲しいのは、このメタモデルは主にカスタムBeanを宣言するためのポータブル・エクステンションで使われるものではありますが、あなたのBeanにおいて、現在のBeanやインターセプター、デコレーター、現在インターセプトされているBeanやデコーレトされているBeanなどに関するイントロスペクション機能を得るために使うことができるということです(訳注:イントロスペクションの説明はこちら)。
残りのCDIメタデータSPIインタフェースは以下のとおりです。
ObserverMethodとEventMetadata
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を含むときに限って実行される
ProducerとInjectionTargetとそれらのファクトリ
ProducerとInjectionTargetもほとんどエクステンションで使われます。しかし、あなたが上に表示された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);
InjectionPointメタデータ
このSPIファミリーで大事なことを言い忘れていました。それはInjectionPointです。この万能ナイフは通常のコードよりもむしろエクステンションでよく使われます。しかしこの場合、それは@DependentスコープのBeanに関連するインジェクションポイントの情報を取得するために使うことができるだけです。それはインジェクションポイントのユニークさを保証する唯一の方法です(たとえば、同じ@RequestScopedインスタンスは複数の場所にインジェクトされる可能性があります)。それが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> mapはTreeMapを使います。これはビジネスコードに(実装の詳細が)漏れ出ることなく振る舞いの最適化や変更をするエレガントな方法です。
おわりに
InjectionPointを使って構築可能なたくさんの機能があります。そして、この記事では通常のコードでのいくつかの例を見てきたに過ぎないことを覚えておいてください。エクステンションで何ができるかを考えてみてください。
エクステンション専用のSPI
ようやくここでこのSPIのツアーを終わらせることにしましょう。
次のSPIクラスは全部丸ごとエクステンション開発専用です。
つまり、ポータブルエクステンションの魔法が起こる(主にブートストラップでの)コンテナーライフサイクルの各ステップごとのイベント型が定義されています。
エクステンションに関する次の記事でこの魔法について知ることになるでしょう。