S2JSFでダーティリード対応

もう何度目だろう、S2JSFいじり。サンプル追ったり、前に作った軽いシステムの一部を実装してみたりとかしましたが、今回はサンプルの従業員管理画面を、サンプルをカンニングしつつ一から(S2JSFBlankからだけど)実装。

写経とはよくぞ云ったりですよ、本当。こう云うセンスって一番重要ですねぇ。

で、一昨日昨日であらかた動いたんですけど、versionNoでUpdateFailureRuntimeExceptionが出たときの対応がサンプルには無いんですねーこれが。ダーティリード対策は必須なので、やってみますか。

何処で例外を投げるか

サンプルだと、例えば検索条件が宜しく無くてヒット件数0の時、BadCriteriaRuntimeExceptionって自前の例外を投げてますが、これはActionクラスで行っています。

で、キャッチするのはActionThrowsInterceptorってアスペクトで各Actionクラスを横断してやっとる訳です。例外を受けて、そこからメッセージIDやらなにやら取って、JSFのFacesContext#addMessageにつめてます。

が、投げるのがActionでってのがどうにも落ち着かない。

完全webアプリならいいんですけど、同じ業務ロジックをRMIなりWebサービスなりで叩こうかって野望がある場合、処理が分散してしまいそうです。Actionクラスはあくまでも、Webプレゼンテーション層ですよね。JSFの画面IDが戻り値ですから。

特にダーティリード対策の場合、プレゼンテーション層で投げるのはどうにもこうにもアレです。それにActionクラスで投げたらActionThrowsInterceptorにつかまってしまい、無条件でJSF用のエラーメッセージを吐かれてしまいます。

JSF用メッセージは、JSFでしか使えません。多分。うーむ。

YAGNIですか。いいんです!ここは目をつむってGOだ。

んな訳で、取り敢えずBadCriteriaRuntimeExceptionを投げる所をActionクラスからLogicクラスへ移動してみました。

以下、削除コードは線を引いて、追加コードは太字です。ファウラー方式。

●EmployeeSearchActionImpl

    public String checkSearchCount() {
        if(employeeLogic.getSearchCount(employeeSearchDto) == 0){
            throw new BadCriteriaRuntimeException();
        }
        employeeLogic.getSearchCount(employeeSearchDto);
        
        return "employeeList";
    }
●EmployeeLogicImpl

    public int getSearchCount(EmployeeSearchDto dto) {
        return employeeDtoDao.getSearchCount(dto);
        int searchCount = employeeDtoDao.getSearchCount(dto);
        if(searchCount == 0){
            throw new BadCriteriaRuntimeException();
        }
        return searchCount;         
    }

結果は、ばっちり、例外はそのままActionクラスまで渡って、ActionThrowsInterceptorでキャッチされてJSFのエラーメッセージに表示されました。

これで方針は決まった。例外はLogicクラスで投げるどー。つづく。

S2JSFでダーティリード対応 しょにょ2(谷岡先生風)

ピンポェ〜ン。例外はLogicクラスで投げようキャンペーンが開始された訳ですが、そこへ踏み込む前に、まずはどんな動きするか確認したいので安易な方法でダーティリード対策してみました。

Logicクラスは処理モードを知るべきか

どんな方法かと云うと、ActionThrowsInterceptor#handleThrowableをオーバーロードしちゃってみたりなんかしたりしたのです。早くMr.Booボックス来ないかな。

で、ダーティリード時にseasarから投げられるUpdateFailureRuntimeExceptionをキャッチしてまうのです。

●ActionThrowsInterceptor

    public String handleThrowable(AppRuntimeException ex, MethodInvocation invocation) 
        throws Throwable {
		
        FacesContext context = FacesContext.getCurrentInstance();
        context.addMessage(null, MessageUtil.getErrorMessage(ex.getMessageId(), ex.getArgs()));
        return null;
    }
    
    public String handleThrowable(UpdateFailureRuntimeException ex, MethodInvocation invocation) 
        throws Throwable {
		
        FacesContext context = FacesContext.getCurrentInstance();
        context.addMessage(null, MessageUtil.getErrorMessage("example.jsf.UpdateFailure", null));
        return null;
    }

もちろんappMessages_ja.propertiesに上で指定してるメッセージを追加します。あくまでも動作確認のための応急処置ですよ。ちゃんとアプリケーション固有の例外クラス(サンプルではAppRuntimeException)を継承した専用の例外を作り、UpdateFailure・・・をそっちに振り替えて、元々のhandleThrowableで一括管理させるのが筋です。

ブラウザ2つ立ち上げて、同じデータに時間差攻撃で更新、どりゃっ!見事メッセージ表示されました。イェー。

と思ったが。

サンプルは編集画面 → 確認画面 と遷移して登録、となってますが、この確認画面にエラー表示されるんですけど、登録ボタンが出っ放しなのです。当然です。処理モードが更新モードのままですから。

で、エラーが出たまま登録ボタンを押すとなんと!エラー表示されてる画面の内容でちゃんと更新が出来ちゃうのです。バージョンNoが最新に置き替わってるのかな?うーん、従業員情報、再取得してないんだけど。謎だ。

てな訳で、登録エラーなのに登録ボタンが生きてるのは随分と不健全なUI設計です。

て事はですな、更新エラー時には処理モードを登録ボタンが非表示となる何かに変更しなけりゃいけないのです。つまり、UpdateFaulureRuntimeExceptionが発生した時、処理モードを保持しているProcessModeDtoを知っていなけりゃならんのです。

サンプルを見ると、LogicクラスではProcessModeDtoは一切見てません。見てるのはActionクラスのみで、つまりは処理モードはプレゼンテーション層内で閉じられた関心事だと云う位置付けの様です。そしてその方針に反論するのはなかなかに面倒そうなので頷いておくのが吉っぽいです(ああ情けない判断基準だ)。

それにLogicクラスはステートレスにしてSingletonが基本ですから、そこへsessionオブジェクトであるProcessModeDtoをインジェクションするのは、もう何がなんだかわけわかめになってしまいます(ちゃんとした判断も出来るのだ)。

ProcessModeDto#setProcessModeを書き換えるのはActionクラスで、て事になりましょう。そうすると結局Action側でもキャッチせにゃならん事に。

で、それは各画面共通の関心事ですから、ActionThowsInterceptorかそれに準じた専用のInterceptorでアスペクトでやりたいのが人情。そうなると、そのInterceptorはProcessModeDtoを知ってなきゃならん事に。

これも無理だよなー。InterceptorもLogicと同じくsessionオブジェクトを持てないでしょう。何か良い手はないか。Actionクラスのメソッド一個一個の中でやらなきゃいかんかなあ・・・。

むーん、根本的に間違ってるのかなあ。

S2JSFでダーティリード対応 しょにょさん

ピンゴツ〜ン。ダーティリードの場合、結局の所もう一度最新の状態で一覧画面を表示させて、そこから編集画面に入り直さないといけない訳です。

単に他の人が更新済みってだけなら一覧まで戻らなくても編集画面に戻ってオーケーですが、削除済みだったら何も表示出来ません。

なら強制的に画面遷移させちゃって、そこでエラー表示しちゃおうか計画。そうすりゃ処理モードなんて知ったこっちゃありません。

さっきの応急処置メソッドの戻り値を画面IDにしてやれ、えい。

●ActionThrowsInterceptor

    public String handleThrowable(UpdateFailureRuntimeException ex, MethodInvocation invocation) 
        throws Throwable {

        FacesContext context = FacesContext.getCurrentInstance();
        context.addMessage(null, MessageUtil.getErrorMessage("example.jsf.UpdateFailure", null));
        return null;
        return "employeeList";
    }

さて、実行。わーい遷移したー。が。

エラーが表示されません・・・・。なんとなく予想はしてたよ・・・あわよくばって思ってたんだけど。メッセージ表示って画面遷移と同時に出来ないんだっけなあ。

結構要件としてあるんだけど。「正常に処理が終了しました」なんてメッセージも出してくれ、とか。その場合は遷移とメッセージ表示、同時じゃん。出来そうだけどな。

JSFの本、置いてきちゃった。むーん。

S2JSFでダーティリード対応 補遺

UpdateFailureRuntimeExceptionってのは、S2Dao-1.0.26までみたいですね。1.0.27からはNotSingleRowUpdatedRuntimeExceptionってのになってる。確かにUpdateFailureだと排他以外でコケたかと思うべな。最新1.0.28でもこのまんまなのかな。

それにしても、どうしたもんか。

上でInterceptorの戻り値の画面ID、ハードコーディングしてるのは動作確認のためのやっつけですよ勿論。これじゃ画面依存なのでアスペクト出来ないです。

S2JSFでダーティリード対応 よん

ピンポェ〜ン。もう画面の事は後回しにしました。頭痛い。まずは例外はLogicで投げようキャンペーンの実施です。まずはActionThrowsInterceptorに施した応急処置のゴミを取って、GOだ。

ダーティリード例外を作ろう

まんず、アプリケーション独自の例外、AppRuntimeExceptionを継承してダーティリード用例外を作りましたよ。

public class DirtyReadRuntimeException extends AppRuntimeException {

    public DirtyReadRuntimeException() {
        super("example.jsf.DirtyRead");
   }
   
}

AppRuntimeExceptionを継承する事で、ActionThrowsInterceptorに拾って貰えるようになる訳です。なんでも拾うようにしちゃうと、致命的なエラーなどで別途エラーページに遷移させたい場合とかに困っちゃいますので。

UpdateFailureRuntimeExceptionを変換する

S2Daoが投げるUpdateFailure〜を、DirtyReadへ変換します。

この変換処理、更新する所では必ず必要となりますんで、いの一番にアスペクト化しましょう。Logicクラス専用のThrowsInterceptorを作ります。

public class LogicThrowsInterceptor extends ThrowsInterceptor {
	
    public void handleThrowable(UpdateFailureRuntimeException ex, 
            MethodInvocation invocation)throws Throwable{
        
        throw new DirtyReadRuntimeException();
    }

}

他にLogicクラス内で変換したい例外を追加する場合は、handleThrowableをオーバーロードしていけばいいのね。簡単ですね。

diconファイルへ登録しましょう

さてこれをdiconファイルへ登録、ついでにInterceptorChainにもつっこみましょう。ああ強調出来ない。ので追加箇所はXMLコメントで書いておきます。と思ったら、はてな側でもコメント扱いしやがった。くそー。ベタで書きます!

<components>
    <include path="aop.dicon"/>
    <include path="j2ee.dicon"/>
    
    <component name="actionThrowsInterceptor" 
        class="example.jsf.interceptor.ActionThrowsInterceptor"/>
        
    ■Logic用のThrowsInterceptorをコンテナに登録■
    <component name="logicThrowsInterceptor" 
        class="example.jsf.interceptor.LogicThrowsInterceptor"/>
    
    <component name="actionInterceptorChain" 
        class="org.seasar.framework.aop.interceptors.InterceptorChain">
        <initMethod name="add"><arg>aop.traceInterceptor</arg></initMethod>
        <initMethod name="add"><arg>actionThrowsInterceptor</arg></initMethod>
    </component>

    <component name="logicInterceptorChain" 
        class="org.seasar.framework.aop.interceptors.InterceptorChain">
        <initMethod name="add"><arg>aop.traceThrowsInterceptor</arg></initMethod>
        <initMethod name="add"><arg>aop.traceInterceptor</arg></initMethod>
        <initMethod name="add"><arg>j2ee.requiredTx</arg></initMethod>
        ■ そんでもって Chain に追加しときます ■
        <initMethod name="add"><arg>logicThrowsInterceptor</arg></initMethod>
    </component>
</components>

これでいいのか。あっけない。でっきるっかな♪

さてさてほほー実行っ!・・・・。


出来ました。


ちゃんとActionThrowsInterceptorで、AppRuntimeExceptionとしてつかまえてメッセージも出してくれてます。相変わらず登録ボタンは出っ放しだけどな。

アスペクトにしたので、logicInterceptorChainをつっこまれたLogicクラスでは全て、ダーティリード対応出来たぞ。すごいっ、5倍のエネルギーゲインがある!

エネルギーゲインってなんだろう・・・。
http://www.geocities.co.jp/Playtown-Denei/2111/hallo.html
ここによると、電力の事か。