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がクラスなどの型情報でなくて、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モジュールでは
の条件を満たす任意のクラスは自動的に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を使うことで
といったメリットが得られます。
CDIの更なるメリットと注意点
それに加えて、CDIではJava EE6標準の範囲でフルスタックのWebアプリケーションが簡単に作成できることを確かめてみました。 - 達人プログラマーを目指してでも説明したように、従来のJSFでは利用できなかった、会話スコープを利用することが可能になっています。つまり、@ConversationScopedを利用することで、Beanを丸ごとセッションスコープに格納する代わりに特定の画面遷移ごとに会話スコープを定義することができます。会話スコープは複数同時進行させることもできるため、タブブラウザやマルチウィンドウのアプリケーションでは特に有用です。
しかし、CDIをJSFと組み合わせる場合には以下のような点に注意する必要があります。
利用可能なスコープの違い
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"; } } }