コンテキスト依存コンポーネントモデルを補完するものとして、 Seam アプリケーションの特徴となっている極度の疎結合を促進させる 2 つの基本概念が存在します。 最初のものは、 イベントが JSF ライクなメソッド結合式 (method binding expression) を介してイベントリスナーにマップできるような強力なイベントモデルです。 2 番目のものは、 ビジネスロジックを実装するコンポーネントに対して横断的関心事 (cross-cutting concerns) を適用するためにアノテーションやインターセプタを広範囲に使用しているということです。
Seam コンポーネントモデルはイベント駆動アプリケーション で使うために開発されました。 特に、 微調整が行われたイベントモデルで微調整を必要とする疎結合コンポーネントの開発を可能にします。 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 によって定義される別の 2 種類のイベントについて見ていきます。
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>
現在のビュー ID に一致するページアクションがワイルドカードを使って指定すると複数ある場合、 Seam は曖昧な指定から明確な指定への順でそれらすべてのアクションを呼び出します。
ページアクションのメソッドは JSF outcome を返すことができます。 その outcome が null 以外の場合、 Seam はビューのナビゲートに定義済みナビゲーション規則を使用します。
さらに、 <page> 要素で指定されたビュー id は実際の JSP や Facelets ページに対応する必要がありません。 このため、 ページアクションを使用した Struts や WebWork のような従来のアクション指向フレームワークの機能を再現することができます。
TODO: translate struts action into page action
non-facesリクエスト (たとえば、 HTTP Get リクエスト) に対するレスポンスで複雑な処理をしたい場合などに非常に便利です。
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 へ戻るような) 他の任意のページを得ることができるのにもかかわらず、 値結合で参照されるモデル属性の値は対話 (または他のサーバー側の状態) を必要とせずに「記録されている」ということです。
かなり複雑な構造で、 このような非標準の構造が本当に役に立つのだろうかと不思議に思われるかもしれません。 実際には、 理解するとこの概念はごく自然なことになりますので、 時間を割いて理解するだけの価値は十分にあります。 ページパラメータは non-faces リクエスト全体への状態の伝播に最適な方法となります。 同じコードで POST と GET の両リクエストを処理する独自のアプリケーションコードを記述できるようにしたい場合、 特にブックマーク可能な結果ページでの画面検索などの問題に対して役立ちます。 ページパラメータによりビュー定義内でのリクエストパラメータの反復表示を解消し、 コードに 対するリダイレクトをより容易ににします。
ページパラメータを使用するために実際のページアクションメソッドバインディングは必要ありません。 以下で十分に有効になります。
<pages> <page view-id="/hello.jsp"> <param name="firstName" value="#{person.firstName}"/> <param name="lastName" value="#{person.lastName}"/> </page> </pages>
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>
Seam アプリケーションでは faces-config.xml で定義される標準の JSF ナビゲーション規則を使用できます。 しかし、 JSF ナビゲーション規則には厄介な制限があります。
リダイレクトする場合はリクエストパラメータを指定できません。
規則から対話の開始や終了はできません。
規則はアクションメソッドの戻り値の評価によって動作します。 つまり、 任意の EL 式を評価することはできません。
さらに問題なのは組合せ (orchestration) のロジックが 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>
しかし、 文字列の値を持つ戻り値 (JSF outcome) と DocumentEditor コンポーネントを併用する必要がなかったならば、 より良かったでしょう。そこで 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>
最初の形式は後続の規則によって使用されるように outcome の値を決定する値結合を評価します。 二番目のアプローチは outcome を無視し、 それぞれ可能性のある規則に対して値結合を評価します。
更新が成功したら当然、 現在の対話を終了させたい場合がほとんどでしょう。 これには、次のようにします。
<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>
ただし、 対話を終了すると、 現在表示させているドキュメントなどその対話に関連する状態はすべて失うことになります。 解決策のひとつとして、 リダイレクトを使わず直接レンダリングを使う方法です。
<page view-id="/editDocument.xhtml"> <navigation from-action="#{documentEditor.update}"> <rule if="#{documentEditor.errors.empty}"> <end-conversation/> <render 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>
Null outcome は JSF では特殊なケースです。 null coucome は「そのページを再表示する」という意味に解釈されます。 次のナビゲーション規則は 非 null の outcome すべてに合致しますが、 null outcome には合致しません。
<page view-id="/editDocument.xhtml"> <navigation from-action="#{documentEditor.update}"> <rule> <render view-id="/viewDocument.xhtml"/> </rule> </navigation> </page>
null outcome が発生したときにナビゲーションを行いたい場合には次の形式を使います。
<page view-id="/editDocument.xhtml"> <navigation from-action="#{documentEditor.update}"> <render view-id="/viewDocument.xhtml"/> </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 expression="#{helloListener.sayHelloBack}"/> <action expression="#{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.beginPageflow.<name> — ページフローの <name> が開始すると呼び出されます
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.postAuthenticate.<name> — ユーザーが認証されると呼び出されます
org.jboss.seam.preAuthenticate.<name> — ユーザー認証が試行される前に呼び出されます
org.jboss.seam.notLoggedIn — 認証されるユーザーがなく認証が必要とされている場合に呼び出されます
org.jboss.seam.rememberMe — Seam セキュリティがクッキーにユーザー名を検出すると発生します
Seam コンポーネントは、 他のコンポーネント駆動イベントを監視するのとまったく同じようにこれらのいずれのイベントも監視することができます。
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 { ... }
@Redirect は JSF ライフサイクルのレンダリングフェーズ中に発生した例外に対しては動作しないので注意してください。
この例外は 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 によってキャッチした例外インスタンスにアクセスすることができます。 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">#{handledException.message}</message> </redirect> </exception> </pages>