SeamFramework.orgCommunity Documentation
本章では、 そろそろ Seam の対話モデルについて詳細に理解していくことにします。
事のはじまりは、 3 つの思いつきが融合した結果、 Seam 「対話」の概念となったことです。
ワークスペース という思いつき、 これは 2002 年にビクトリア州政府 (オーストラリア) のプロジェクトで思いつきました。 このプロジェクトで、私は Struts の上にワークスペース管理を実装せざるを得なくなりました。2 度と繰り返したいとは思わないような経験でした。
楽観的セマンティクスで動作するアプリケーショントランザクションという思いつきに加え、 ステートレスなアーキテクチャをベースとする既存のフレームワークでは拡張された永続コンテキストの効率的な管理は実現できないことを実感した事実でした。 (Hibernate チームは LazyInitializationException
に対する非難は聞き飽きていましたし、 実際にはこれは Hibernate に問題があるのではなく、 むしろ Spring フレームワークや J2EE における従来の stateless session facade (anti) パターンなどステートレスアーキテクチャでサポートされる極端に限定的な永続コンテキストモデルに問題があったのです。)
ワークフロータスクという思いつき
こうした思いつきを統一しフレームワークで強力なサポートを提供することで、 以前よりすっきりしたコードでより豊かで効率的なアプリケーションをビルドできるパワフルな構成概念を得ました。
これまでに見た例は、以下の規則に従う非常に単純な対話モデルを利用します。
JSF 要求ライフサイクルである、 要求値の適用、 プロセスの妥当性検証、 モデル値の更新、 アプリケーションの呼び出し、 応答のレンダリングの各フェーズの期間、 常にアクティブな対話コンテキストがあります。
JSF 要求ライフサイクルであるビューの復元フェーズの終わりで、 Seam はそれまでの長期対話コンテキストの復元を試みます。 存在しなければ、 Seam は新しい一時的な対話コンテキストを生成します。
@Begin
メソッドが出てくると、 一時的な対話コンテキストは長期対話に昇格します。
@End
メソッドが出てくると、 どのような長期対話コンテキストでも一時的な対話に降格されます。
JSF 要求ライフサイクルである応答のレンダリングのフェーズの終わりで、 Seam は長期対話コンテキストの内容を記憶するか、 一時的な対話コンテキストの内容を破棄します。
どのような faces 要求 (JSF ポストバック) でも対話コンテキストを伝播します。 デフォルトでは、 non-faces 要求 (例えば、 GET 要求) は対話コンテキストを伝播しませんが、 これについての詳細は下記を参照してください。
If the JSF request lifecycle is foreshortened by a redirect, Seam transparently stores and restores the current conversation context — unless the conversation was already ended via @End(beforeRedirect=true)
.
Seam は透過的に対話コンテキスト (一時的な対話コンテキストを含む) を JSF ポストパックおよびリダイレクト全体に伝播します。 特に何もしなければ 非 faces 要求 (たとえば GET 要求など) は対話コンテキストを伝播せず新しい一時的な対話内で処理されます。 常にとは限りませんが、 これが通常求められる動作になります。
non-faces 要求全体に Seam 対話を伝播させたい場合、 要求パラメータとして Seam 対話 ID (conversation id) を明示的にコード化する必要があります。
<a href="main.jsf?#{manager.conversationIdParameter}=#{conversation.id}"
>Continue</a
>
JSF のようにする場合には以下のようにします。
<h:outputLink value="main.jsf">
<f:param name="#{manager.conversationIdParameter}" value="#{conversation.id}"/>
<h:outputText value="Continue"/>
</h:outputLink
>
Seam タグライブラリを使用する場合、 以下は等価です。
<h:outputLink value="main.jsf">
<s:conversationId/>
<h:outputText value="Continue"/>
</h:outputLink
>
ポストバック用の対話コンテキストの伝播を無効にしたい場合は、 同様のトリックが使えます。
<h:commandLink action="main" value="Exit">
<f:param name="conversationPropagation" value="none"/>
</h:commandLink
>
Seam タグライブラリを使用する場合、 以下は等価です。
<h:commandLink action="main" value="Exit">
<s:conversationPropagation type="none"/>
</h:commandLink
>
対話コンテキストの伝播を無効にすることと、 対話を終了することとは全く異なることですので注意してください。
conversationPropagation
要求パラメータ または <s:conversationPropagation>
タグは、 対話の開始と終了、あるいはネストされた対話の開始にも使用することができます。
<h:commandLink action="main" value="Exit">
<s:conversationPropagation type="end"/>
</h:commandLink
>
<h:commandLink action="main" value="Select Child">
<s:conversationPropagation type="nested"/>
</h:commandLink
>
<h:commandLink action="main" value="Select Hotel">
<s:conversationPropagation type="begin"/>
</h:commandLink
>
<h:commandLink action="main" value="Select Hotel">
<s:conversationPropagation type="join"/>
</h:commandLink
>
この対話モデルは、 マルチウィンドウ操作に関して正常に動作するアプリケーションの構築を容易してくれます。 多くのアプリケーションにとって必要なものはこれだけです。 複雑なアプリケーションのなかには以下の追加要件の両方あるいはどちらかを必要とするものがあります。
対話には、 連続的に実行したり同時に実行することもある多くの小さな単位のユーザーインタラクションも含まれます。 より小さい ネストされた対話 (nested conversation) には単独の対話状態セットがあり、 また外側の対話状態へのアクセスもあります。
ユーザーは同じブラウザのウィンドウ内でいくつもの対話を切り換えることができます。 この機能がワークスペース管理と呼ばれるものです。
ネストされた対話は既存の対話のスコープ内で @Begin(nested=true)
とマークされたメソッドを呼び出すことによって作成されます。 ネストされた対話はそれ自体の対話コンテキストを持っていますが、 外側の対話のコンテキストから値を読み取ることができます。 オブジェクトは参照により取得されるため外側の対話のコンテキストはネストされた対話内では読み取り専用となりますが、 オブジェクト自体への変更はその外側のコンテキストに反映されます。
対話をネストするとオリジナルの対話または外側の対話のコンテキストに積み重ねられるコンテキストを初期化します。 外側の対話が親とみなされます。
ネストされた対話のコンテキストに直接セットされるまたはアウトジェクトされる値はすべて親となる対話のコンテキスト内にアクセス可能なオブジェクトに影響は与えません。
対話コンテキストからのコンテキスト検索やインジェクションはまず現在の対話コンテキストにある値を検索し、 値がみつかならないと対話がネストされている場合はその対話スタックに進みます。 このあとわかるように、 この動作は上書き可能です。
次に @End
が出てくると、 対話スタックから「ポップ(Pop)」されることで、 その取り出された対話は破棄されます。 対話は任意の深さにネストすることができます。
特定のユーザーアクティビティ (ワークスペース管理や戻るボタン) により、 内側の対話が終了する前に外側の対話が開始させることができます。 この場合、 同じ外側の対話に属する複数の並列ネスト対話を持つことができます。 ネストされた対話が終了する前に外側の対話が終了すると、 Seam はネストされた対話コンテキストを外側のコンテキストと共にすべて破棄します。
対話スタックの最下位にある対話がルートの対話になります。 この対話を破棄すると常に派生した対話はすべて破棄されます。 @End(root=true)
を指定すると宣言的にこれを行うことができます。
対話は継続可能な状態と見なすことができます。 ネストされた対話により、 ユーザーとの交信でのさまざまなポイントにおいてアプリケーションは一貫した継続可能な状態を捕らえることができるようになります。 従って、 戻るボタンを押すことやワークスペース管理に対して正しい動作を保証します。
前述した通り、 現在ネストされている対話の親となる対話にコンポーネントが存在する場合、 このネストされている対話は同じインスタンスを使用します。ときには、 親となる対話内に存在するコンポーネントインスタンスがその子となる対話からは見えなくなるように、 ネストされるそれぞれの対話内に別々のインスタンスを持たせると便利なことがあります。 コンポーネントに @PerNestedConversation
アノテーションを付けるとこれを行うことができます。
ページが non-faces 要求 (例、 HTTP GET 要求) 経由でアクセスされる場合、 JSF は起動されるアクションリスナーを全く定義しません。 ユーザーがそのページをブックマークする、あるいは <h:outputLink>
からそのページに行き着く場合などに発生します。
ページがアクセスされたら直ちに対話を開始したい場合があります。 JSF アクションメソッドがないため、 アクションに @Begin
アノテーションを付けるという普通の方法では問題を解決することができません。
このページが状態をコンテキスト変数にフェッチする必要がある場合、 さらなる問題が発生します。 すでに、2 つの問題解決方法を見てきました。 Seam コンポーネントにその状態が保持される場合、 @Create
メソッドでその状態をフェッチできます。 保持されていなければ、 コンテキスト変数に対して @Factory
メソッドを定義することができます。
これらのオプションがうまくいかない場合、 Seam では pages.xml
ファイルに ページアクション を定義することができます。
<pages>
<page view-id="/messageList.jsp" action="#{messageManager.list}"/>
...
</pages
>
ページがレンダリングされるようとするときは常に、 応答のレンダリングフェーズの冒頭でこのアクションメソッドが呼び出されます。 ページアクションが null 以外の結果を返す場合、 Seam は適切な JSF および Seam ナビゲーションの規則を処理するので、 まったく異なるページがレンダリングさることになるかもしれません。
ページのレンダリング前に行いたいことが対話の開始だけなら、 組み込みアクションメソッドを次のように使用できます。
<pages>
<page view-id="/messageList.jsp" action="#{conversation.begin}"/>
...
</pages
>
また、 この組み込みアクションは JSF コントロールからも呼び出すことができ、 同様に #{conversation.end}
を使って対話を終了することができます。
既存の対話にジョインするあるいはネストされる対話を開始する、 ページフローまたはアトミック対話を開始するためにさらに制御が必要な場合は、 <begin-conversation>
エレメントを使用してください。
<pages>
<page view-id="/messageList.jsp">
<begin-conversation nested="true" pageflow="AddItem"/>
<page>
...
</pages
>
また、 <end-conversation>
エレメントもあります。
<pages>
<page view-id="/home.jsp">
<end-conversation/>
<page>
...
</pages
>
1 番目の問題を解決するには、 現在 5 つのオプションから選択できます。
@Create
メソッドに @Begin
アノテーションを追加する
@Factory
メソッドに @Begin
アノテーションを追加する
Seam ページアクションメソッドに @Begin
アノテーションを追加する
pages.xml
で <begin-conversation>
を使用する
#{conversation.begin}
を Seam ページアクションメソッドとして使用する
JSF コマンドリンクは常に JavaScript でフォームサブミットを行います。 これによりウェブブラウザの「新しいウィンドウで開く」または「新しいタブで開く」機能を動作させなくしてしまいます。 プレーンの JFS でこの機能が必要な場合は、 <h:outputLink>
を使用する必要があります。 ただし、 <h:outputLink>
には重要な制限が 2 つあります。
JSF はアクションリスナーを <h:outputLink>
につなげる方法を提供していません。
実際にフォームサブミットがないため、 JSF は選択された DataModel
の列を伝播しません。
Seam provides the notion of a page action to help solve the first problem, but this does nothing to help us with the second problem. We could work around this by using the RESTful approach of passing a request parameter and requerying for the selected object on the server side. In some cases — such as the Seam blog example application — this is indeed the best approach. The RESTful style supports bookmarking, since it does not require server-side state. In other cases, where we don't care about bookmarks, the use of @DataModel
and @DataModelSelection
is just so convenient and transparent!
この機能を補ってさらに対話伝播の管理をより簡略化するために、 Seam は <s:link>
JSF タグを提供しています。
このリンクは JSF ビュー ID だけを指定することができます。
<s:link view="/login.xhtml" value="Login"/>
あるいは、 アクションメソッドを指定することができます (この場合、 アクションの結果は最終的なページを確定する)。
<s:link action="#{login.logout}" value="Logout"/>
JSF ビュー ID とアクションメソッドの両方を指定すると、 アクションメソッドが null 以外の結果を返さない限り「ビュー」が使用されます。
<s:link view="/loggedOut.xhtml" action="#{login.logout}" value="Logout"/>
リンクは <h:dataTable>
内で使用する DataModel
の選択列を自動的に伝播します。
<s:link view="/hotel.xhtml" action="#{hotelSearch.selectHotel}" value="#{hotel.name}"/>
既存の対話のスコープを残しておくことができます。
<s:link view="/main.xhtml" propagation="none"/>
対話を開始、 終了、 またはネストすることができます。
<s:link action="#{issueEditor.viewComment}" propagation="nest"/>
リンクが対話を開始すると、 使用されるページプローを指定することもできます。
<s:link action="#{documentEditor.getDocument}" propagation="begin"
pageflow="EditDocument"/>
The taskInstance
attribute is for use in jBPM task lists:
<s:link action="#{documentApproval.approveOrReject}" taskInstance="#{task}"/>
(上記の例は DVD ストアデモアプリケーションを参照してください。)
最後に、 ボタンとしてレンダリングされる「リンク」が必要な場合は <s:button>
を使用します。
<s:button action="#{login.logout}" value="Logout"/>
動作に対して成功したか失敗したかをユーザーに示すメッセージを表示するのは非常に一般的です。 これには、 JSF FacesMessage
を使うと便利です。 残念ながら、 成功のアクションはブラウザリダイレクトを要することが多く、 JSF はリダイレクト全体に faces のメッセージは伝播しません。 このためプレーン JSF で成功のメッセージを表示するのはかなり困難になります。
組み込みの対話スコープ Seam コンポーネントである facesMessages
がこの問題を解決してくれます。 (Seam リダイレクトフィルタをインストールしておく必要があります。)
@Name("editDocumentAction")
@Stateless
public class EditDocumentBean implements EditDocument {
@In EntityManager em;
@In Document document;
@In FacesMessages facesMessages;
public String update() {
em.merge(document);
facesMessages.add("Document updated");
}
}
facesMessages
に追加されるメッセージはすべてすぐ次のフェースである応答レンダリングフェーズで現在の対話に対して使用されます。 これは Seam がリダイレクト全体に一時的な対話コンテキストを維持するので長期実行の対話がない場合でも機能します。
JSF EL 式を faces メッセージサマリーに含ませることもできます。
facesMessages.add("Document #{document.title} was updated");
たとえば、 通常の方法でメッセージを表示することができます。
<h:messages globalOnly="true"/>
永続オブジェクトを処理する対話を作業する場合は標準のサロゲート対話 ID ではなくそのオブジェクトのナチュラルキーを使用する方が望ましい場合があります。
既存の対話に容易にリダイレクト
ユーザーが同じ操作を 2 回要求する場合は既存の対話にリダイレクトすると便利な場合があります。 例をあげてみます。 「 「ebay で両親へのクリスマスプレゼントを獲得しそのアイテムの支払いプロセスが途中まで進んでいます。 ここでこのアイテムが直接両親に配達されるようにしようと思います。 支払い詳細は入力しましたが両親の明確な住所を思い出すことができません。 住所を検索しようとして誤って同じブラウザウィンドウを使ってしまいました。 もう一度さっきのアイテムの支払いに戻る必要があります。」 」
With a natural conversation it's really easy to have the user rejoin the existing conversation, and pick up where they left off - just have them to rejoin the payForItem conversation with the itemId as the conversation id.
ユーザーフレンドリーな URL
個人的には操作可能な階層と (URL を編集しながら操作可能) 意味のある URL (この Wiki に見られるようにランダムな ID での識別を行わない) で構成されているように見えますが、 アプリケーションユーザーにとってはフレンドリーな URL はそれほど重要ではない場合もあるでしょう。
With a natural conversation, when you are building your hotel booking system (or, of course, whatever your app is) you can generate a URL like http://seam-hotels/book.seam?hotel=BestWesternAntwerpen
(of course, whatever parameter hotel
maps to on your domain model must be unique) and with URLRewrite easily transform this to http://seam-hotels/book/BestWesternAntwerpen.
かなり改善されたはずです。
ナチュラル対話は pages.xml
で定義されます。
<conversation name="PlaceBid"
parameter-name="auctionId"
parameter-value="#{auction.auctionId}"/>
上記の定義でまず判るのは対話に名前があることです。 この場合 PlaceBid
になります。 名前は特定の指定対話を固有に識別するため page
定義により参加する指定対話の識別に使用されます。
次の属性 parameter-name
はデフォルトの対話 ID パラメータの代わりにナチュラル対話の ID を含む要求パラメータを定義します。 この例では parameter-name
は auctionId
になります。 つまり、 ページの URL 内に出現する cid=123
のような対話パラメータの代わりに auctionId=765432
を含むようになります。
上記の設定の最後の属性 parameter-value
は対話 ID として使用するナチュラルビジネスキーの値の評価に使用される EL 式を定義します。 この例では対話 ID は現在範囲内の auction
のプライマリーキー値になります。
次に指定対話に参加するページを定義します。 page
定義の conversation
属性を指定してこれを行います。
<page view-id="/bid.xhtml" conversation="PlaceBid" login-required="true">
<navigation from-action="#{bidAction.confirmBid}"
>
<rule if-outcome="success">
<redirect view-id="/auction.xhtml">
<param name="id" value="#{bidAction.bid.auction.auctionId}"/>
</redirect>
</rule
>
</navigation>
</page
>
ナチュラル対話を開始またはリダイレクトする場合、 ナチュラル対話名を指定するいくつかのオプションがあります。 次のページ定義をまず見てみることにします。
<page view-id="/auction.xhtml">
<param name="id" value="#{auctionDetail.selectedAuctionId}"/>
<navigation from-action="#{bidAction.placeBid}">
<redirect view-id="/bid.xhtml"/>
</navigation>
</page
>
auction ビューからアクション #{bidAction.placeBid}
を呼び出していること (これらのサンプルはすべて Seam にある seamBay から取得している)、 前述のようにナチュラル対話 PlaceBid
で設定される /bid.xhtml
にリダイレクトしていることがわかります。 アクションメソッドの宣言は以下のようになります。
@Begin(join = true)
public void placeBid()
名前が付けられた対話が <page/>
エレメントに指定される場合、 その名前の対話へのリダイレクトはアクションメソッドが完全に呼び出された後でナビゲーションルールの一部として発生します。 既存の対話にリダイレクトする場合はアクションメソッドが呼び出される前にリダイレクトが発生する必要があるため問題となってきます。 したがってアクションが呼び出される場合は対話名を指定する必要があります。 s:conversationName
タグを使って行うのがその方法のひとつとなります。
<h:commandButton id="placeBidWithAmount" styleClass="placeBid" action="#{bidAction.placeBid}">
<s:conversationName value="PlaceBid"/>
</h:commandButton
>
もうひとつの方法は s:link
または s:button
のどちらかを使用するときに conversationName
を指定する方法です。
<s:link value="Place Bid" action="#{bidAction.placeBid}" conversationName="PlaceBid"/>
ワークスペース管理は、1 つのウィンドウの中で対話を「切り換える」能力です。 Seam はワークスペース管理を Java コードのレベルで完全に透過的にします。 ワークスペース管理を可能にするために、必要なすべては以下の通りです。
それぞれのビュー ID (JSF または Seam ナビゲーションルールを使用する場合) またはページノード (jPDL ページフロー) に詳細のテキストを入力します。 この詳細テキストはワークスペース切り替えによってユーザーに表示されます。
ページの中に 1 つ以上の標準ワークスペース切り替え JSP または facelets の断片を含ませます。 標準の断片はドロップダウンメニュー、 対話のリスト、 ブレッドクラム (breadcrumbs) を通じてワークスペース管理をサポートします。
JSF または Seam ナビゲーションルールを使用する場合、 Seam は対話の現在の view-id
を復元してその対話に切り替えます。 ワークスペースの記述的なテキストは pages.xml
と呼ばれるファイルで定義され、 Seam はこのファイルが WEB-INF
ディレクトリ内の faces-config.xml
のすぐ次に配置されていることを期待します。
<pages>
<page view-id="/main.xhtml">
<description
>Search hotels: #{hotelBooking.searchString}</description>
</page>
<page view-id="/hotel.xhtml">
<description
>View hotel: #{hotel.name}</description>
</page>
<page view-id="/book.xhtml">
<description
>Book hotel: #{hotel.name}</description>
</page>
<page view-id="/confirm.xhtml">
<description
>Confirm: #{booking.description}</description>
</page>
</pages
>
このファイルが期待する場所になくても Seam アプリケーションは正常に動作を続行します。 動作しない機能はワークスペースの切り替え機能のみです。
jPDL ページフロー定義を使う場合、 Seam は現在の jBPM のプロセス状態を復元することによって対話に切り替えます。 同じ view-id
に現在の <page>
に応じて異なる詳細を持たせることができるためこれはより柔軟なモデルになります。 詳細テキストは <page>
ノードで定義されます。
<pageflow-definition name="shopping">
<start-state name="start">
<transition to="browse"/>
</start-state>
<page name="browse" view-id="/browse.xhtml">
<description
>DVD Search: #{search.searchPattern}</description>
<transition to="browse"/>
<transition name="checkout" to="checkout"/>
</page>
<page name="checkout" view-id="/checkout.xhtml">
<description
>Purchase: $#{cart.total}</description>
<transition to="checkout"/>
<transition name="complete" to="complete"/>
</page>
<page name="complete" view-id="/complete.xhtml">
<end-conversation />
</page>
</pageflow-definition
>
次の断片を JSP または facelets のページに含ませて、 現在の対話またはその他いずれのアプリケーションのページにも切り替えられるドロップダウンメニューを取得します。
<h:selectOneMenu value="#{switcher.conversationIdOrOutcome}">
<f:selectItem itemLabel="Find Issues" itemValue="findIssue"/>
<f:selectItem itemLabel="Create Issue" itemValue="editIssue"/>
<f:selectItems value="#{switcher.selectItems}"/>
</h:selectOneMenu>
<h:commandButton action="#{switcher.select}" value="Switch"/>
この例では、 ユーザーに新しい対話を開始させる 2 つの追加アイテムに加えて、 各対話のためのアイテムを含むメニューがあります。
詳細を持つ対話 (pages.xml
で指定) のみがドロップダウンメニューに含まれます。
対話一覧は対話切り替えに非常によく似ていますが、 表形式で表示される点が異なります。
<h:dataTable value="#{conversationList}" var="entry"
rendered="#{not empty conversationList}">
<h:column>
<f:facet name="header"
>Workspace</f:facet>
<h:commandLink action="#{entry.select}" value="#{entry.description}"/>
<h:outputText value="[current]" rendered="#{entry.current}"/>
</h:column>
<h:column>
<f:facet name="header"
>Activity</f:facet>
<h:outputText value="#{entry.startDatetime}">
<f:convertDateTime type="time" pattern="hh:mm a"/>
</h:outputText>
<h:outputText value=" - "/>
<h:outputText value="#{entry.lastDatetime}">
<f:convertDateTime type="time" pattern="hh:mm a"/>
</h:outputText>
</h:column>
<h:column>
<f:facet name="header"
>Action</f:facet>
<h:commandButton action="#{entry.select}" value="#{msg.Switch}"/>
<h:commandButton action="#{entry.destroy}" value="#{msg.Destroy}"/>
</h:column>
</h:dataTable
>
恐らく、 多くの方が独自のアプリケーションに合うようカスタマイズを希望するだろうと思います。
詳細を持つ対話のみがこの一覧に含まれます。
対話一覧によりユーザーがワークスペースを破壊することができるので留意してください。
重要ではありませんが、 JSF コンポーネントへのバインディングの保持には使用できないという制限が対話型コンポーネントにはあります。 (一般的には、 アプリケーション論理からビューに対する強い依存関係を作ってしまうため、 絶対的に必要でない限り一般的にはこの JSF の機能は使用しないようにした方がよいでしょう。) postback 要求で、 Seam 対話コンテキストが復元される前、 ビューの復元フェーズ中にコンポーネントのバインディングは更新されます。
これを回避するには、 イベントスコープコンポーネントを使ってコンポーネントバインディングを格納し、 それを必要とする対話スコープコンポーネントにインジェクトします。
@Name("grid")
@Scope(ScopeType.EVENT)
public class Grid
{
private HtmlPanelGrid htmlPanelGrid;
// getters and setters
...
}
@Name("gridEditor")
@Scope(ScopeType.CONVERSATION)
public class GridEditor
{
@In(required=false)
private Grid grid;
...
}
また、 対話スコープのコンポーネントを JSF コントロールのバインド先となるイベントスコープのコンポーネントにはインジェクトできません。 これには facesMessages
のようなコンポーネント内での Seam 構築が含まれます。
Alternatively, you can access the JSF component tree through the implicit uiComponent
handle. The following example accesses getRowIndex()
of the UIData
component which backs the data table during iteration, it prints the current row number:
<h:dataTable id="lineItemTable" var="lineItem" value="#{orderHome.lineItems}">
<h:column>
Row: #{uiComponent['lineItemTable'].rowIndex}
</h:column>
...
</h:dataTable
>
JSF UI のコンポーネントはこのマップ内のクライアント識別子で使用可能になります。
Seam コンポーネントへの並列コールに関する全般は 項4.1.10. 「同時並行処理モデル」 でご覧ください。 ここでは並列が発生する最も一般的な状況について説明しています (AJAX 要求から対話的コンポーネントにアクセスする)。 クライアントで発生したイベントの制御に Ajax クライアントライブラリが提供すべきオプションについて説明してから RichFaces で提供されるオプションについて見ていきます。
対話的コンポーネントでは実際の並列アクセスは許可されないので、 Seam は要求を連続的に処理するようそれぞれを待ち行列に入れます。 これにより各要求は決定論的に実行されるようになります。 ただし、 シンプルなキューはあまりよくありません。 まず、 メソッドがなんらかの理由で完了に長時間かかっている場合、 クライアントが要求を生成するたび繰り返してそれを実行するのはよくありません (サービス妨害攻撃の可能性)。 また、 ユーザーに対するステータスのクィックアップデートに AJAX が頻繁に使用されるためアクションを長時間実行し続けるのは実用的ではありません。
したがって、 長時間実行の対話の内側で作業する場合は Seam は一定期間アクションイベントを待ち行列に入れます (並列要求タイムアウト)。 時間通りにイベントを処理できないと一時的な対話を作成してユーザーに状況を知らせるメッセージを出力します。 このため、 AJAX イベントでサーバーを溢れさせないようにすることが非常に重要となります。
components.xml で並列要求のタイムアウトにミリ秒単位で適当なデフォルトを設定することができます。
<core:manager concurrent-request-timeout="500" />
また、 ページごとに並列要求のタイムアウトを微調整することもできます。
<page view-id="/book.xhtml"
conversation-required="true"
login-required="true"
concurrent-request-timeout="2000" />
ここまではユーザーに対して連続的に出現する AJAX 要求について説明してきました。 クライアントはサーバーにイベントが発生したことを伝え、 その結果に応じてページの一部を表示します。 この方法は AJAX が軽量である場合には最適となります (1 コラム内の数字の合計を計算するなど呼び出されるメソッドがシンプルである場合)。 しかし、 計算に 1 分を要するような複雑な計算が必要な場合はどうでしょう。
重量のある計算にはポーリングベースの方法を使用してください。 クライアントがサーバーに AJAX 要求を送信し、 これによりサーバーで非同期にアクションが実行され (クライアントへの応答が即時となる)、 クライアントは次にサーバーに更新をポーリングします。 それぞれのアクションがすべて実行する (いずれもタイムアウトしない) ことが重要となるような長時間実行のアクションがある場合に適した方法となります。
まず、 簡単な「連続」要求を使用するのかどうか、 あるいはポーリングの方法を使用するのかどうかを決める必要があります。
「連続」要求を選択する場合は要求が完了する長さを推測する必要があります。 並列要求のタイムアウトに比べ十分に短い時間で完了するかどうか推測してみます。 不十分な場合はこのページの並列要求のタイムアウトを変更します (前述の通り)。 サーバーを要求で溢れさせないようにするためクライアント側で行列待ちをさせた方がよいでしょう。 イベントが頻繁に発生し (入力フィールドの keypress や onblur など) クライアントの即時更新が優先事項ではない場合はクライアント側で要求の遅延をセットしてください。 要求遅延の作業を行う場合、 イベントがサーバー側でも行列待ちできるようにすることを考慮に入れてください。
最後に、 クライアントライブラリは未完了の重複要求を最新のものを残してすべて停止するオプションを提供することができます。
ポールスタイルのデザインを使用する場合は細かな調整があまり必要ありません。 アクションメソッド @Asynchronous
をマークしてポーリングの間隔を決定するだけです。
int total;
// This method is called when an event occurs on the client
// It takes a really long time to execute
@Asynchronous
public void calculateTotal() {
total = someReallyComplicatedCalculation();
}
// This method is called as the result of the poll
// It's very quick to execute
public int getTotal() {
return total;
}
並列要求を対話的コンポーネントに対して行列待ちに入れるよう十分注意をしてアプリケーションを設定しても、 サーバー側がオーバーロードとなり要求処理にかかる時間が concurrent-request-timeout
を越えるまでに全要求を処理できなくなる危険性があります。 こうした場合、 Seam は pages.xml
で処理が可能な ConcurrentRequestTimeoutException
を送出します。 HTTP 503 エラーの送信をお勧めします。
<exception class="org.jboss.seam.ConcurrentRequestTimeoutException" log-level="trace">
<http-error error-code="503" />
</exception>
サーバーは現在、 一時的な過負荷またはメンテナンスのため要求を処理することができません。 しばらく待つと緩和されるであろう一時的な状態であることを指します。
代わりにエラーページにリダイレクトすることができます。
<exception class="org.jboss.seam.ConcurrentRequestTimeoutException" log-level="trace">
<end-conversation/>
<redirect view-id="/error.xhtml">
<message>The server is too busy to process your request, please try again later</message>
</redirect>
</exception>
ICEfaces, RichFaces Ajax and Seam Remoting can all handle HTTP error codes. Seam Remoting will pop up a dialog box showing the HTTP error. ICEfaces will indicate the error in its connection status component. RichFaces provides the most complete support for handling HTTP errors by providing a user definable callback. For example, to show the error message to the user:
<script type="text/javascript"> A4J.AJAX.onError = function(req,status,message) { alert("An error occurred"); }; </script>
If instead of an error code, the server reports that the view has expired, perhaps because the session timed out, you use a separate callback function in RichFaces to handle this scenario.
<script type="text/javascript"> A4J.AJAX.onExpired = function(loc,message) { alert("View expired"); }; </script>
Alternatively, you can allow RichFaces handle the error, in which case the user will be presented with a prompt that reads "View state could't be restored - reload page?" You can customize this message globally by setting the following message key in an application resource bundle.
AJAX_VIEW_EXPIRED=View expired. Please reload the page.
RichFaces (Ajax4jsf) is the Ajax library most commonly used with Seam, and provides all the controls discussed above:
eventsQueue
— provides a queue in which events are placed. All events are queued and requests are sent to the server serially. This is useful if the request to the server can take some time to execute (e.g. heavy computation, retrieving information from a slow source) as the server isn't flooded.
ignoreDupResponses
— ignores the response produced by the request if a more recent 'similar' request is already in the queue. ignoreDupResponses="true" does not cancel the processing of the request on the server side — just prevents unnecessary updates on the client side.
このオプションは複数の並列要求が作成されるのを許可するため、 Seam の対話で使用する際は注意が必要です。
requestDelay
— 要求がキューに残っている時間を定義します (ミリ秒単位)。 要求がこの時間を過ぎても処理されないと要求は送信されるか (応答が受信されているかどうかにかかわらず) 破棄 (より新しい同様のイベントがキューにある場合) されます。
このオプションは複数の並列要求が作成されるのを許可するため、 Seam の対話で使用する場合は注意が必要です。 アクションが実行に要する時間よりセットした遅延 (並列要求のタイムアウトと併用) の方が長いことを確認する必要があります。
<a:poll reRender="total" interval="1000" />
— サーバーにポーリングを行い必要に応じてエリアを表示します。