Programmatic CDI

このエントリはJavaEE Advent Calendar 2012 第16日目のエントリです。
昨日は @kokuzawa さんの「Point-to-Point on JMS」でした。
明日は @yumix_h さんの「JAX-RSでファイルアップロード!」です。

今日のテーマはCDI 1.0 (JSR 299: Contexts and Dependency Injection for the Java EE platform)です。特にAPIからCDIを使う方法について書き留めたいと思います。なお、動作検証はMac OS X 10.8.2 + JDK 1.7.0_05 + GlassFish 3.1.2 で行っています。

CDIの基本

まずはCDIの基本的な使い方について、簡単にまとめておきましょう。キーになるのはスコープとインジェクションポイントです。

スコープ

スコープとは、インスタンスの生存期間のことです。それぞれのアノテーションでクラス、あるいはプロデューサフィールド/メソッドをマークすることによって表します。

// Userクラスをセッションスコープとしてマーク
@SessionScoped
public class User implements Serializable {

}

予め用意されているスコープは

  • @ApplicationScoped
  • @SessionScoped
  • @ConversationScoped (with JSF)
  • @RequestScoped

の4種類です。意味は名前からだいたい想像がつくと思いますが、ConversationScopeは少し特殊かもしれませんね。これはJSFと共に使用するもので、ブラウザタブ毎に複数リクエストに渡って維持することのできるスコープです。

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

インジェクションポイントとは、依存性を解決されたインスタンスが注入される場所のことで、@Injectを使って表します。フィールド、コンストラクタ、あるいはメソッドをマークします。

@RequestScoped
public class Ephemera {
  // Userインスタンスをインジェクション
  @Inject
  private User user;
}

beans.xml

CDIを有効化するためには、beans.xmlファイルをWEB-INF/(Webモジュールの場合)、あるいはMETA-INF/(EJBモジュールの場合)に配置します。また、依存jarファイルにインジェクション対象となるクラスを含む場合、そのjarファイル内のMETA-INF/にもbeans.xmlファイルを配置する必要があります。いずれのbeans.xmlも空のファイルで構いません。

APIを利用する

ほかにもQualifierとかいろいろ面白いのですが、その辺りは今日の本題ではないので別の機会に譲ります。ということで、このようにアノテーションによってコンテキストと依存性を解決することができるのがCDIです。ただしアノテーションはあくまで静的な記述ですから、

  • インジェクション対象となるクラスは、依存性を一意に解決できる
  • 1つのインジェクションポイントには、常に1つのクラスのインスタンスがインジェクションされる

といった前提条件があります。しかし場合によってはもう少し柔軟な処理が必要になることもあるでしょう。例えばフレームワークやライブラリを書いているときなどは、あるインターフェースを実装するクラスが複数存在したり、1つのインジェクションポイントに条件次第で異なる実装クラスをインジェクションしたり・・・などということもよくよくあるものです。

CDIにはアノテーション以外のAPIも定義されていますので、それを使ってみましょう。

Instance<T>

javax.enterprise.inject.Instance<T>はT型のCDI管理インスタンスを選択・取得するためのクラスです。T型の実装が存在しない、あるいは複数存在する可能性がある場合に使用できます。

例えばこのようなクラス構成を考えます。(それぞれのクラスについているアノテーションはQualifierとします)

Programmatic CDI classes

そして次のサーブレットを実行してみましょう。

public class MyServlet extends HttpServlet {
     private static final long serialVersionUID = 1L;

     // インターフェースPersonの実装をインジェクション
     @Inject @Any Instance<Person> instance;

     protected void doGet(HttpServletRequest request,
               HttpServletResponse response) throws ServletException, IOException {
          PrintWriter out = response.getWriter();

          if (instance.isUnsatisfied()) {
               out.println("Person is not implemented");
          } else if (instance.isAmbiguous()) {
               out.printf("Person is implemented:%n");
               for (Person person : instance) {
                    out.printf(" - %s%n", person);
               }
          } else {
               out.printf("Person is implemented: %s%n", instance.get());
          }
     }
}
$ curl "http://localhost:8080/cdi/MyServlet"
Person is implemented:
 - Admin@6e028a6a
 - User@7a0c4fd9
 - SpecialUser@3e4e6e28

なお@Injectと共に付いている@AnyはQualifierを無視するためのアノテーションです。@Anyを付けない場合、QualifierのないUserクラスのインスタンスのみが選択されてインジェクションされます。

さてこれで複数の実装クラスを手にすることができました。次はいずれかの実装を選択しましょう。そのためにはInstance#select(Annotation…)メソッドにQualifierを渡します。

・・・ちょっと待ってください、引数としてアノテーションのインスタンスを渡す必要があるのですね。アノテーションのインスタンスってどうやって作るのでしょう? この話題はJava Advent Calendarの方で書こうと思っているのですが、CDIにはちゃんとインスタンス化の方法が用意されています。(でもやっぱりちょっと面倒です)

具体的にはjavax.enterprise.util.AnnotationLiteral<T>を拡張して以下のようなクラスを作成します。

@SuppressWarnings("all")
public class SupervisorQualifier
     extends AnnotationLiteral<Supervisor> implements Supervisor {
     private static final long serialVersionUID = 1L;
}

こうして作ったクラスを使って、Instance#select(Annotation…)メソッドをコールします。ついでに言うとこのメソッドの戻り値もInstance<T>なので、複数のQualifierを順に絞っていくような選択の仕方も可能です。

out.printf("@Supervisor %s%n",
          instance.select(new SupervisorQualifier()).get());
out.printf("@Default %s%n",
          instance.select(new DefaultQualifier()).get());
out.printf("@Customized %s%n",
          instance.select(new CustomizedQualifier()).get());
$ curl "http://localhost:8080/cdi/MyServlet"
...
@Supervisor Admin@77f606ea
@Default User@81bbd46
@Customized SpecialUser@49414ab

これでそれぞれの実装を選択することができました。

BeanManager, Bean<T>

前項のInstance<T>は@Injectと共に使用するクラスでした。つまりInstance<T>が使用できるのは、@Injectが有効な場所に限られます。

それに対してjavax.enterprise.inject.spi.BeanManagerはJNDI名を持っているため、ほぼどこでも使用することができます。(もちろんBeanManagerそのものを@Injectでインジェクションすることも可能です)

CDIと無関係なクラス、MySeviceからBeanManagerを使ってCDIを呼び出してみましょう。

public class MyService {
     public void exec(PrintWriter out){
          try {
               BeanManager bm  = InitialContext.doLookup("java:comp/BeanManager");

               Set<Bean<?>> beans = bm.getBeans(Person.class);
               Bean<?> bean = bm.resolve(beans);   // 一意に解決できる場合
               CreationalContext<?> cc = bm.createCreationalContext(bean);
               Person person = (Person) bm.getReference(bean, Person.class, cc);
               out.printf("Person: %s%n", person);

          } catch (NamingException e) {
               e.printStackTrace();
          }
     }
}
$ curl "http://localhost:8080/cdi/MyServlet2"
Person: User@cbbe607

ちなみにMyServlet2は、普通にnew MyService()してからexec(PrintWriter)を呼び出すだけのサーブレットです。

CDI管理下のクラスそれぞれに対応する、javax.enterprise.inject.spi.Bean<T>クラスのインスタンスをBeanManagerから取得することができます。スコープやインジェクションポイント、QualifierなどCDIとしての情報はこのBean<T>クラスが保持しています。

これでどこでもCDIが使えるようになりました。ただこのBeanManagerからPersonを取得するまでのコードは少し冗長ですよね。もともとPortable Extensionで使用するためにあるクラスとは言え、簡易メソッドとしてBeanManager#getReference(Type beanType, Annotation… qualifiers) ぐらいはあってもいい気がするんですが・・・。

CDI 1.1

ちょっと追記です。現在Public ReviewになっているCDIの次期バージョン、CDI 1.1 (JSR 346: Contexts and Dependency Injection for Java EE 1.1) を見てみたところ、新しくjavax.enterprise.inject.spi.CDI<T>というクラスが定義される予定になっているようです。このクラスはInstance<T>を実装していて、かつCDI<Object>を返すstaticメソッドcurrent()を持っています。つまり、

Person person = CDI.current()
     .select(Person.class, new DefaultQualifier()).get();

などとすれば、Personインターフェースの実装インスタンスを取得することができるようになるということですね。

コメントを残す