Java EE6環境でJSF2を使う場合はCDIのBeanを管理Beanとして使う方がよい

先週の勉強会で紹介させていただいたjsf-scrumtoys-refactoredでは、JSFの管理Beanを使用する代わりにCDIのBeanを利用しています。この点説明が不十分だったので、ここで簡単に補足させていただきます。

JSFと管理Bean

勉強会の中で、JSFコンポーネントベースのWebアプリケーションフレームワークであると説明させていただきました。(この点については以下のエントリーもご参照ください。Struts1に代わるWebフレームワークの選択 - 達人プログラマーを目指して

コンポーネントベースのフレームワークの場合、VBやSwingといった伝統的な(Webでない)GUIアプリケーションのように、フォームや入力フィールド、ボタンといった画面コンポーネントのツリーが構築されます。そして、画面部品の入力やクリックなどのイベントに従って、管理Beanと呼ばれるPOJOに対してデータのやり取りやメソッドの呼び出しが実行されます。

JSF1.2までのxmlを使った管理Beanの定義

従来のJSF1.2までは、この管理Beanをfaces-config.xmlと呼ばれるxmlファイルにて登録する必要がありました。*1たとえば、今回のSkinActionに相当する管理Beanを登録するためには、以下のような設定が必要でした。

    <managed-bean>
        <managed-bean-class>jsf2.demo.scrum.web.controller.SkinAction</managed-bean-class>
        <managed-bean-name>skinAction</managed-bean-name>
        <managed-bean-scope>session</managed-bean-scope>
        <managed-property>#{skinValuesAction}</managed-property>
    </managed-bean>

つまり、JSFの世界だけで簡単なDIコンテナのようになっておりsetterメソッドを使ってインジェクションが可能でした。

JSF2のアノテーションによる管理Beanの定義

JSF2.0では従来のJSF1.2のxmlを使った定義をそのまま拡張する形で様々なアノテーションが使えるようになっています。上記のような管理Beanはアノテーションを使うと以下のように書けます。

package jsf2.demo.scrum.web.controller;

import java.io.Serializable;
import javax.annotation.PostConstruct;
import javax.faces.bean.ManagedBean;
import javax.faces.bean.SessionScoped;
import javax.faces.bean.ManagedProperty;

//@ManagedBean(name = "skinAction")と同様
@ManagedBean
@SessionScoped
public class SkinAction extends AbstractAction implements Serializable {

    private String selectedSkin;
    @ManagedProperty("#{skinValuesAction}")
    private SkinValuesAction skinValuesAction;
...
}

これで、xmlの登録は不要になったのですがこの場合、以下のイマイチな点があります。

  • DIは型でなく文字列の管理Bean名で行われる
  • @ManagedPropertyによるDIはあくまでもJSFの管理Beanに閉じた世界でのみ可能。(EJBのインジェクションなどは別の方法で行う)

まず、前者の問題はDIがクラスなどの型情報でなくて、EL式で記述した文字列の情報に依存しているため、リファクタリングなどの変更に弱く、また、(IDEによるサポートが不可能ではないとはいえ)一般に間違いの検出が容易ではありません。さらに、xmlで明示的に管理Beanを宣言していないので、インジェクションする相手のソースコードを読んでBean名を調査しないといけないといったこともあります。
後者の問題は、もともとJSF1.2の頃からあった問題ですが、DIできる対象がJSFの管理Bean同士に限られるということですね。通常は管理BeanからEJBなどサービス層のBeanを呼び出すことが多いのですが、その場合にプログラミングのやり方が統一されていませんでした。さらに、EJBそのものをJSFの管理Beanとして直接利用するということもできません。どんなに単純なケースであっても、必ず管理Beanを作成し、そこからEJBを呼び出すといったことが必要になります。

CDIのBeanをJSFの管理Beanとして使うメリット

リファクタリング版のScrumToysではJSFの管理Beanの使用をやめ、代わりにCDIを導入しています。CDIの仕様に従うとwarモジュールのWEB-INFフォルダ中にbaens.xmlが格納されていれば、そのwar全体でCDIが有効になりますが、CDIが有効になっているwarモジュールでは

  • 引数なしコンストラクタを持つ
  • 引数なしコンストラクタがない場合、コンストラクタに@Injectがついているものが定義されている(コンストラクタインジェクション)

の条件を満たす任意のクラスは自動的にBeanとしてコンテナに登録されることになっています。*2また、EJBは一般のクラスとは違い特別な扱いを受けますが、やはり、CDIのBeanとして登録されます。
CDIのプログラミングモデルの基本は非常に簡単で、このように自動的に登録されたBean同士は@InjectによってDIすることが可能というものです。この時、基本的には型情報をもとにDI対象が決定されます。*3この場合、CDIのBeanの定義は以下のようになります。

package jsf2.demo.scrum.web.controller.skin;

import jsf2.demo.scrum.infra.web.controller.AbstractAction;
import java.io.Serializable;
import javax.annotation.PostConstruct;
import javax.enterprise.context.SessionScoped;
import javax.enterprise.inject.Model;
import javax.inject.Inject;

@SessionScoped @Model
// @SessionScoped @Namedと同義、また@SessionScoped @Named("skinAction")と同義
public class SkinAction extends AbstractAction implements Serializable {    
    private String selectedSkin;

    @Inject
    SkinValuesAction skinValuesAction;

ここで、@SessionScopedのアノテーションのパッケージがJSF2の管理Beanのパッケージとはimport元が違っている点に注意してください。また、DIは文字列ではなく型に基づいて行われていることに注意してください。なお、@Namedで文字列の名前をBeanに与えているのはxhtml画面定義中のEL式から名前で参照するためで、インジェクションのために定義しているのではありません。
このようにCDIのBeanを使うことで

  • インジェクションが型安全になる
  • インジェクション対象がJSFの管理Beanだけでなく、EJBを含めたほぼすべてのBeanになる
  • EJB自体に@Namedを付与することで画面から直接参照できる

といったメリットが得られます。

CDIの更なるメリットと注意点

それに加えて、CDIではJava EE6標準の範囲でフルスタックのWebアプリケーションが簡単に作成できることを確かめてみました。 - 達人プログラマーを目指してでも説明したように、従来のJSFでは利用できなかった、会話スコープを利用することが可能になっています。つまり、@ConversationScopedを利用することで、Beanを丸ごとセッションスコープに格納する代わりに特定の画面遷移ごとに会話スコープを定義することができます。会話スコープは複数同時進行させることもできるため、タブブラウザやマルチウィンドウのアプリケーションでは特に有用です。
しかし、CDIJSFと組み合わせる場合には以下のような点に注意する必要があります。

利用可能なスコープの違い

JSF2で定義されているスコープは

  • @javax.faces.bean.ApplicationScoped
  • @javax.faces.bean.SessionScoped
  • @javax.faces.bean.RequestScoped
  • @javax.faces.bean.ViewScoped
  • @javax.faces.bean.NoneScoped
  • @javax.faces.bean.CustomScoped

があります。一方、CDIで利用可能なスコープは

  • @javax.enterprise.context.ApplicationScoped
  • @javax.enterprise.context.SessionScoped
  • @javax.enterprise.context.RequestScoped
  • @javax.enterprise.context.ConversationScoped
  • @javax.inject.Singleton疑似スコープ
  • @javax.enterprise.context.Depenent疑似スコープ

となっていて、ほとんど共通しているのですが完全には対応していません。(大混乱に陥っているJavaEE 6のアノテーションに関する使い分けについて - 達人プログラマーを目指して)特に、JSFで有用なViewScopedは直接は対応するスコープがCDIでは利用できません。このようなViewスコープをCDIでも利用する方法については、Java EE6標準の範囲でフルスタックのWebアプリケーションが簡単に作成できることを確かめてみました。 - 達人プログラマーを目指してで書いた通りです。

JSFコンバーターやバリデーターがCDIのBeanとして利用できない

管理Bean以外にも、コンバーターやバリデーターといったJSF固有のBeanが存在します。これらも従来JSF1.2まではxmlで宣言していましたが、JSF2からはアノテーションで登録できるようになりました。ただし、この場合これらのBeanはJSFの側で生成、管理されてしまい、CDIのコンテナと統合されていないといった制約が現状はあるようです。
したがって、これらのBeanに対して@Injectを使ってEJBや他のBeanを普通にインジェクションすることができません。さらに、コンバーターなどは管理Beanでもないため、@EJBによるインジェクションもできないようです。よって、以下のように面倒でもJNDIルックアップが必要になってしまいます。

@FacesConverter("projectConverter")
public class ProjectConverter implements Converter {

    // @Injectも@EJBも動作しないため、JNDIルックアップが必要
    private ProjectRepository getProjectRepository() {
        Context ctx;
        try {
            ctx = new InitialContext();
            return (ProjectRepository) ctx.lookup("java:module/ProjectRepository");
        } catch (NamingException ex) {
            Logger.getLogger(ProjectConverter.class.getName()).log(Level.SEVERE, null, ex);
            
            throw new RuntimeException(ex);
        }
    }
    
    @Override
    public Object getAsObject(FacesContext context, UIComponent component, String value) {
        if (value == null || value.equals("0")) {
            return null;
        }
        try {
            return getProjectRepository().findById(Long.parseLong(value));
        } catch (NumberFormatException e) {
            throw new ConverterException("Invalid value: " + value, e);
        }
    }

    @Override
    public String getAsString(FacesContext context, UIComponent component, Object object) {
        if (object == null) return "";
        
        Project project = (Project) object;
        Long id = project.getId();
        if (id != null) {
            return String.valueOf(id.longValue());
        } else {
            return "0";
        }
    }
}

まとめ

JSF2の仕様はCDIの仕様よりも一足先に決まったという歴史的背景もあり、CDIの仕様を取り込んでうまく融合するという形になっていないため、現状は多少不便なところもあります。しかしながら、JSF2とCDIを組み合わせることでプログラミングモデルを簡易化できるというメリットは無視できないものがあると思います。
GlassfishのようにJava EE6に準拠した環境であれば、あらかじめCDIがサポートされているのですから、そのような環境においてはJSFの管理Beanの使用は避け、CDIのBeanを利用するようにするというのがよいのではないでしょうか。

*1:ただし、ファイルは任意に分割することも可能。

*2:staticな入れ子のクラスすら以上の条件を満たせば管理Beanとして登録されます。

*3:同一型の実装が複数存在する場合のように、型のみでインジェクション対象を判定できなかった場合は限定子と呼ばれるアノテーションを定義して、Bean定義とインジェクションポイントに@Injectとともに限定子アノテーションを指定します。