SeamFramework.orgCommunity Documentation
コンテキスト依存コンポーネントモデルを補完するものとして、Seamアプリケーションの特徴となっている極度の疎結合を促進させる二つの基本概念が存在します。 最初のものは、イベントがJSFライクなメソッドバインディング式(method binding expression) を通じてイベントリスナーへマップできるような強力なイベントモデルです。 二番目のものは、ビジネスロジックを実装するコンポーネントに対して横断的関心事 (cross-cutting concerns) を適用するためにアノテーションやインタセプタを広範囲に使用しているということです。
Seamコンポーネントモデルはイベント駆動アプリケーション で使うために開発されました。特に、細粒度イベントモデル (fine-grained eventing model) での細粒度かつ疎結合コンポーネント開発を可能にします。 Seamでのイベントは、すでにご存知のように、いくつかのタイプがあります。
JSFイベント
jBPM状態遷移イベント
Seamページアクション
Seamコンポーネント駆動イベント
Seamコンテキスト依存イベント
これらの多様なイベントすべてはJSF ELメソッドバインディング式を通じてSeamコンポーネントへマップされます。JSFイベントは、JSFテンプレートで次のように定義されます。
<h:commandButton value="Click me!" action="#{helloWorld.sayHello}"/>
jBPM遷移イベントは、jBPMプロセス定義またはページフロー定義で規定されます。
<start-page name="hello" view-id="/hello.jsp">
<transition to="hello">
<action expression="#{helloWorld.sayHello}"/>
</transition>
</start-page
>
JSF イベントや jPBM イベントの詳細については本ガイド以外でも見つけることができるので、 ここでは Seam によって定義される別の二種類のイベントについて見ていきます。
Seamページアクションはページのレンダリングの直前に発生するイベントです。 ページアクションはWEB-INF/pages.xml
で宣言します。 特定のJSFビューidのためのページアクションを定義することも可能です。
<pages>
<page view-id="/hello.jsp" action="#{helloWorld.sayHello}"/>
</pages
>
あるいは、 *
ワイルドカードを使ってパターンに一致するすべてのビュー ID
を指定することもできます。
<pages>
<page view-id="/hello/*" action="#{helloWorld.sayHello}"/>
</pages
>
もし <page>
要素が細粒度のページ記述子に定義されていたなら、その view-id
属性は暗黙的に指定されるので省くことができます。
複数のワイルドカード化されたページアクションがカレントビューidに一致するなら、 Seamは曖昧な指定から明確な指定への順 (least-specific to most-specific) で、 それらすべてのアクションを呼び出します。
ページアクションのメソッドはJSF outcomeを返すことができます。もしも、そのoutcome がnullでなければ、Seamはビューをナビゲートするためその定義済みナビゲーション規則を使います。
さらに、<page>
要素で指定されたビューidは、実際の JSP や Facelets のページに対応する必要はないのです! そこで、ページアクションを使用した Struts や WebWork のような 伝統的なアクション指向フレームワークの機能を再現することもできます。これは non-faces 要求に応答して複雑なことをしたいならばとても役に立ちます。
複数または条件付きのページアクションは<action>
タグを使って指定できます。
<pages>
<page view-id="/hello.jsp">
<action execute="#{helloWorld.sayHello}" if="#{not validation.failed}"/>
<action execute="#{hitCount.increment}"/>
</page>
</pages
>
ページアクションは初期要求 (non-faces 要求) とポストバック要求 (faces 要求) の両方に対して実行されます。データをロードするのにページアクションを使うとすると、この操作はポストバックを実行しようとしている標準の JSF アクションと衝突する可能性があります。このページアクションを無効にする一つの方法は初期要求に対してのみ条件を true に設定することです。
<pages>
<page view-id="/dashboard.xhtml">
<action execute="#{dashboard.loadData}"
if="#{not facesContext.renderKit.responseStateManager.isPostback(facesContext)}"/>
</page>
</pages
>
この条件は要求がポストバックかどうかを決定するために ResponseStateManager#isPostback(FacesContext)
を調べます。ResponseStateManager は FacesContext.getCurrentInstance().getRenderKit().getResponseStateManager()
を使ってアクセスされます。
JSF の API の冗長さに患わされないように、タイピング量を減らすのと同じ効果を達成するための組み込み条件を提供します。ポストバックに関するページアクションを無効にするには、次のように単に on-postback
を false
に設定します。
<pages>
<page view-id="/dashboard.xhtml">
<action execute="#{dashboard.loadData}" on-postback="false"/>
</page>
</pages
>
下位互換性のため on-postback
属性のデフォルト値は true になっていますが、反対の設定の方がより頻繁に行われることになるでしょう。
JSF faces 要求 (フォーム送信) は「アクション」 (メソッドバインディング) と「パラメータ」 (入力値バインディング) の両方をカプセル化します。 ページアクションにもパラメータが必要かもしれません。
GET 要求はブックマーク可能なので、 ページパラメータは人間が読める要求パラメータとして引き渡されます (JSF フォーム入力とは異なるもの)。
アクションメソッドを指定する、あるいは指定しないページパラメータを使うことができます。
Seamでは、名前付き要求パラメータをモデルオブジェクトの属性を対応させる値バインディングが可能です。
<pages>
<page view-id="/hello.jsp" action="#{helloWorld.sayHello}">
<param name="firstName" value="#{person.firstName}"/>
<param name="lastName" value="#{person.lastName}"/>
</page>
</pages
>
<param>
宣言は双方向で、まさにJSF入力の値バインディングのようです。
指定されたビューidに対するnon-faces (GET) 要求が発生するとき、 Seamは、適切な型変換を施した後に、名前付きパラメータの値をそのモデルオブジェクトに設定します。
任意の <s:link>
や <s:button>
は透過的に要求パラメータを含みます。 パラメータ値は、 レンダリングフェーズの間に (<s:link>
がレンダリングされるとき) 値バインディングを評価することによって決定されます。
ビューidに対する<redirect/>
の任意のナビゲーションルールは要求パラメータを透過的に含みます。パラメータの値はアプリケーションフェーズの最後に値バインディングを評価することで決定されます。
その値は透過的にビューidで指定されたページへのJSFフォーム送信に伝播します。 これはビューパラメータはfaces要求のためのPAGE
スコープのコンテキスト変数のように振舞うことを意味します。
この背後にある本質的な考えは、他の任意のページから /hello.jsp
への (または /hello.jsp
から /hello.jsp
へ戻るような) 遷移があるにもかかわらず、 値バインディングで参照されるモデル属性の値は対話 (または他のサーバ側の状態) を必要とせずに「記録されている」ということです。
もし name
属性が指定されていたら、要求パラメータは PAGE
コンテキストを使って伝播します(モデルプロパティへはマッピングされません)。
<pages>
<page view-id="/hello.jsp" action="#{helloWorld.sayHello}">
<param name="firstName" />
<param name="lastName" />
</page>
</pages
>
ページパラメータの伝播は、マスターから詳細画面に遷移するようなマルチレイヤのCRUDページを作成したいときには特に便利です。それは以前操作していた(例えば、保存ボタンを押したときの)ビューや編集していたビューを「覚えておく」のに使えます。
任意の <s:link>
や <s:button>
は、要求パラメータがビューのページパラメータとして記入されていれば、透過的にそのパラメータを伝播します。
その値は透過的にビューidで指定されたページへのJSFフォーム送信に伝播します。 (これはビューパラメータはfaces要求のためのPAGE
スコープのコンテキスト変数のように振舞います)。
これらすべてはかなり複雑に聞こえますし、そのような変わった概念を努力して使う価値があるのかと疑問に思うことでしょう。実際は、そのアイディアは一旦「理解」してしまえばとても自然なものです。時間をかけてこれを理解することは絶対に価値があります。ページパラメータはnon-faces要求をまたがって状態を伝播するのに最も洗練された方法です。それらは検索結果をブックマーク可能な検索画面にするような問題にとって特に素晴らしい方法です。そのような問題では、同じコードでPOSTとGET要求の両方を処理できるようなアプリケーションコードを書けるようにしたいと望むでしょう。ページパラメータを使えば、ビュー定義内で要求パラメータのリストを繰り返し書く必要がなくなり、リダイレクトをもっと簡単にコーディングできるようになります。
書き換えはpages.xml
内のビューで発見される書き換えパターンを元に発生します。SeamのURL書き換えは同一のパターンに基づいて入力方向と出力方向の両方をURL書き換えを実施します。
<page view-id="/home.xhtml">
<rewrite pattern="/home" />
</page>
この場合は、 /home
のための任意の入力要求は /home.xhtml
に送られます。さらに興味深いことには、通常 /home.seam
を指し示す任意のリンクは /home
に書き換えられます。書き換えパターンはクエリーパラメータの前のURL部分にのみマッチします。それゆえ、 /home.seam?conversationId=13
と /home.seam?color=red
は両方ともこの書き換え規則にマッチします。
書き換え規則は、以下の規則に示すように、これらのクエリーパラメータを考慮することができます。
<page view-id="/home.xhtml">
<rewrite pattern="/home/{color}" />
<rewrite pattern="/home" />
</page>
この場合、 /home/red
の入力要求はあたかも /home.seam?color=red
のように振る舞います。同様に、もしcolor がページパラメータなら /home.seam?color=blue
と通常表示される出力URLは、代わりに /home/blue
と出力されます。
Seamのフィンガープリントを隠す別のオプションを指定することで、デフォルトのSeamクエリーパラメータもURL書き換えを使ってマップ可能です。次の例では、/search.seam?conversationId=13
は/search-13
と書き換えられます。
<page view-id="/search.xhtml">
<rewrite pattern="/search-{conversationId}" />
<rewrite pattern="/search" />
</page>
Seam URL書き換えは、ビュー単位での単純で双方向の書き換えを提供します。非Seamコンポーネントをカバーするより複雑な書き換え規則のためには、Seamアプリケーションは継続して org.tuckey URLRewriteFilter
を使う、あるいはWebサーバでの書き換え規則を適用することが可能です。
URL書き換えはSeamに書き換えフィルタを有効にすることを要求します。書き換えフィルタについては項30.1.4.3. 「URL のリライト」で説明します。
複雑なモデルのプロパティのためにJSFコンバータを指定することも可能です。
<pages>
<page view-id="/calculator.jsp" action="#{calculator.calculate}">
<param name="x" value="#{calculator.lhs}"/>
<param name="y" value="#{calculator.rhs}"/>
<param name="op" converterId="com.my.calculator.OperatorConverter" value="#{calculator.op}"/>
</page>
</pages
>
あるいは代わりに次のようにすることもできます。
<pages>
<page view-id="/calculator.jsp" action="#{calculator.calculate}">
<param name="x" value="#{calculator.lhs}"/>
<param name="y" value="#{calculator.rhs}"/>
<param name="op" converter="#{operatorConverter}" value="#{calculator.op}"/>
</page>
</pages
>
次のように、JSFバリデータと required="true"
も使用できます。
<pages>
<page view-id="/blog.xhtml">
<param name="date"
value="#{blog.date}"
validatorId="com.my.blog.PastDate"
required="true"/>
</page>
</pages
>
あるいは代わりに次のようにすることもできます。
<pages>
<page view-id="/blog.xhtml">
<param name="date"
value="#{blog.date}"
validator="#{pastDateValidator}"
required="true"/>
</page>
</pages
>
さらに良いことには、モデルベースの Hiberante バリデータアノテーションは自動的に認識されて妥当性の検証をします。Seam は文字列パラメータ値と date を相互に変換するデフォルト date コンバータも提供します。
型変換や妥当性検証が失敗したなら、グローバルな FacesMessage
がFacesContext
に追加されます。
Seamアプリケーションではfaces-config.xml
で定義される標準のJSFナビゲーション規則を使用できます。しかし、JSFナビゲーション規則は厄介な制限があります。
リダイレクトで使われるときに要求パラメータを指定できません。
規則から対話 (conversation) の開始や終了ができません。
規則はアクションメソッドの戻り値の評価によって動作します。 つまり、任意のEL式を評価することはできません。
さらにpages.xml
と faces-config.xml
の間に「オーケストレーション」ロジックが分散してしまうという問題があります。このロジックは pages.xml
に統合した方が良いでしょう。
このJSFナビゲーション規則は、
<navigation-rule>
<from-view-id
>/editDocument.xhtml</from-view-id>
<navigation-case>
<from-action
>#{documentEditor.update}</from-action>
<from-outcome
>success</from-outcome>
<to-view-id
>/viewDocument.xhtml</to-view-id>
<redirect/>
</navigation-case>
</navigation-rule
>
次のように書き直すことができます。
<page view-id="/editDocument.xhtml">
<navigation from-action="#{documentEditor.update}">
<rule if-outcome="success">
<redirect view-id="/viewDocument.xhtml"/>
</rule>
</navigation>
</page
>
しかし、DocumentEditor
コンポーネントが文字列の戻り値(JSFの結果)を持つような汚いコードを書かなくてすめばさらに良くなるでしょう。Seamでは次のように書くことができます。
<page view-id="/editDocument.xhtml">
<navigation from-action="#{documentEditor.update}"
evaluate="#{documentEditor.errors.size}">
<rule if-outcome="0">
<redirect view-id="/viewDocument.xhtml"/>
</rule>
</navigation>
</page
>
または、次のようにすら書くことができます。
<page view-id="/editDocument.xhtml">
<navigation from-action="#{documentEditor.update}">
<rule if="#{documentEditor.errors.empty}">
<redirect view-id="/viewDocument.xhtml"/>
</rule>
</navigation>
</page
>
最初の形式は後続の規則によって使用されるようにcoutcomeの値を決定する値バインディングを評価します。 二番目のアプローチはoutcomeを無視し、各々の規則の値バインディングを評価します。
もちろん、更新が成功したなら、現在の対話 (conversation) を終了させたいことでしょう。 これには、次のようにします。
<page view-id="/editDocument.xhtml">
<navigation from-action="#{documentEditor.update}">
<rule if="#{documentEditor.errors.empty}">
<end-conversation/>
<redirect view-id="/viewDocument.xhtml"/>
</rule>
</navigation>
</page
>
対話が終了してしまうと、後続の要求は関心があるのがどのドキュメントであるのか知ることができません。要求パラメータとしてドキュメントIDを渡すことができます。そして、それはビューをブックマーク可能にもしてくれます。
<page view-id="/editDocument.xhtml">
<navigation from-action="#{documentEditor.update}">
<rule if="#{documentEditor.errors.empty}">
<end-conversation/>
<redirect view-id="/viewDocument.xhtml">
<param name="documentId" value="#{documentEditor.documentId}"/>
</redirect>
</rule>
</navigation>
</page
>
outcomeがnullとなるのはJSFでは特別なケースです。coucomeがnullは「そのページを再表示する」 という意味に解釈されます。次のナビゲーション規則はnullではないoutcomeに適合しますが、outcomeがnullのものには適合しません。
<page view-id="/editDocument.xhtml">
<navigation from-action="#{documentEditor.update}">
<rule>
<render view-id="/viewDocument.xhtml"/>
</rule>
</navigation>
</page
>
outcomeがnullの場合にナビゲーションをしたいのであれば、 代わりに次の形式を使います。
<page view-id="/editDocument.xhtml">
<navigation from-action="#{documentEditor.update}">
<render view-id="/viewDocument.xhtml"/>
</navigation>
</page
>
ビューidはJSF EL式として与えることができます。
<page view-id="/editDocument.xhtml">
<navigation>
<rule if-outcome="success">
<redirect view-id="/#{userAgent}/displayDocument.xhtml"/>
</rule>
</navigation>
</page
>
もしも、大量のページアクション、ページパラメータ、ナビゲーション規則が あるなら、それらの定義を複数のファイルに分割したいことでしょう。 ビューidが/calc/calculator.jsp
のアクションやパラメータは calc/calculator.page.xml
という名前のリソースに定義可能です。 この場合のルート要素は<page>
要素で、ビューidは暗に指定されます。
<page action="#{calculator.calculate}">
<param name="x" value="#{calculator.lhs}"/>
<param name="y" value="#{calculator.rhs}"/>
<param name="op" converter="#{operatorConverter}" value="#{calculator.op}"/>
</page
>
Seamコンポーネント同士は互いのメソッドを呼ぶことだけでやりとりができます。 ステートフルコンポーネントはobserver/observableパターンを実装することすらできます。 しかし、コンポーネントが互いにメソッドを直接呼ぶとき、より疎結合な方法でやりとりできるために、Seamはコンポーネント駆動イベントを提供します。
イベントリスナー (observers) をcomponents.xml
に指定します。
<components>
<event type="hello">
<action execute="#{helloListener.sayHelloBack}"/>
<action execute="#{logger.logHello}"/>
</event>
</components
>
ここでevent type は単なる任意の文字列です。
イベントが発生するとき、そのイベント用に登録されたアクションはcomponents.xml
に出現した順番で呼び出されます。コンポーネントはどのようにイベントを発行するのでしょうか。Seamはこのために組み込みコンポーネントを提供します。
@Name("helloWorld")
public class HelloWorld {
public void sayHello() {
FacesMessages.instance().add("Hello World!");
Events.instance().raiseEvent("hello");
}
}
あるいは、アノテーションを使うことも可能です。
@Name("helloWorld")
public class HelloWorld {
@RaiseEvent("hello")
public void sayHello() {
FacesMessages.instance().add("Hello World!");
}
}
このイベントプロデューサはイベントコンシューマになんら依存していないことに注意してください。 そのイベントリスナーはまったくプロデューサと依存関係がないように実装できるのです。
@Name("helloListener")
public class HelloListener {
public void sayHelloBack() {
FacesMessages.instance().add("Hello to you too!");
}
}
上記の components.xml
で定義されたメソッドバインディングは、コンシューマへのイベントのマップを扱います。もし components.xml
ファイルを編集するのが嫌ならば、その代わりにアノテーションを使うこともできます。
@Name("helloListener")
public class HelloListener {
@Observer("hello")
public void sayHelloBack() {
FacesMessages.instance().add("Hello to you too!");
}
}
なぜイベントオブジェクトについて今まで何も言及してこなかったのか疑問を思われるかもしれません。Seamでは、イベントプロデューサとリスナーの間の状態の伝播のためのイベントオブジェクトは必要ではありません。状態はSeamコンテキストで保持され、コンポーネント間で共有されます。しかし、もしイベントオブジェクトを渡したいのであれば、次のようにすることも可能です。
@Name("helloWorld")
public class HelloWorld {
private String name;
public void sayHello() {
FacesMessages.instance().add("Hello World, my name is #0.", name);
Events.instance().raiseEvent("hello", name);
}
}
@Name("helloListener")
public class HelloListener {
@Observer("hello")
public void sayHelloBack(String name) {
FacesMessages.instance().add("Hello #0!", name);
}
}
Seamは特殊なフレームワークの統合のためにアプリケーションが利用可能な多くの組み込みイベントを定義します。そのイベントとは次のようなものです。
org.jboss.seam.validationFailed
— JSFバリデーションが失敗したときに呼ばれます
org.jboss.seam.noConversation
— 長期対話が存在しない状態で長期対話が要求されたときに呼ばれます
org.jboss.seam.preSetVariable.<name>
— コンテキスト変数 <name> 設定されたときに呼ばれます
org.jboss.seam.postSetVariable.<name>
— コンテキスト変数 <name> が設定されたときに呼ばれます
org.jboss.seam.preRemoveVariable.<name>
— コンテキスト変数 <name> が設定されなくなったら呼ばれます
org.jboss.seam.postRemoveVariable.<name>
— コンテキスト変数 <name> が設定されなくなったら呼ばれます
org.jboss.seam.preDestroyContext.<SCOPE>
— <SCOPE> コンテキストが破壊される前に呼ばれます
org.jboss.seam.postDestroyContext.<SCOPE>
— <SCOPE> コンテキストが破壊された後に呼ばれます
org.jboss.seam.beginConversation
— 長期対話が開始するときにはいつでも呼ばれます
org.jboss.seam.endConversation
— 長期対話が終了するときはいつでも呼ばれます
org.jboss.seam.conversationTimeout
— 対話タイムアウトが発生するときに呼ばれます。対話IDはパラメータとして渡されます。
org.jboss.seam.beginPageflow
— ページフローが開始するときに呼ばれます
org.jboss.seam.beginPageflow.<name>
— ページフロー <name> が開始するときに呼ばれます
org.jboss.seam.endPageflow
— ページフローが終了するときに呼ばれます
org.jboss.seam.endPageflow.<name>
— ページフロー <name> が終了するときに呼ばれます
org.jboss.seam.createProcess.<name>
— プロセス <name> が生成されるときに呼ばれます
org.jboss.seam.endProcess.<name>
— プロセス <name> が終了するとき呼ばれます
org.jboss.seam.initProcess.<name>
— プロセス <name> が対話と関連付けられるときに呼ばれます
org.jboss.seam.initTask.<name>
— タスク <name> が対話と関連付けられるときに呼ばれます
org.jboss.seam.startTask.<name>
— タスク <name> が開始させられるときに呼ばれます
org.jboss.seam.endTask.<name>
— タスク <name> が終了させられるときに呼ばれます
org.jboss.seam.postCreate.<name>
— コンポーネント <name> が生成されたときに呼ばれます
org.jboss.seam.preDestroy.<name>
— コンポーネント <name> が破壊されるときに呼ばれます
org.jboss.seam.beforePhase
— JSFフェーズの開始前に呼ばれます
org.jboss.seam.afterPhase
— JSFフェーズの終了後に呼ばれます
org.jboss.seam.postInitialization
— Seamが初期化完了し、すべてのコンポーネントを起動するときに呼ばれます
org.jboss.seam.postReInitialization
— Seamが再初期化を完了し、再デプロイ後にすべてのコンポーネントを起動するときに呼ばれます
org.jboss.seam.exceptionHandled.<type>
— キャッチされなかった例外がSeamによって処理されるときに呼ばれます
org.jboss.seam.exceptionHandled
— キャッチされなかった例外がSeamによって処理されるときに呼ばれます
org.jboss.seam.exceptionNotHandled
— キャッチされなかった例外のためのハンドラが存在しなかったときに呼ばれます
org.jboss.seam.afterTransactionSuccess
— Seamアプリケーションフレームワークでトランザクションが成功するときに呼ばれます
org.jboss.seam.afterTransactionSuccess.<name>
— <name>
という名前のエンティティを管理するSeamアプリケーションフレームワークでトランザクションが成功するときに呼ばれます
org.jboss.seam.security.loggedOut
— ユーザーがログアウトするときに呼ばれます
org.jboss.seam.security.loginFailed
— ユーザー認証が失敗するときに呼ばれます
org.jboss.seam.security.loginSuccessful
— ユーザー認証が成功したときに呼ばれます
org.jboss.seam.security.notAuthorized
— 認可のチェックが失敗するときに呼ばれます
org.jboss.seam.security.notLoggedIn
— ユーザー認証が必要とされるところで未認証のユーザがアクセスしたときに呼ばれます
org.jboss.seam.security.postAuthenticate.
— ユーザーが認証された後に呼ばれます
org.jboss.seam.security.preAuthenticate
— ユーザーを認証しようとする前に呼ばれます
Seamコンポーネントは、他のコンポーネント駆動イベントを観察する (observe) のとまったく同様に これらのどのイベントでも観察することが可能です。
EJB 3.0 はセッション Bean コンポーネントに対して標準的なインタセプタモデルを導入しました。 Bean にインタセプタを追加するには、 @AroundInvoke
というアノテーションが付加されたメソッドの付いたクラスを記述して、 その Bean に対してインタセプタのクラス名を指定する @Interceptors
のアノテーションを付ける必要があります。
public class LoggedInInterceptor {
@AroundInvoke
public Object checkLoggedIn(InvocationContext invocation) throws Exception {
boolean isLoggedIn = Contexts.getSessionContext().get("loggedIn")!=null;
if (isLoggedIn) {
//the user is already logged in
return invocation.proceed();
}
else {
//the user is not logged in, fwd to login page
return "login";
}
}
}
このインタセプタをアクションリスナーとして動作するセッションBeanに対して適用するためには、そのセッションBeanに @Interceptors(LoggedInInterceptor.class)
というアノテーションを付加しなければなりません。これはちょっと見栄えの悪いアノテーションです。Seamはクラスレベルインタセプタのためのメタアノテーションとして @Interceptors
を使えるようにEJB3のインタセプタフレームワーク上に構築されています。例えば、以下では、 @LoggedIn
アノテーションを生成します。
@Target(TYPE)
@Retention(RUNTIME)
@Interceptors(LoggedInInterceptor.class)
public @interface LoggedIn {}
こうして、 このインタセプタを適用するのにアクションリスナー Bean に@LoggedIn
アノテーションだけを付加すればよくなりました。
@Stateless
@Name("changePasswordAction")
@LoggedIn
@Interceptors(SeamInterceptor.class)
public class ChangePasswordAction implements ChangePassword {
...
public String changePassword() { ... }
}
インタセプタの順番が重要な場合 (通常は重要となる)、 インタセプタクラスに対して @Interceptor
アノテーションを追加しインタセプタの半順序を指定することが可能です。
@Interceptor(around={BijectionInterceptor.class,
ValidationInterceptor.class,
ConversationInterceptor.class},
within=RemoveInterceptor.class)
public class LoggedInInterceptor
{
...
}
「クライアント側」インタセプタを持つこともできます。 EJB3 のいずれの組み込み機能とでも併用することができます。
@Interceptor(type=CLIENT)
public class LoggedInInterceptor
{
...
}
EJB インタセプタはステートフルで、 インタセプトする対象となるコンポーネントと同じライフルサイクルに従います。 状態を維持する必要がないインタセプタの場合、 Seam では @Interceptor(stateless=true)
を指定することでパフォーマンス最適化ができるようになります。
Seamの多くの機能は、前の例で登場したようなインタセプタを含む組み込みのSeamインタセプタによって実装されています。コンポーネントにアノテーションを付加することによってこれらのインタセプタを明示的に指定する必要はありませんが、インタセプタを適用可能なすべてのSeamコンポーネントのために存在しているのです。
Seam インタセプタは EJB3 Bean だけでなく JavaBean コンポーネントにも使うことができます。
EJB は、 インタセプションを (@AroundInvoke
を使った) ビジネスメソッドだけでなく、 ライフサイクルメソッド @PostConstruct
、 @PreDestroy
、 @PrePassivate
そして @PostActive
に対しても定義します。 Seam は、 コンポーネントとインタセプタに対するこれらすべてのライフサイクルメソッドを EJB3 Bean だけでなく JavaBean コンポーネントに対してもサポートします (JavaBean コンポーネントにとって意味のない @PreDestroy
は除きます)。
JSF は例外処理に関しては驚くほど制限があります。 この問題の部分的な回避策として、 Seam は例外クラスにアノテーションを付けるか XML ファイルに例外クラスを宣言することで例外となる特定クラスを処理する方法を定義することができます。 この機能は、 指定された例外がトランザクションロールバックの原因になるべきか否かを指定するのに EJB 3.0 標準の @ApplicationException
アノテーションと一緒に使われることが意図されていています。
Bean のビジネスメソッドによって例外がスローされると、 その例外は現在のトランザクションに直ちにロールバックが必要として印を付けるかどうかを制御できるよう明確な規則を EJB は定義しています。 システム例外 は常にトランザクションロールバックとなり、 アプリケーション例外 はデフォルトではロールバックとはなりませんが @ApplicationException(rollback=true)
が指定されるとロールバックとなります。 (アプリケーション例外とは、 チェックの付いた例外、 または @ApplicationException
アノテーションが付いたチェックのない例外です。 システム例外とは、 @ApplicationException
アノテーションもチェックも付いていない例外です。)
ロールバック用にトランザクションに印を付けるのと、 実際にロールバックを行うのとは異なります。 例外規則にはトランザクションにロールバックが必要であると印が付けられることだけしか言及していませんが、 例外がスローされた後でもそれはアクティブのままである可能性があるということに注意してください。
Seam は EJB 3.0 例外のロールバック規則を Seam JavaBean コンポーネントに対しても適用します。
しかし、 これらの規則は Seam コンポーネント層でのみ適用されるます。 では、 例外がキャッチされることなく Seam コンポーネント層の外部に伝播し、 さらに JSF 層の外に伝播したらどうなるでしょうか。 仕掛かり中のトランザクションをオープンしたままで放置するのは間違いなので、 例外が発生し Seam コンポーネント層でキャッチされないと Seam はアクティブトランザクションを必ずロールバックします。
Seamの例外処理を有効にするには、主となるサーブレットフィルタをweb.xml
で宣言したことを確認する必要があります。
<filter>
<filter-name
>Seam Filter</filter-name>
<filter-class
>org.jboss.seam.servlet.SeamFilter</filter-class>
</filter>
<filter-mapping>
<filter-name
>Seam Filter</filter-name>
<url-pattern
>*.seam</url-pattern>
</filter-mapping
>
例外ハンドラを機能させる場合は、 web.xml
の Facelets 開発モードおよび components.xml
の Seam デバッグモードも無効にする必要があります。
次の例外は Seam コンポーネント層の外部に伝播すると必ず HTTP 404 エラーになります。 スローされてもすぐには現在のトランザクションをロールバックしませんが、 別の Seam コンポーネントによって例外がキャッチされないとこのトランザクションはロールバックされます。
@HttpError(errorCode=404)
public class ApplicationException extends Exception { ... }
この例外は Seam コンポーネント層の外部に伝播すると必ずブラウザリダイレクトになります。 また、 現在の対話も終了させます。 これにより現在のトランザクションを即時ロールバックさせることになります。
@Redirect(viewId="/failure.xhtml", end=true)
@ApplicationException(rollback=true)
public class UnrecoverableApplicationException extends RuntimeException { ... }
ELを使ってリダイレクト先の ビューid
を指定することも可能です。
この例外は Seam コンポーネント層の外部に伝播すると必ずユーザーへのメッセージを付けてリダイレクトされます。 また、 現在のトランザクションも即時ロールバックさせます。
@Redirect(viewId="/error.xhtml", message="Unexpected error")
public class SystemException extends RuntimeException { ... }
関心のあるすべての例外クラスへアノテーションを付加することは不可能なので、Seamはこの機能を pages.xml
でも指定できるようにしています。
<pages>
<exception class="javax.persistence.EntityNotFoundException">
<http-error error-code="404"/>
</exception>
<exception class="javax.persistence.PersistenceException">
<end-conversation/>
<redirect view-id="/error.xhtml">
<message
>Database access failed</message>
</redirect>
</exception>
<exception>
<end-conversation/>
<redirect view-id="/error.xhtml">
<message
>Unexpected failure</message>
</redirect>
</exception>
</pages
>
最後の <exception>
宣言はクラスを指定していないので、 アノテーションまたは pages.xml
で指定されているもの以外すべての例外をキャッチします。
ELを使ってリダイレクト先の view-id
を指定することもできます。
EL によってキャッチした例外インスタンスにアクセスすることができます。 Seamはそれを対話コンテキストに置きます。例外のメッセージにアクセスする例は次の通り。
...
throw new AuthorizationException("You are not allowed to do this!");
<pages>
<exception class="org.jboss.seam.security.AuthorizationException">
<end-conversation/>
<redirect view-id="/error.xhtml">
<message severity="WARN"
>#{org.jboss.seam.handledException.message}</message>
</redirect>
</exception>
</pages
>
org.jboss.seam.handledException
は例外ハンドラによって実際に処理されたネストされた例外を保持します。その最も外側の(ラッパーの)例外はorg.jboss.seam.caughtException
によって取得可能です。
pages.xml
で定義された例外ハンドラについて、どの例外がログに記録されるかのログレベルを宣言できますし、ログに記録される例外を抑えることもできます。属性 log
と log-level
は例外のロギングを制御するために使用できます。次の例のように log="false"
と設定すると、指定された例外が発生するときにログメッセージは何も出力されません。
<exception class="org.jboss.seam.security.NotLoggedInException" log="false">
<redirect view-id="/register.xhtml">
<message severity="warn"
>You must be a member to use this feature</message>
</redirect>
</exception
>
もし log
属性が指定されなければ、 true
がデフォルトになります(つまり、例外はログに記録されます)。別の方法として、例外がログに記録されるレベルを制御する log-level
を指定することも可能です。
<exception class="org.jboss.seam.security.NotLoggedInException" log-level="info">
<redirect view-id="/register.xhtml">
<message severity="warn"
>You must be a member to use this feature</message>
</redirect>
</exception
>
log-level
に指定可能な値は、fatal, error, warn, info, debug
または trace
です。もし log-level
が指定されていない、もしくは、不正な値が設定されていれば、その値は error
がデフォルトになります。
もしJPAを使っているなら
<exception class="javax.persistence.EntityNotFoundException">
<redirect view-id="/error.xhtml">
<message
>Not found</message>
</redirect>
</exception>
<exception class="javax.persistence.OptimisticLockException">
<end-conversation/>
<redirect view-id="/error.xhtml">
<message
>Another user changed the same data, please try again</message>
</redirect>
</exception
>
もしSeamアプリケーションフレームワークを使っているなら
<exception class="org.jboss.seam.framework.EntityNotFoundException">
<redirect view-id="/error.xhtml">
<message
>Not found</message>
</redirect>
</exception
>
もしSeamセキュリティを使っているなら
<exception class="org.jboss.seam.security.AuthorizationException">
<redirect>
<message
>You don't have permission to do this</message>
</redirect>
</exception>
<exception class="org.jboss.seam.security.NotLoggedInException">
<redirect view-id="/login.xhtml">
<message
>Please log in first</message>
</redirect>
</exception
>
そして、JSFの場合
<exception class="javax.faces.application.ViewExpiredException">
<redirect view-id="/error.xhtml">
<message
>Your session has timed out, please try again</message>
</redirect>
</exception
>
ViewExpiredException
はセッションの有効期限が切れたときにポストバックをページに送信しようとすると発生します。 項7.4. 「長期対話の要求」 で説明しているように、Seam ページ記述子内でno-conversation-view-id
と conversation-required
を設定することで、対話の中でページにアクセスしているのであれば細かなセッションの有効期限の制御が可能になります。