seraphyの日記

日記というよりは過去を振り返るときのための単なる備忘録

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 DeltaSpikeBeanProvider.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は、それを行っている。)

*1:メソッド インジェクションも可