Jakarta EE 10 - Jakarta Contexts and Dependency Injection 4.0 変更内容まとめ

blog1.mammb.com


はじめに

Jakarta EE10 で導入される CDI 4.0 (Jakarta Contexts and Dependency Injection 4.0) についてのまとめです。 参照実装は Weld 5 です。


CDI Lite と CDI Full の仕様分割

2020年3月に公開された CDI Lite の原案が、多くの議論の後、2021年5月のコミュニティ投票にて実現が決定されました。

CDI Lite は CDI Full のサブセットとなります。 現在の CDI は「ランタイム指向」となっており、実行時の Bean ディスカバリーなどがアプリケーション起動時間短縮の足かせとなります。

これに対して、Quarkus の CDI 実装である ArC は、CDI仕様を限定的にサポートし、ビルドタイムに多くの処理を行うことで、起動時間の短縮を行うようになっています。

これからのクラウドネイティブなニーズに答えるために、現在の包括的で大きな CDI 仕様を軽量化したものが CDI Lite となります。

仕様書上も、Part I - Core CDI の中に、CDI Lite と CDI Full で章分けされています。 仕様上で明示されてはいませんが、 CDI Full は 「ランタイム指向」のままであり、CDI Lite は「ビルドタイム指向」と区別しても良いでしょう。

CDI Lite では利用できない(CDI Full に限定される)機能は以下となります。

  • デコレータ
  • @Interceptors アノテーションによるインタセプタ
    • ただし @InterceptorBinding@Interceptor で定義したインタセプタは CDI Light でも動作する
  • スペシャライゼーション(@Specializes によるBeanの差し替え)
  • Bean の非活性化(スコープに応じたBeanの非活性化)
  • 会話スコープ(Conversation Scope)
  • ポータブル・エクステンション
    • 現状のポータブル・エクステンションは、ビルド指向のフレームワークには不向きであり、CDI Light では、新しい拡張APIを利用する(Build Compatible Extensions)

Jar としては、今まで通りの単一 Jar に両APIが含まれる形で提供されます。


Build Compatible Extensions

CDI Lite の目標の一つは、可能な限りリフレクションを排除することにあります。 しかし現在の Portable Extensions でのメタモデル(AnnotatedType など)はリフレクションの利用が前提となっています。

そのため、CDI Lite では、Build Compatible Extensions で新しいメタモデル(AnnotationInfo, ClassInfo, MethodInfo, FieldInfo など)が導入されました。

これらの新しいメタモデルは jakarta.enterprise:jakarta.enterprise.lang-model APIアーティファクトで提供されます。 Javaモジュール名は jakarta.cdi.lang.model です(他のCDIは jakarta.cdi というモジュール名になります)。

Build Compatible Extensions は、META-INF/services で宣言された jakarta.enterprise.inject.build.compatible.spi.BuildCompatibleExtension インターフェースを介して提供されます。

Build Compatible Extensions は、ビルド時など、アプリケーションが開始される前にBean検出を実行できるようにするため CDI イベントのオブザーバーメソッドを宣言しません。

拡張機能の実行フェーズには、対応した以下のアノテーションが定義されています。

  • @Discovery:アプリケーションへのクラスの追加、カスタムのCDIメタアノテーション登録
  • @Enhancement:アノテーションの変換
  • @Registration:登録されたビーン及びオブザーバをオブザーブ
  • @Synthesis:合成Beanとオブザーバーの登録
  • @Validation:カスタム検証の実行

Build Compatible Extensions についての詳細は別記事で紹介します。


bean-discovery-mode 属性のデフォルトが annotated となった

CDI 4.0 の beans.xml のスキーマは https://jakarta.ee/xml/ns/jakartaee/beans_4_0.xsd で定義されます。

CDI 3.0 では bean-discovery-mode は必須定義となっていました。

<xs:attribute name="bean-discovery-mode" use="required">

CDI 4.0 ではオプショナルとなり、デフォルトとして annotated が選択されたものとして扱われます。

<xs:attribute name="bean-discovery-mode" use="optional" default="annotated">

annotatedで対象となる の定義は以前と変わらず、以下の通りです。

  • @ApplicationScoped@RequestScoped アノテーション
  • その他全てのノーマルスコープ(@NormalScope)を持つアノテーション
  • @Interceptor アノテーション
  • 全てのステレオタイプ(@Stereotype)を持つアノテーション
  • @Dependent スコープアノテーション


Startup イベントと Shutdown イベント

CDI では、EJBの @Startup に該当するものがありませんでした。

EJB ではアプリケーションの起動時に以下のように @Startup@PostConstruct で処理を行うことができます。

@javax.ejb.Startup
@javax.ejb.Singleton
public class StartupService {

    @PostConstruct
    public void init() {
        // ...
    }
}

CDI では、@Initialized(ApplicationScoped.class) というイベントをオブザーブすることでこれを模倣することができました。

@ApplicationScoped
public class ObservingBean {

    public void initAppScope(@Observes @Initialized(ApplicationScoped.class) Object init) {
    }
}

しかし、このイベントは、コンテナの状態イベントではなく、コンテキストに関連するイベントです。 特に「ビルドタイム指向」で CDI を使った場合には、実行前にコンテキストを開始することができるため、このイベントでコンテナ初期化時の処理を行うことは意味を成さなくなります。

CDI 4.0 では、jakarta.enterprise.event.Startupjakarta.enterprise.event.Shutdown イベントが追加され、コンテナ起動時と終了時のイベントを正確にリスンすることができるようになりました。

今後は以下のように StartupShutdown を使うことで、@javax.ejb.Startup と同様な機能を利用できます。

@ApplicationScoped
public class ObservingBean {

    public void startup(@Observes Startup startup) {
    }

    public void initAppScope(@Observes @Initialized(ApplicationScoped.class) Object init) {
    }

    public void shutdown(@Observes Shutdown shutdown) {
    }

    public void observeBeforeShutdown(@Observes @BeforeDestroyed(ApplicationScoped.class) Object event) {
    }
}


Handle からBeanのメタデータを簡単に参照できるようになった

Instance<T>Handle<T> を取得するメソッドが追加されました。

public interface Instance<T> extends Iterable<T>, Provider<T> {

    Instance<T> select(Annotation... qualifiers);
    <U extends T> Instance<U> select(Class<U> subtype, Annotation... qualifiers);
    <U extends T> Instance<U> select(TypeLiteral<U> subtype, Annotation... qualifiers);
   
    Stream<T> stream();
   
    // Handle 取得メソッド
    Handle<T> getHandle();
    Iterable<Handle<T>> handles();
    Stream<Handle<T>> handlesStream();
}

この Handle<T> からは、Bean<T> が取得でき、Beanのメタデータを参照することができます。

interface Handle<T> extends AutoCloseable {

    /**
     * The contextual reference is obtained lazily, i.e. when first needed.
     */
    T get();

    /**
     * @return the bean metadata
     */
    Bean<T> getBean();

    /**
     * Destroy the contextual instance.
     */
    void destroy();

    /**
     * Delegates to {@link #destroy()}.
     */
    @Override
    void close();

}

旧来は以下のようにコンテナから管理のBeanを取得していました。

container.instance().select(Foo.class).get();

または Instance を以下のようにインジェクトすることもできます。

@Inject
Instance<FooInterface> instance;

public void exec() {
    instance.select(Foo.class).get();
}

しかし、多くの Bean の候補を繰り返し処理した場合、 @Dependent な Bean のインスタンスが都度作成され、効率的ではありませんでした。

CDI 4.0 からはHandle<T> を介することで、Bean<T> を取得してメタデータを参照できると共に、コンテキストインスタンスの取得と、必要に応じて破棄することが可能となりました。


ステレオタイプに @Priority で優先度を指定可能となった

ステレオタイプは、@Stereotype を付けたアノテーションで、さまざまなアノテーションをまとめたアノテーションを定義できます。 たとえば、 @Named@RequestScoped をまとめるために、@Stereotype を付与して @Model を定義しています。

@Named
@RequestScoped
@Documented
@Stereotype
@Target({ TYPE, METHOD, FIELD })
@Retention(RUNTIME)
public @interface Model { }

この CDI ステレオタイプに @Priority で優先順位を与えられるようになりました。 特に、テストで特定のモックやスタブに切り替える必要がある場合に重宝するでしょう。


まとめ

Jakarta CDI 4.0 の変更点について紹介しました。

利用者視点ではインパクトの大きな変更はありませんが、クラウドネイティブ向けの「ビルドタイム指向」な CDI Lite 仕様ができたことは、Eclipse Microprofile 仕様を含めて、転換点にとなる重要なリリースだと思います。 既に Quarkus の ArC で実証が先行していますしね。

その他、こちらのチケットにあるように deprecated だったものの削除も行われています。