CDIで@Dependentなクラスを@Injectするときに引数を渡す方法(メモ)
概要
CDIで、@Dependentなクラスを定義し、利用する側のクラスのフィールドで@Injectするときに、任意のパラメータを渡したい。
@Dependent public class Foo { @Inject private AnyService srv; private String arg; public void setArg(String arg) { this.arg = arg; } } @Dependent public class Bar { @Inject private Foo foo; // ← ここでパラメータargを渡したい。 }
解決方法
引数つきの@Qualifierを定義し、@Producesを使うことで実現できる。
@Qualifier @Retention(RUNTIME) @Target({ TYPE, METHOD, FIELD, PARAMETER }) public @interface FooParameter { @Nonbinding String value(); } @ApplicationScoped public class FooProducer { @Inject @Default private Instance<Foo> fooProvider; @Dependent @Produces @FooParameter("") public Foo create(InjectionPoint ip) { FooParameter fooParam = ip.getAnnotated() .getAnnotation(FooParameter.class); String arg = fooParam.value(); Foo inst = fooProvider.get(); inst.setArg(arg); return inst; } } @Dependent public class Bar { @Inject @FooParameter("baz") // ← Foo構築時にパラメータを渡せる private Foo foo; }
@Qualifierを定義するとき、@Nonbindingを指定することで、マッチングする場合に引数の中身は無視されるようになる。
なので、@FooParameter("")の引数にかかわらず、@FooParameterの限定子をつけているものは、すべて、この@Producesメソッドにマッチするようになる。
@Producesでは、InjectionPoint引数を受け取ることでアノテーションを取得できる(@Qualifier以外も可)ので、ここでパラメータを受け取ることができる。
あとは、@DefaultにマッチするFooのインスタンスを生成して明示的にパラメータを渡してから返せば良い。
@Disposesの実装
Fooインスタンスが不要になったタイミングでの後始末が必要ならば、以下のように@Disposesを実装する。
public void dispose(@Disposes @FooParameter("") Foo inst) { fooProvider.destroy(inst); }
Instance#get()で取得されたインスタンスは、対となるInstance#destroyメソッドによって明示的な破棄処理を行うことができる。
これにより、Fooクラス内に@PreDestroyメソッドが定義されている場合、これが呼び出されることになる。
(この処理を入れないと@PreDestroyを呼び出すべきタイミングがなくなってしまう。)
別解、もしくは、引数つきコンストラクタで使う場合
Apache DeltaSpikeのBeanProvider.injectFieldsで直接入れることも可。
DeltaSpikeではinjectFielsで以下のようなことをやっている。
public static <T> T injectFields(T instance) { BeanManager beanManager = CDI.current().getBeanManager(); CreationalContext<T> creationalContext = beanManager.createCreationalContext(null); @SuppressWarnings("unchecked") AnnotatedType<T> annotatedType = beanManager .createAnnotatedType((Class<T>) instance.getClass()); InjectionTarget<T> injectionTarget = beanManager .createInjectionTarget(annotatedType); injectionTarget.inject(instance, creationalContext); return instance; }
もし、Fooを以下のように引数つきコンストラクタにしてパラメータなしでのインスタンス化をできないようにしたとする。
また、CDIの管理外とする。
public class Foo { @Inject private AnyService srv; private String arg; public Foo(String arg) { this.arg = arg; } }
これは、@Producesによって明示的に非管理Beanをインスタンス化できる。
@ApplicationScopred public class FooProducer { @Dependent @Produces @FooParameter("") public Foo create(InjectionPoint ip) { FooParameter fooParam = ip.getAnnotated().getAnnotation( FooParameter.class); String arg = fooParam.value(); Foo inst = new Foo(arg); // Fooクラスのフィールド中の@Injectを実施する。 BeanProvider.injectFields(inst); return inst; } }
newによって生成されたインスタンスなので、Fooの@Injectの指定のあるフィールドは未初期化状態のままである。
ここでDeltaSpikeのinjectFieldsによって@Injectのフィールドを明示的に注入する。*1
ただし、この方法では注入ができるだけで、@PostConstruct, @PreDestroyなどのイベント的な処理も行うわけではないので、既存の非管理クラスをインスタンス化する場合でなければ、javax.enterprise.inject.Instanceを使って管理ビーンとして生成するのが結局は簡単になると思われる。
また、もし、マニュアルでinjectしたビーンの@PreDestroyを動かしたいのであれば、BeanProvider#injectFieldsで使用したCreationalContextを保存しておいて、@Disposes内で、creationContext.releaseする必要がある。(javax.enterprise.inject.Instanceは、それを行っている。)
参考
- Weld :: Chapter 4. Dependency injection and programmatic lookup
- Burak Aktas :: CDI Dependency Injection Producer Method Example
以上、メモ終了
*1:メソッド インジェクションも可