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:メソッド インジェクションも可