読者です 読者をやめる 読者になる 読者になる

CDI(Contexts and Dependency Injection)まとめ


CDI のバージョン

現在 CDI 2.0 仕様はドラフト。

Ver JSR JavaEE RI
CDI 1.0 JSR-299 JavaEE6 Weld 1.0
CDI 1.1 JSR-346 JavaEE7 Weld 2.0
CDI 1.2 JSR-346 JavaEE7 Weld 2.2
CDI 2.0 JSR-365 JavaEE8 Weld 3.0

各仕様についてはここにまとまっている。

http://www.cdi-spec.org/download/

インジェクションポイント

コンストラクタインジェクション

1つのコンストラクタに @Inject を付けることで、コンストラクタを経由した DI が行える。

public class Application {

    private final HelloService helloService;

    @Inject
    public Application(HelloService helloService) {
        this.helloService = helloService;
    }

}

フィールドは final にできるので immutable 化できる。

フィールドインジェクション

フィールドに直接 @Inject を指定する。

public class Application {

    @Inject
    private HelloService helloService;

    public Application() {
    }

}

メソッドパラメータ(イニシャライザメソッド)インジェクション

セッターメソッドに @Inject を指定する。

public class Application {

    private HelloService helloService;

    @Inject
    void setHelloService(HelloService helloService) {
        this.helloService = helloService;
    }

}

インジェクトのタイミング

weld におけるインジェクトは以下の順序で行われる。

  • デフォルトコンストラクタまたは@Injectでマークされた一つのコンストラクタにてインスタンス生成
  • @Inject でマークされたフィールドへインジェクトする
  • イニシャライザメソッドが呼ばれる(CDIの仕様では順序は定義されていない)
  • @PostConstruct メソッドが呼ばれる

スコープアノテーション

CDI ではインジェクト対象のクラスにスコープアノテーションを付けることでコンテキストに応じたインジェクションが行われる。

スコープアノテーションはノーマルスコープと擬似スコープの2種類がある。

ノーマルスコープ
スコープアノテーション 説明
@RequestScoped 同一リクエストで共有
@SessionScoped 同一セッションで共有
@ApplicationScoped アプリケーション内で共有
@ConversationScoped 明示的に開始した会話間で共有

ノーマルスコープのアノテーションは @NormalScope が付いたもの。

コンテナによりインジェクトされるインスタンスが Proxy 化される。Proxy を介してスコープに応じた実際のインスタンスに処理が委譲される。

@ConversationScoped の場合、スコープの開始を Conversation#begin()、終了を Conversation#end() で制御する。Conversation#setTimeout(millisec) でタイムアウト設定も可能。明示的に begin() を呼ばない場合は @RequestScoped と同じスコープになる。

擬似スコープ
スコープアノテーション 説明
@Dependent インジェクション先のスコープと同じになる
@javax.inject.Singleton built-in外。一応扱えるが @ApplicationScoped を利用した方が良い

擬似スコープのアノテーションは @Scope が付いたもの。

Proxy化されず実際のインスタンスがインジェクトされる。

Bean のライフサイクルコールバック

CDI 管理の Bean のインスタンス生成時と破棄時にコールバックを受けることができる。

@RequestScoped
public class BookStoreBean {

  @PostConstruct
  public void setup() { ・・・ }

  @PreDestroy
  public void destroy() { ・・・ }
}

コールバックのタイミングは以下。

アノテーション 説明
@PostConstruct インスタンスの生成時(インジェクション完了後)
@PreDestroy インスタンスの破棄時

インジェクトされたインスタンスに対して初期化処理を行う場合は @PostConstruct を使う必要がある。

beans.xml

CDI1.0の場合は空でも良いので beans.xml を用意しないと CDI が有効にならない。

CDI1.1やCDI1.2はファイルが無くともCDIが有効になる。

beans.xml の配置場所は以下。

packaging 配置場所 gradleの場合
JAR META-INF以下 src/main/resources/META-INF/
EJB META-INF以下 src/main/resources/META-INF/
WAR WEB-INF以下 src/main/webapp/WEB-INF/

beans.xml を用意する場合のテンプレは以下。

CDI 1.0
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://java.sun.com/xml/ns/javaee"
            xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
            xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/beans_1_0.xsd">
</beans>
CDI 1.1
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://xmlns.jcp.org/xml/ns/javaee"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/beans_1_1.xsd"
       version="1.1" bean-discovery-mode="annotated">
</beans>
CDI 1.2

CDI1.2 はメンテナンスリリースなので、スキーマ定義はそのまま

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://xmlns.jcp.org/xml/ns/javaee"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/beans_1_1.xsd"
       version="1.2" bean-discovery-mode="annotated">
</beans>

Qualifiers

1つのインターフェースに2つの実装があり、インジェクション対象を指定したい場合はインジェクションポイントに Qualifier(限定子)を付ける。

@Qualifier を持つアノテーションを自作する。

@Qualifier
@Retention(RUNTIME)
@Target({TYPE, METHOD, FIELD, PARAMETER})
public @interface MyQualifier {}

限定子(Qualifier) を実装クラスに付け、インジェクションポイントで指定する。

フィールドインジェクションの場合は以下のように指定する。

@Inject
@MyQualifier
private Local myLocale;

コンストラクタインジェクションの場合は以下のように指定する。

@Inject
public Notifications(@MyQualifier Locale myLocale) {
  this.myLocale = myLocale;
}

@Nonbinding

限定子はアノテーションのメンバーに設定された値も含めて評価される。

以下のようなメンバを持つアノテーションを作成した場合、

@Qualifier
@Retention(RUNTIME)
@Target({FIELD, PARAMETER, METHOD, TYPE})
public @interface MyQualifier {
  @Nonbinding
  String description() default "";
}

@Nonbinding を付けないと、@MyQualifier(description="A")@MyQualifier(description="B") は異なる限定子として扱われる。

限定子としての判定に加えたくない場合は @Nonbinding を指定する必要がある。

@Named

@Named にて名前を付けることで JSFなどで EL式 として参照できる。

@Named("book")
public class HistoryBook implements Serializable { ・・・ }
<h:outputText value="#{book.isbn}" />

@Named で明示的に名前を指定しない場合は、historyBook といった、先頭を小文字にしたクラス名となる。

@Any と @Default

CDI が管理する Beans は全て暗黙的に @Any 限定子が必ず付く。

@Default は自作した限定子など他の限定子(@Namedは除く)が付いていない場合に、暗黙的に付与される。

通常これらを意識することは少ないが、Instance<T> へのインジェクションの対象を指定する場合などで利用する。

@Admin
public class Admin implements Account { ・・ }
public class User implements Account { ・・ }
@Inject @Any
Instance<Account> accounts;

@Any を付けないと、@Default が暗黙的に付いている User のインスタンスしか得られない。

@New

インジェクションポイントに @New を付けるとインジェクトされるインスタンスのスコープが @Dependent となる。

@Alternative

環境などによりインジェクションする対象を切り替えたい場合には @Alternative を使い beans.xml で指定する。

@Alternative
@Admin
public class MockAccount implements Account { ・・・ }

通常時は MockAccount は CDI対象とはならないが、以下のような beans.xml を使うとCDI対象としてインジェクトされる。

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://xmlns.jcp.org/xml/ns/javaee"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/beans_1_1.xsd"
       version="1.2" bean-discovery-mode="annotated">
  <alternatives>
    <class>com.mycompany.MockAccount</class>
  </alternatives>
</beans>

CDI インスタンスのプログラマティックな取得

javax.enterprise.inject.Instance<T> へのインジェクションポイントを作成する。

@Inject
Instance<BookSearch> bookSearch;

以下のようにT型のインスタンスが取得できる。

BookSearch search = bookSearch.get();


複数のインジェクション対象があり、@Fiction 限定子が付いているインスタンスを取得するには select() でアノテーションを指定する。

bookSearch.select(new AnnotationLiteral<Fiction>(){}).get();

この時インジェクションポイントは @Any としてアノテートする必要がある。

@Inject
@Any
Instance<BookSearch> bookSearch;

限定子のアノテーションにメンバーがある場合、以下のようにアノテーションリテラル(BookLiteral)を自作して使う。

BookSearch search = bookSearch.select(new BookLiteral(Category.NONFICTION)).get();

BookLiteralAnnotationLiteral を継承して作成する。

public class BookLiteral extends AnnotationLiteral<Book> implements Book {
  private final Category category;
  public BookLiteral(Category category) {
    this.category = category;
  }
  public Category value() { return category; }
}


プロデューサー

@Produces でアノテートしたメソッドを用意すると、その戻り値が合致する型のインジェクションポイントにDIできる。

public class AccountManager {
  @Produces
  @User
  Account getUserAccount() {・・・}
}

@Produces をフィールドに付与した場合も、フィールドのインスタンスが、合致するインジェクションポイントにDIできる。

public class AccountManager {
  @Produces
  @User
  Account userAccount = ・・・;
}

インジェクションポイントには先のメソッドで作成したインスタンスがDIされる。

  @Inject
  @User
  Account userAccount;

@Produces で Bean を直接 new して返却することもできるが、インタセプターなど利用できなくなる。その場合はプロデューサーメソッドの引数にCDI管理のBeanを定義するとコンテナによりインスタンス化されたBeanを得ることができる。

@Produces
public BookSearch getSerch(FictionSearch fs, NonFictionSearch nfs) {
  switch (searchType) {
    case FICTION: return fs;
    case NONFICTION: return nfs;
    default: return null;
  }
}

プロデューサーのスコープ

プロデューサー により生成されるインスタンスのスコープはデフォルトで @Dependent となる。

@RequestScoped @SessionScoped @ConversationScoped @ApplicationScoped などのスコープアノテーションを付与することでコンテキストに応じたインスタンスがDIできる。つまりプロクシ化される。

例えば @ApplicationScoped を付与すれば、アプリケーションの開始時に1度だけプロデューサーが呼ばれ、以後は生成されたBeanがアプリケーションコンテキストで全てのクライアントから共有される。

以下のような@SessionScopedのプロデューサーメソッドがあった場合、

@Produces
@SessionScoped
public BookSearch getSearch(FictionSearch fs, NonFictionSearch nfs) {
  switch (searchType) {
    case FICTION: return fs;
    case NONFICTION: return nfs;
    default: return null;
  }
}

FictionSearch@RequestScoped だと、getSearch()が呼ばれるのは同セッションの最初の1回だけで、インジェクションポイントにはリクエスト毎に新しい FictionSearch 設定される。

引数に @New を付けるとFictionSearch@Dependent となり、メソッドに付与された @SessionScoped と同じスコープとして扱える

@Produces
@SessionScoped
public BookSearch getSearch(@New FictionSearch fs, @New NonFictionSearch nfs) {
  switch (searchType) {
    case FICTION: return fs;
    case NONFICTION: return nfs;
    default: return null;
  }
}

CDI1.1 の仕様では @New の利用は非推奨で@DependentのBeanを使うべきとある。

プロデューサーのDisposer

Producer メソッド(フィールド)によって生成されたビーンの後始末は、 @Disposer を使う(@PreDestroy などのコールバックメソッドが利用できない)。

public class Databases {

  @Produces
  @ConversationScoped
  @AccountDB
  public EntityManager accountDB(EntityManagerFactory factory) {
    return factory.createEntityManager();
  }
  
  @Produces
  @ConversationScoped
  @OrderDB
  public EntityManager orderDB(EntityManagerFactory factory) {
    return factory.createEntityManager();
  }
  
  public void close(@Disposes @Any EntityManager em) {
    em.close();
  }
}

Disposer メソッドは、Producer によって生成されたクラスと合致する、@Disposes でアノテートされた引数を持つメソッドがコンテナにより自動的にスキャンされる。 上記例だと @Any でアノテートしているためAccountDBとOrderDBの双方のクローズ処理として機能する。

インジェクションポイントメタデータ

プロデューサーメソッドの引数に InjectionPoint を指定することで、インジェクションポイントのメタデータを取得できる。

@Dependent
class LoggerFactory {
  @Produces
  Logger createLogger(InjectionPoint injectionPoint) {
  ・・・
  }
}

以下のようなメソッドで各種情報が取得できる。

  • InjectionPoint#getBean()
  • InjectionPoint#getType()
  • InjectionPoint#getQualifiers()
  • InjectionPoint#getMember()
  • InjectionPoint#getAnnotated()
  • InjectionPoint#isDelegate()

インタセプター

@InterceptorBinding でアノテーションを作る。

@Inherited
@InterceptorBinding
@Retention(RUNTIME)
@Target({METHOD, TYPE})
public @interface Logged { }

インタセプタを噛ませたい対象に作成したアノテーションでマーキングする。

@SessionScoped
public class AccountService {
  @Logged
  public void createAccount() { ・・・ }
}

クラスにアノテーションを付けると全てのメソッドがインタセプタの対象になる。

インタセプタの処理を @Interceptor と作成した @Audited を付けたクラスを作成する。

@Audited
@Interceptor
public class AuditedInterceptor implements Serializable {
 
  @AroundInvoke
  public Object arountInvoke(InvocationContext ic) throws Exception {
  }
}

@AroundInvoke にインタセプト時の処理を書く。 インタセプタ自体にCDIでインスタンスをDIすることもできる。

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://xmlns.jcp.org/xml/ns/javaee"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/beans_1_1.xsd"
       version="1.2" bean-discovery-mode="annotated">
    <interceptors>
      <class>com.mycompany.AuditedInterceptor</class>
    </interceptors>
</beans>

インタセプタの適用順は beans.xml の interceptors 要素内に書いた順序となる。 @Priority による指定もできる。

デコレータ

デコレータパターンのサポートがある。

以下のインターフェースがあり、

public interface Account {
  ・・・
  void changePassword(String pass);
}

以下の実装があった場合、

@Admin
public class Admin implements Account { ・・ }

public class User implements Account { ・・ }

@Decorator でアノテートしたクラスでデコレートできる。

@Decorator
public abstract class PasswordMonitorDecorator implements Account {

  @Inject
  @Delegate
  @Any
  private Account account;
  
  public void changePassword(String pass) {
    System.out.println("change Password.");
    account.changePassword(pass);
  }
}

@Delegate でデコレートする対象に処理を委譲するインスタンスを指定する。 デコレータクラスは abstract でも良い。

@Any とすることで、Account の実装が複数あっても同様の処理をデコレータで処理できる。

デコレータの有効化はbeans.xmlで指定する必要がある。

  <decorators>
    <class> PasswordMonitorDecorator </class>
  </decorators>

インタセプタとデコレータが同じメソッドにある場合にはインタセプタが最初に呼ばれる。

イベント

オブザーバによるイベントの処理がサポートされている。

イベントをリスンするには@Observes でアノテートされた引数を持つオブザーバメソッドを定義する。

以下のようなメソッドを定義すると Book に関するイベントが通知される。

public void onBookEvent(@Observes Book book) { ・・・ }

イベントの通知側では、Event<T> のインスタンスの fire() メソッドによりイベントを発行する。

@Inject
Event<Book> bookEvent;
bookEvent.fire(book);

イベントの限定

限定子を使うことでリスンするイベントを限定できる。

以下のような限定子を定義し、

@Qualifier
@Target({FIELD, PARAMETER})
@Retention(RUNTIME)
public @interface Removed{}

オブザーバメソッドで限定子を指定する。

public void onBookEvent(@Observes @Removed Book book) { ・・・ }
@Inject
@Removed
Event<Book> bookRemovedEvent;
bookRemovedEvent.fire(book);

@Any で受けて

@Inject
@Any
Event<Book> bookEvent;

アノテーションリテラルで選択することもできる。

bookEvent.select(new AnnotationLiteral<Removed>(){}).fire(book);

アノテーションにメンバフィールドがある場合の扱いは Instance<T> と同様に扱える。

限定子を重ねて受けることもできるし、

public void onBookEvent(@Observes @Book(FICTION) @Added Book book) { ・・・ }

@Any を指定することで全てのBookイベントをリスンできる。

public void onBookEvent(@Observes @Any Book book) { ・・・ }

トランザクショナルオブザーバ

イベントがオブザーバで処理されるトランザクションのタイミングはトランザクショナルオブザーバメソッドで制御できる。

during に javax.enterprise.event.TransactionPhase で定義される値を指定する。

public void refreshOnBookRemoval(
    @Observes(during = AFTER_SUCCESS) 
    @Removed Book book) {
       ・・・ 
}

during に指定できる値は以下。

TransactionPhase 説明
IN_PROGRESS デフォルト値。イベントのFireで直ちに実行される。
BEFORE_COMPLETION トランザクションの完了前に実行される。
AFTER_COMPLETION トランザクションの完了後の実行される。
AFTER_SUCCESS AFTER_COMPLETIONと同じタイミングでトランザクション成功時に実行される。
AFTER_FAILURE AFTER_COMPLETIONと同じタイミングでトランザクション失敗時に実行される。

コンディショナルオブザーバ

オブザーバメソッドのインスタンスはコンテキストに応じてコンテナによりインスタンス化されるが、これを制御したい場合は notifyObserver に javax.enterprise.event.Reception で定義される値を指定する。

public void refreshOnBookRemoval(
    @Observes(notifyObserver = IF_EXISTS) 
    @Removed Book book) { ・・・ }
Reception 説明
ALWAYS デフォルト値。常に通知。
IF_EXISTS オブザーバメソッドのインスタンスが存在すれば通知。

CDI in Java SE(CDI2.0)

CDI 2.0 では JavaSE 環境での CDI サポートが追加される。

JavaSE 環境で CDI を利用するには、CDIProvider から CDI インスタンスを取得する。

main メソッドは以下のようになる。

import javax.enterprise.inject.spi.CDI;
import javax.enterprise.inject.spi.CDIProvider;

public class Main {

    public static void main(String... args) {

        CDIProvider provider = CDI.getCDIProvider();
        CDI<Object> cdi = provider.initialize();

        Application app = cdi.select(Application.class).get();
        app.run();

        cdi.shutdown();
    }
}

CDI オブジェクトは AutoCloseable を実装しているため以下のように書くこともできる。

try (CDI<Object> cdi = CDI.getCDIProvider().initialize()) {
    Application app = cdi.select(Application.class).get();
    app.run();
}

JavaSE 環境 でのサポートは以下。

  • @PostConstruct@PreDestroy のライフサイクルコールバック
  • 限定子と alternative のDI
  • @Application, @Dependent @Singleton スコープ
  • インタセプタとデコレータ
  • ステレオタイプ
  • イベント
  • ポータブルエクステンション

実際の利用に際しては以下参照。

etc9.hatenablog.com


JBoss Weld CDI for Java Platform

JBoss Weld CDI for Java Platform