【もうハマらない!】JSFをきちんと理解しよう。


対象読者

  • JSF及びその周辺領域をある程度理解している(ライフサイクルとかFaceletsとか)
  • JSFを使ったアプリケーションを作ったことがある or これから作る
  • JSFを使ったアプリケーションで、性能問題に直面している

本記事の概要

これまで約3年間、JSFやFaceletsを使ったWebアプリケーションを構築してきた。
その中でハマった事や参考にした記事などを紹介する。

はじめに

JSF、Facelets、PrimeFaces(その他のJSFライブラリ)などを利用することで、
大規模なWebアプリケーションを短期間で構築することができるようになっている。

一方で、概念や内部構造を理解しないまま設計・実装を進めると、思わぬ不具合や性能問題を引き起こす可能性もある。

そこで、これまでの経験から、抑えておきたい概念やハマりどころになりそうなところ、性能アップTipsをまとめる。

JSFのライフサイクルを理解する

JSFを使ってアプリケーションを作る上で、ライフサイクルをきちんと理解することが非常に重要。
この辺りは既にまとまっている記事があるので、引用させていただく。

http://yoshio3.com/2012/08/24/detail-of-jsf20/

http://thinkit.co.jp/free/article/0607/8/3/

各フェーズの概要を記載しておきます。

  ◆RESTORE_VIEWフェーズ

   View のコンポーネント(UIViewRoot) ツリーを復元(存在しなければ生成)する。

    →開発者はあまり意識することはないが、各コンポーネントが内部的に Java のクラスに変換されツリー構造で扱われている事を理解しておく。

  ◆APPLY_REQUEST_VALUESフェーズ

   クライアントから送信されたリクエスト値を、UI コンポーネントのツリーに含まれる全コンポーネントに対して適用する。

   →内部的には、UIVewRoot#processDecodes()を実行し、ツリー配下に存在する全ての子コンポーネントに対して再帰的にprocessDecodes()を実行する。

  ◆PROCESS_VALIDATIONSフェーズ

   コンポーネントに対するヴァリデーションが実施される。

   →実際には、下記のような処理が行われている。

   ・ Converter#getAsObjectメソッドがコールされる。
   ・ Validator#validateメソッドがコールされる。
   ・ コンポーネントに設定された「valueChangeListener」が実行される。

   このフェーズでチェックNGとなった場合、RENDER_RESPONSEフェーズに遷移する。

  ◆UPDATE_MODEL_VALUESフェーズ

   コンポーネントにバインドされているBeanに値をセットする。

   →内部的には、UIVewRoot#processUpdates() を実行し、再帰的に子のコンポーネントで同様のprocessUpdates()を実行する。

   ◆INVOKE_APPLICATIONフェーズ

   アプリケーション・ロジックの呼び出しを実施する。(開発者がメインでコーディングする部分)

   →内部的には、UIVewRoot#processApplication()を実行し、その後、ActionSourceを実装する全UIComponentで、UIComponent#broadcast()を実行する。
   ※INVOKE_APPLICATIONにおけるメソッド呼び出し順については後述。

   ◆RENDER_RESPONSEフェーズ

   クライアントに返却するレスポンスを生成する。

   →Converter#getAsStringメソッドがコールされる。コンポーネントの状態を保存し、クライアントに返却するHTMLを生成する。

 

まとめ

JSFのライフサイクルをきちんと抑えることは基本中の基本。

例えば、性能テストをしていると、どうしても性能が出ないことがある。
そういったケースでは、まず"上記のどのフェーズで時間がかかっているか計測すること"を再優先に実施することをおすすめする。

タグ属性を正確に理解する

JSFはXHTMLファイルでビューを定義していくが、利用するタグの属性は多岐にわたる。
タグ属性の用途、動きを理解しないまま画面を作ることは非常に危険である。

そこで今回は、間違えやすい属性をピックアップし、正しい使い方を示す。

◆actionとactionListenerをきちんと使い分ける


 action:

// public修飾子
// 戻り値はString
// 引数なし
public String doAction(){
    return ${nextPage}; 
    // return null; →現在の画面に留まる。ViewScopeの変数はそのままキープ。
    // return ""; return ${currentPageId}; →現在の画面に留まる。ViewScopeの変数は破棄される。
} 

 actionListener:

import javax.faces.event.ActionEvent;

// public修飾子
// 戻り値はvoid
// 引数はActionEventパラメータ
public void doActionListener(ActionEvent actionEvent){
    // doSomething
}
  • 呼び出し順番に注意 下記のように記述した場合、
<h:commandButton value="ボタン"
        action="#{myBean.doAction}" 
        actionListener="#{myBean.doActListener}" >
    <f:ajax execute="@this" render=":form:after"/>
    <f:setPropertyActionListener
        value="#{myBean.hogeVal}" target="#{myBean.hoge}"/>
</h:commandButton>

コールされる順番は下記。
1. actionListener
2. setPropertyActionListener
3. action

setPropertyActionListenerって何だ?
value属性の値を、target属性にセットするリスナー。
※親タグのactionListenerが実行された後に、このリスナーが実行されることに注意する必要がある。

https://docs.oracle.com/javaee/6/javaserverfaces/2.0/docs/pdldocs/facelets/f/setPropertyActionListener.html

◆process="@form" update="@form" は悪!!


本題に入る前に、JSFのリッチコンポーネントを提供するPrimeFacesについて簡単に触れておく。
http://www.primefaces.org/showcase/index.xhtml

JSF2.0からはAjax対応が可能となっており、PrimeFacesではより簡単にAjaxを利用したアプリを作ることができるようになっている。
http://yoshio3.com/2011/01/18/jsf-20-ajax-support/

しかし、Ajax通信を利用して部分Submit&部分再描画を行う際には細心の注意が必要
下記のようなコードを発見したことはないだろうか。

<p:commandButton value="ボタン"
        actionListener="#{myBean.doActListener}"
        update="@form"
        process="@form"
        onstart="・・・"
        ・・・>
</p:commandButton>

JSFのライフサイクルをきちんと理解していないと、このようなコードが生まれてしまう。。

  • process属性:Ajax通信時、クライアント→サーバにSubmitするコンポーネントID(複数指定可)を指定する。  ここで無暗に広範囲(上記例ではformタグ内の全要素)にSubmit対象を指定すると、下記のような弊害が生まれる可能性がある。  
    • HTTPリクエストのサイズが肥大化することによる性能劣化。
    • 指定された範囲に含まれる全コンポーネントに対して、ヴァリデーションやコンバージョン、リスナーなどが実行されるため、意図しない動作をすることがある。(全く関係のないコンポーネントでバリデーションエラーが起きてしまうなど)
    • 多数のコンポーネントに対して、JSFライフサイクルに則った処理が実施されることによる性能劣化。 ・・・

 

  • update属性:Ajax通信後、再描画するコンポーネントID(複数指定可)を指定する。
     ここで無暗に広範囲(上記例ではformタグ内の全要素)にSubmit対象を指定すると、下記のような弊害が生まれる可能性がある。

    • RENDER_RESPONSEでの性能劣化。(広範囲になればなるほど、レンダリングの時間がかかるのは当然)
    • サーバ→クライアントに返却するレスポンスHTMLサイズ肥大化による性能劣化。
    • ブラウザのレンダリング負荷による性能劣化。
    • なぜか画面上のある項目がクリアされる。(リクエストスコープの項目が再描画によりクリアされるケース) ・・・

このように、JSFのライフサイクル・JSFタグの属性を理解せずに、「とりあえず動くコード」を記述することで、
意図しない動作・性能劣化を誘発してしまうことがあるということがわかって頂けるかと思う。。

性能アップTips

◆immediate属性を活用して、JSFライフサイクルをハックする!


「immediate」属性を利用することで、JSFを使ったアプリケーションの性能アップを図ることができる。
下記のようなケースで有効です。
※ただし、属性の意味をきちんと理解してから利用することをおすすめする。

  • 入力項目のヴァリデーションチェックをする前に、ナビゲーション処理を実施したい。
  • 入力項目に優先度をつけて、優先度高の項目は、他の入力項目より先にバリデーションチェックを行いたい。

(例)PROCESS_VALIDATIONSフェーズが実施される前に、ナビゲーションを行う。

<p:commandButton value="ボタン"
        action="#{myBean.doNextPage}">
</p:commandButton>

上記のナビゲーション処理は、Ajaxではないため全UIコンポーネントがサーバにSubmitされてしまう。
画面構成に依ってはPROCESS_VALIDATIONSフェーズに時間を要する可能性がある。
また、バリデーションエラーとなった場合には、ナビゲーションされずにRENDER_RESPONSEフェーズに進んでしまう。

そこで、immediate属性の出番です。

<p:commandButton value="ボタン"
        action="#{myBean.doNextPage}"
        immediate="true">
</p:commandButton>

immediate="true"を使用すると、コンポーネントのActionListenerまたはアクションメソッドがAPPLY_REQUEST_VALUESフェーズの最後に実行される。

上記の例では、PROCESS_VALIDATIONS・UPDATE_MODEL_VALUESの前にmyBean#doNextPage()がコールされることになる。

入力内容の妥当性に関わらずナビゲーションしたいようなケースでは有効な属性になる。

参考ページ:

http://oss.infoscience.co.jp/myfaces/cwiki.apache.org/confluence/display/MYFACES/How+the+immediate+attribute+works.html

◆ViewStateの肥大化は性能問題を引き起こす


"JSFのおけるViewStateとは" については下記を引用する。

http://www.synacktiv.fr/ressources/JSF_ViewState_InYourFace.pdf

過去に何度か、ViewStateの肥大化による性能問題に直面したことがある。
「30秒経っても画面が再描画されない(レスポンスが返ってこない)・・・」
→クライアントサイドにViewStateを抱える設定で、数百KB~数MBのViewStateを保持している画面を発見。

クライアントサイドでは、下記のようにinputタグのvalue属性にViewStateの値を保持する。

<input name="javax.faces.ViewState" id="javax.faces.ViewState" type="hidden" autocomplete="off" value="H4sIAAAAAAAAAO19CXAb15lm674P6/AxHjuyrUQZj00SFyXGnngkygcjStGYshzLlU2aQJOEiCuNhkQ6iZcAr・・・"/>

Ajax通信などで部分Submit&部分再描画をしていても、このViewStateは必ずサーバからクライアントに送られてくるため、ViewStateのサイズが大きい=レスポンスサイズが大きい ことになる。

このViewStateサイズを大幅に削減することで、画面にレスポンスが返ってこない問題を解決することに成功!

【原因】
画面に表示しない値にもかかわらず、ViewScopeで大量のデータを保持していたこと。

【解決策】

  • 画面に表示しない値(裏でデータだけ保持しているような値)はViewScopeでは保持しない
  • 必要なデータは必要なタイミングで取得するようにロジックを変更する

参考記事

http://www.coreservlets.com/JSF-Tutorial/jsf2/
http://stackoverflow.com/questions/25339056/understanding-process-and-update-attributes-of-primefaces

まとめ

  • JSFのライフサイクルをきちんと理解する
  • Ajax利用時、process/update属性の属性値には注意する
  • ViewStateの肥大化は性能問題を引き起こす
  • immediate属性を活用することで性能アップ ※ただし、属性の効果をしっかりと理解した上で利用すること