[原文ならびに画像]

JSF(JavaServer Faces)再訪 --by デイヴィッド・ギアリー

【概要】
  Webアプリケーションのためのフレームワークの真打ちになることを狙って登場したJavaServer Faces(JSF)は、2002年の9月に最初の初期リリース(early access 1, EA1)が出て以来、多くの変更が加えられてきた。本稿では、JavaServer Facesエキスパート・グループのメンバーであるデイヴィッド・ギアリーが、2003年6月に発表されたEA4をベースに、JSFの規格の成熟について説明する(2003年11月14日)

  JavaServer Faces(JSF)の最初の初期リリース(EA1)が出た直後に、私はJSFに関する詳しい記事を二つ、JavaWorldのサイトに掲出した。その後JSFは、4つのEAが発表され、その間に大きな成熟を遂げた。近くやっと、ベータリリースが出る予定だが、今やJSFは、JavaによるWebアプリケーションのためのフレームワークの最も高度な規格であるだけでなく、Webアプリケーションの開発するためのフレームワークのスタンダードを目指している。とくにSunは、MicrosoftのVisual Studioと対抗するRaveプロジェクトにJSFのサポートツールを揃えることによって、Javaサーバテクノロジ全体の中でのJSFの位置づけの本格的なメジャー化を目指している。

  JSFの基本部分は、私の前回の二つの原稿に書いたものと変わらない。しかしその後加わった数々の新しい機能によって、JSFの使い方は多様化している。JSFをまだよく知らない人は、私の前回の原稿※をぜひお読みいただきたい。本稿では、JSFに関する初歩的な解説は省略させていただく。

  EA1以降、JSFに加わった新しい機能は、主に以下のものである:

・アプリケーションコンフィギュレーションリソース
・ナビゲーション処理 ..... (a1)
・アクション
・JavaBeansのイベントモデル
・マネージドビーン(後述)の作成と管理機能 ..... (a2)
・ポートレットとの互換性
・フレームワークの拡張

※私の前回の原稿:
●第一部:JSFを使ってWebのユーザインタフェイスを作る
●第二部:JavaServer Facesの構造を学ぼう

注記:本稿で説明するコードは、JavaServer Faces(JSF)のEA4に基づいている。JSFのEA4は、Java Web Services Developer Pack(JWSDP)をダウンロードすれば含まれている。ダウンロードページはここ


アプリケーションコンフィギュレーションリソース

  アプリケーションコンフィギュレーションリソースは、JavaではなくXMLで書くコンフィギュレーションファイルで指定する、リソースだ(通常は/WEB-INF/faces-config.xml)。たとえばこのfaces-config.xmlの中で、ナビゲーションやマネージドビーン(JSFの中で作られ管理されるビーン、という意味)を指定できる。
  このように、リソースをドキュメントで指定する機能は、宣言的プログラミング(declarative programming)と呼ばれ、J2EEのデベロッパたちに重宝がられている。Javaのプログラムを書くよりもXMLのドキュメントを書くほうが容易だし、しかもJavaプログラミングの経験のないページ作者にも理解できる。さらにまた、リソースを変えても再コンパイルの必要がない。これからは、JSFのリソースの多くを、コンフィギュレーションファイルで指定できるのだ。主なリソースは、次のようなものだ:

・ナビゲーションルール
・マネージドビーン
・カスタムコンポーネント
・コンバータ
・ヴァリデータ
・レンダーキット

  アプリケーションコンフィギュレーションリソースは、最初に挙げたJSFの新機能のうちの二つとも密接に関連している。それら二つは、ナビゲーション処理(a1)とマネージドビーン作成機能(a2)だ。これら二つの機能のためのリソースも、コンフィギュレーションファイルで指定できるのだ。詳しくは、すこし後で述べる。

注記:JSFのコンフィギュレーションファイルはデフォルトでは/WEB-INF/faces-config.xmlだが、別のファイルや一連の複数のファイルをサーバに対して指定することもできる。


アクションとナビゲーション処理

  JSFのEA1の時点では、アプリケーションのアクションを処理するハンドラをすべてデベロッパが書かなければならなかった。それは、数多くのアクションをswitch文で処理するコードになるので、大きなアプリケーションを書くのには向いていない。そこで最新のEA4では、Strutsと同じように、デベロッパがCommandパターンを使って、アクションをオブジェクトとして表現できるようにした。すなわち、switch文の中の多数のcase、という書き方からデベロッパは卒業できる。(Commandパターンについては、この記事で勉強していただきたい。)

  JSFのアクションにはprocessAction()というメソッドがあって、このメソッドがアクションの結果を文字列で返す。JSFは、その結果に基づいて、リクエストをどこかに転送(forward)する。このような、アクションの結果に基づくナビゲーションは、どこで指定するか?。そう、あなたが今想像したとおり、アプリケーションコンフィギュレーションリソースで指定するのだ。

  アクションオブジェクトと、コンフィギュレーションファイルで宣言的に指定するナビゲーション、この二つの組み合わせによって、以下の二つのことが実現する:
  ひとつは、デベロッパがswitch文から解放され、Commandパターンでアクションを記述できること。
  もうひとつは、、アクションの結果やJSPページの内容などに基づいて“ナビゲーションルール”をコンフィギュレーションファイルの中で指定できることだ。
  この二つのことによって、あなたのWebアプリケーションのメンテナンス性と拡張性が大幅に向上する。


JavaBeansのイベントモデル

  Webアプリケーションのフレームワークは、GUIのボタンがクリックされたとか、FORMの値が変更されたなどのイベントに反応しなければならない。だから、イベントモデルというものが、きわめて重要だ。
  JSFのEA4は、二種類のイベントと、それらに対応する二種類のリスナを提供している。ひとつは、ボタンやリンクがクリックされたときに発火するアクションイベントだ。そしてもうひとつのイベントは、ユーザ入力の値が変わったときに発火するヴァリューチェンジドイベント(value-changed event)だ。JSFとJavaServer Pages(JSP)を併用するときには、各GUI/FORM成分のためのイベントハンドラを次のようにして登録する:

<h:input_text valueRef='someBean.someProperty'>
   <f:valuechanged_listener type='SomeListenerClass'/>
</h:input_text>
...
<h:command_button key='submit.button.prompt' bundle='messages'>
   <f:action_listener type='AnotherListenerClass'/>
</h:command_button>

  もっと本格的なイベント処理については、本稿の最後のほうで説明する。


マネージドビーン作成機能

  JSFでは、Webアプリケーションの各部位を簡単にビーンのプロパティにできる。たとえば:

<h:input_secret valueRef='loginForm.password'/>

  上のタグは、Webページ上で入力される値をloginFormというビーンのpasswordプロパティに結び付けている。以前のJSFでは、入力部位そのものをビーンのプロパティに結び付けた(属性名はvalueRefではなくmodelReferenceだった)が、デベロッパは<jsp:useBean>を使ってビーンを作成/指定しなければならなかった。
  EA4では、ビーンの作成についてはJSFが面倒見るから、デベロッパはビーンをコンフィギュレーションファイルの中で指定するだけでよい。コンフィギュレーションファイルの中では、ビーンのプロパティや、複数のビーンのランタイム時の関係も指定できる。実に便利なコンフィギュレーションファイルなのだ!。


ポートレットとの互換性

  JSFのEA4では、ポートレットの規格を重視し、そのために、サーブレットのAPIを直接呼び出さないようになった。たとえばEA1では、FacesContext.getServletContext()という呼び出しでサーブレットコンテキストの参照(リファレンス)を入手していたが、ポートレットではポートレットコンテキストの参照が必要だ。
  そこでEA4にはもはや、FacesContext.getServletContext()という呼び出しはない。その代わりに、FacesContext.getExternalContext()を呼び出すのだ。ExternalContextクラスは、いろんなコンテキスト(通常はサーブレットコンテキストまたはポートレットコンテキスト)に対するラッパー(wrapper,包み役)だ。
  ExternalContextを無指定で使うと、一般的にコンテナ(Webサーバアプリケーションのエンジンコンテナ)のコンテキストにアクセスできる。たとえばExternalContext.getRequest()という呼び出しは、ServletRequestのインスタンスまたはPortletRequestのインスタンス、どちらかを指すObjectの参照を返す。このObjectを、適切なものにキャストすればよい。
  コンテキストを、ServletContextかPortletContextのどちらかに特定したいときには、ExternalContext.getContext()を呼び出し、その返し値を正しくキャストする。

フレームワークの拡張

  さらにJSFのEA4では、デベロッパがオブジェクトをプラグインすることによって、JSFフレームワークそのものを拡張したり部分変更できるようになった。それらのオブジェクトとは、以下のものだ:

・ナビゲーションハンドラ
・デフォルトのアクションリスナ
・プロパティリゾルバ
・変数リゾルバ
・ビューハンドラ

  これらのオブジェクトを、デフォルトのものから変えたくなる理由が、いろいろある。たとえば、ナビゲーションハンドラが、アクションの結果に基づいてリクエストを別のリソースに転送(forward)するとき、必ずセキュリティのチェックをしたい…そんなときには、あなたの特製のハンドラが必要だ。また、値を参照し値を決定するためのオブジェクトを特製して、それをJSFに値参照のために使わせたい、という場合がある…そんなときには、変数リゾルバを自作するとよい。これらの自作クラス(仮にその名前がAcmeNavigationHandlerとAcmeVariableResolverだとしよう)はもちろん、コンフィギュレーションファイルの中で登録する。


旧と新

  以上で、JSFの新機能の理論的な説明はひとまず終わりだ。では次に、私が以前EA1のために書いたアプリケーションを、EA4向けに書き換えるとどうなるか、という例を見ていこう。以前の最初の記事では、図1のような簡単なログインアプリケーションについて説明した。

図1:JSFで作成した簡単なログインページ
(画像は原文上に)

  図1のアプリケーションは、あなたの予想どおりの動作をする:まず入力を検査し、正しければウェルカムページへリクエストを転送する。正しくなければ、エラーメッセージと共にログインページを再表示する。このアプリケーションはJSFの標準の部位とヴァリデータを使っている。では、このアプリケーションの最初の実装と、最新の実装を、比較してみよう。

  JSFのEA1で、デベロッパを最も苦しめたのが、アプリケーションハンドラだ。どのJSFアプリケーションでも、アプリケーションハンドラという形で、イベント処理とナビゲーションを記述しなければならなかった。私の最初の記事では、次のようなアプリケーションハンドラを実装した:

コード1:JSF EA1のころのアプリケーションハンドラ
package com.sabreware.appHandlers;

import javax.faces.FactoryFinder;
import javax.faces.context.FacesContext;
import javax.faces.event.FacesEvent;
import javax.faces.lifecycle.ApplicationHandler;
import javax.faces.tree.TreeFactory;

public class SimpleApplicationHandler implements ApplicationHandler {
   public boolean processEvent(FacesContext context, FacesEvent facesEvent) {
      TreeFactory treeFactory = (TreeFactory)FactoryFinder.
                                              getFactory(FactoryFinder.TREE_FACTORY);
      context.setResponseTree(
                  treeFactory.getTree(context.getServletContext(), "/welcome.jsp"));
      return true;
   }
}


  コード1のアプリケーションハンドラは、すべてのリクエストを/welcome.jspへ転送する。だから、図1のLog Inボタンをクリックすると、ヴァリデーションエラーがなければ必ず/welcome.jspへ行く。このアプリケーションハンドラのほかに、EA1では、デベロッパがサーブレットコンテキストリスナを実装して、アプリケーションハンドラをインストールしなければならなかった。下に示すのが、そのサーブレットコンテキストリスナのコードだ:

コード2:JSF EA1で必要だったサーブレットコンテキストリスナ
package com.sabreware.listeners;

import javax.servlet.*;
import javax.faces.*;
import javax.faces.lifecycle.*;
import com.sabreware.appHandlers.SimpleApplicationHandler;

public class SimpleServletContextListener implements ServletContextListener {
   public void contextInitialized(ServletContextEvent e) {
      LifecycleFactory factory = (LifecycleFactory)
      FactoryFinder.getFactory(FactoryFinder.LIFECYCLE_FACTORY);

      Lifecycle lifecycle = factory.getLifecycle(
                                       LifecycleFactory.DEFAULT_LIFECYCLE);

      lifecycle.setApplicationHandler(new SimpleApplicationHandler());
   }
   public void contextDestroyed(ServletContextEvent e) {
      // Nothing to do here
   }
}


  そして最後に、コード2のコンテキストリスナを、次のように、/WEB-INF/web.xmlに登録しなければならなかった:

コード3:サーブレットコンテキストリスナを/WEB-INF/web.xmlに登録する
<?xml version="1.0"?>
<!DOCTYPE web-app PUBLIC
  "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
  "http://java.sun.com/dtd/web-app_2_3.dtd">

<web-app>
   <!-- Context Listener creates and sets the application handler -->
   <listener>
      <listener-class>
         com.sabreware.listeners.SimpleServletContextListener
      </listener-class>
   </listener>
   ...
</web-app>


  たかだか、/welcom.jspへ行くために、これだけものすごい遠回りをしなければならなかった!。しかしJSF EA4では、ファクトリーとかなんとかの、面倒くさいオブジェクトをデベロッパは相手にしなくてよい。上の、コード1から3までの処理を、EA4では次のように書く:

コード4:ナビゲーションルールをJavaではなくXMLで書く
<navigation-rule>
   <navigation-case>
      <description>All JSF requests (there's only one request in
                   this application) are directed to /welcome.jsp</description>
      <to-tree-id>/welcome.jsp</to-tree-id>
   </navigation-case>
</navigation-rule>


  上のコード4は、コンフィギュレーションファイルの中に書くXMLだ。たったこれだけで、上のコード1から3までを表現している。つまり、すべてのリクエストが/welcome.jspへ行く、ということを。
  EA1では、Javaのソースファイルが2つ、そしてweb.xmlの中の記述が1つ必要だった。EA4では、たった6行のXMLだけだ。しかも、後で/welcome.jspを/start.jspに変更したくなったとき、Javaのソースファイルを書き換えて再コンパイルする必要もない。

  EA1のアプリケーションハンドラでは、ナビゲーションの処理だけでなく、イベント処理も書く必要があった。図1のログインアプリケーションでは、イベントはたった一つだった(Log Inボタンが押された、というアクションイベント)。しかし現実のアプリケーションでは、デベロッパの頭が混乱するような長いswitch文やif/else文を使って、多数のイベントを正しくさばかなければならない。たとえば、こんなふうに:

[醜悪なアプリケーションハンドラ]
public class UglyApplicationHandler implements ApplicationHandler {
   public boolean processEvent(FacesContext context, FacesEvent facesEvent) {
      if(facesEvent instanceof CommandEvent) {
         if("loginForm.login".equals(facesEvent.())) {
            // Code for logging in
         }
         else if("loginForm.cancel".equals(facesEvent.())) {
            // Code for canceling the login form
         }
         else if("loginForm.reset".equals(facesEvent.())) {
            // Code for resetting the login form
         }
         if("registerForm.register".equals(facesEvent.())) {
            // Code for registering
         }
         else if("registerForm.cancel".equals(facesEvent.())) {
            // Code for canceling the registration form
         }
         ...
      }
      TreeFactory treeFactory = (TreeFactory)FactoryFinder.
                                              getFactory(FactoryFinder.TREE_FACTORY);
      if(someConditionSetInCodeAbove) {
         context.setResponseTree(
                  treeFactory.getTree(context.getServletContext(), "/somewhere.jsp"));
      }
      else if(someOtherConditionSetInCodeAbove) {
         context.setResponseTree(
                  treeFactory.getTree(context.getServletContext(), "/somewhere-else.jsp"));
      }
      ...
      return true;
   }
}


  長い長いswitch文をメンテナンスすることは、誰もがやりたがらない。そこで、助っ人としてCommandパターンが登場した。このパターンは、switch文の各caseをオブジェクトにして扱う。SwingやStrutsなどのユーザインタフェイスフレームワークでよく使われていたが、ついにJavaServer Faces(JSF)でも使われるようになった。
  Commandパターンに人気があるのは、メンテナンスが容易、柔軟性がある、そして拡張性に富むからだ。JSFのEA4では、Commandパターンによってアプリケーションハンドラを完全に駆逐した。上の醜悪なコードの中のひとつひとつのif文が、JSFのアクションになるのだ。たとえば、こんなアクションだ。

[JSFのアクションの実装例]
public class LoginAction extends javax.faces.application.Action {
   public String invoke() {
      // Code for logging in
      if(someCondition)
         return "succeeded";
      else
         return "failed";
   }
}


  上のアクションでは、invoke()メソッドが文字列"succeeded"または"failed"を返す。そしてJSFのナビゲーションハンドラがこれらの文字列に基づいて、リクエストの転送先を決める。しかもその指定は、コンフィギュレーションファイルの中に簡単にこのように書く:

[コンフィギュレーションファイルの中に書くナビゲーションルール]
<faces-config>
   <navigation-rule>
      <from-tree-id>/login.jsp</from-tree-id>

      <navigation-case>
         <from-outcome>succeeded</from-outcome>
         <to-tree-id>/welcome.jsp</to-tree-id>
      </navigation-case>

      <navigation-case>
         <from-outcome>failed</from-outcome>
         <to-tree-id>/login-error.jsp</to-tree-id>
      </navigation-case>
   </navigation-rule>
</faces-config>


  上のXMLは、/login.jspから来たリクエストに対する、二つのナビゲーション先を指定している。もしも結果がsucceededならば、ナビゲーションハンドラは/welcome.jspをロードする。結果がfailedならば、ナビゲーションハンドラはlogin-error.jspをロードする。

  以上のようにして、まずアクションを実装し、その結果をナビゲーションのシステムに結び付ける。では、そのアクションを、どうやって使うのか?。たとえばJSPページの中では、actionRefという属性でアクションを参照する:

<%@ taglib uri='http://java.sun.com/jsf/html_basic' prefix='h' %@>
<h:command_button actionRef='login.action'/>


  上のタグは、ボタンをひとつ表現している。ボタンのactionRef属性の値が、login.actionだ。ユーザがそのボタンをクリックすると、loginという名前のビーンのgetAction()メソッドが動きだす。このビーンは、最初に参照されたときにJSFが自動的に作ってくれる。ビーンがコンフィギュレーションファイルに指定されているから、JSFはそのとおりにビーンを作るのだ。

[コンフィギュレーションファイルの中]
<faces-config>
   ...
   <managed-bean>
      <managed-bean-name>login</managed-bean-name>
      <managed-bean-class>actions.Login</managed-bean-class>
   </managed-bean>
   ...
</faces-config>


  上で指定されているLoginクラスは、こんなふうに書かれている:


public class Login {
   public Action getAction() {
      return new LoginAction();
   }
}


  Login.getAction()メソッドは、上でコードを見たLoginActionのインスタンスを指す参照を返す。そこで、actionRef='login.action'というJSPタグの指定は、Login.getAction()の呼び出しへと解決(リゾルブ)され、後者がLoginActionのインスタンスの参照を返す。そうするとJSFはLoginAction.invoke()を呼び出し、次のようにログインの仕事を実行する:

public class LoginAction extends javax.faces.application.Action {
   public void invoke() {
      // ここでログイン処理をするか、または、
      // 純粋なビジネスオブジェクトに処理を委託す
   }
}



ヴァリューチェンジドイベント

  JSFでは、上で述べたように、アクションがビジネスロジックを実装し動かしていく。しかしときには、イベントに対応してユーザインタフェイスのロジックを実装したい場合がある。たとえば図2のアプリケーションを見てみよう。ユーザが自分の国としてアメリカ合衆国を指定すると、州名や都市名を入力するためのFORMが出る。しかしユーザが日本を指定すると、県名や市町村名を入力するためのFORMが必要だ。
  以上のようなことが、“ユーザインタフェイスロジック”の一例だ。
  そしてユーザインタフェイスロジックは、JSFのヴァリューチェンジドイベントを扱うヴァリューチェンジドリスナで実装する。

図2:変様可能なユーザインタフェイス(上は合衆国の場合)
(画像は原文上に)

図3:変様可能なユーザインタフェイス(上はカナダの場合)
(画像は原文上に)

  上のアプリケーションでは、次のような簡単なヴァリューチェンジドリスナを実装すればよい。このコードは、ログインを担当するJSPページの一部だ。

コード5:ヴァリューチェンジドリスナの使い方
<!DOCTYPE HTML PUBLIC '-//W3C//DTD HTML 4.0 Transitional//EN'>
<html>
   <%@ taglib uri='http://java.sun.com/jsf/core' prefix='f' %>

   <f:use_faces>
      <head>
         <%@ taglib uri='http://java.sun.com/jstl/fmt'  prefix='fmt' %>
         <%@ taglib uri='http://java.sun.com/jsf/html'  prefix='h' %>
         <%@ taglib uri='http://java.sun.com/jstl/core' prefix='c' %>

         <fmt:setBundle basename='messages' scope='session' var='messages'/>
  
         <title>
            <h:output_text key='register.window.title' bundle='messages'/>
         </title>
      </head>

      <style type='text/css'>
         @import url(<c:out value='${pageContext.request.contextPath}'/>/site.css);
      </style>

      <body>
         <h:output_text key='register.page.title' bundle='messages'
                outputClass='emphasis'/>
         <p>
         <h:form formName='blank'>
            <h:panel_grid columns='2'>
               <h:output_text key='register.streetAddress.prompt' bundle='messages'/>
               <h:input_text id='streetAddress' valueRef='registerForm.streetAddress'/>

               <h:output_text id='cityPrompt' key='register.city.prompt' bundle='messages'/>
               <h:input_text id='city' valueRef='registerForm.city'/>

               <h:output_text id='statePrompt' key='register.state.prompt' bundle='messages'/>
               <h:input_text id='state' valueRef='registerForm.state'/>

               <h:output_text key='register.country.prompt' bundle='messages'/>
               <h:selectone_menu valueRef="registerForm.country"
                           onchange="submit()">
                  <f:valuechanged_listener type='listeners.CountryChanged'/>
                  <h:selectitems valueRef='registerForm.countryNames'/>
               </h:selectone_menu>
            </h:panel_grid>
            <p>
            <h:command_button actionRef='registerForm.action'
                                     key='register.submit' bundle='messages'
                            commandName='immaterial'/>
         </h:form>
      </body>
   </f:use_faces>
</html>  


  上のコード5では、都市欄や州欄のためにユニークなIDを割り当てている。タグで指定するリスナが、これらのIDを使う。リスナのコードを、次に示そう:

コード6:ヴァリューチェンジドリスナの実装例
package listeners;

import javax.faces.event.ValueChangedEvent;
import javax.faces.event.ValueChangedListener;
import javax.faces.event.PhaseId;

public class CountryChanged implements ValueChangedListener {
   public PhaseId getPhaseId() {
      return PhaseId.ANY_PHASE;
   }
   public void processValueChanged(ValueChangedEvent event) {
      UIComponent c = (UIComponent)event.getSource(),
        cityPrompt  = c.getParent().findComponent("cityPrompt"),
        statePrompt = c.getParent().findComponent("statePrompt");

      if("Canada".equals(event.getNewValue())) {
         cityPrompt.setAttribute ("key", "register.municipality.prompt");
         statePrompt.setAttribute("key", "register.province.prompt");
      }
      else {
         cityPrompt.setAttribute ("key", "register.city.prompt");
         statePrompt.setAttribute("key", "register.state.prompt");
      }
   }
}


  上のリスナは、まず都市欄や州欄の参照を取得し、そして指定された国名に応じてそれらの値を変えている(上のコード例では日本ではなくカナダである)。


JSFの前途は明るい

  JavaServer Faces(JSF)は、最初のEAから大きく成長した。最初のころあった重大な欠陥は、すべて手当てされた。今日では、デベロッパはJSFを使って、堅牢で、メンテナンス性が良く、拡張性に富むWebアプリケーションを書くことができる。もうすぐベータリリースが出る予定だから、データグリッド機能や、使いやすい国際化機能などの実装も期待される。JSFは、Strutsに代わって、JavaによるWebアプリケーションフレームワークのスタンダードになることを狙っている。スタンダードと認められれば、ユーザもIDEベンダもこぞって採用することになるだろう。

--end of text--