システム開発で思うところ

Javaで主にシステム開発をしながら思うところをツラツラを綴る。主に自分向けのメモ。EE関連の情報が少なく自分自身がそういう情報があったら良いなぁということで他の人の参考になれば幸い

【考察】FormとEntityの類似点

Formに関して、アレコレ考えていると、Entityとの類似点が多いように思えてきた。

つらつらと思いついたことをメモ。

Identification

画面のどこの何、という意味で「ここのこのフィールド」という一意な場所が必要。

姓と名が同じ値だった場合(そんな名前の人はいないとは思うけど)、入れ替えても良いかというと それはダメ、みたいな話。

Mutable

identificationとほぼ同じこと。

画面項目として重要なのは同値性ではなくて一意性。

値については書き換えが可能。

実装面においても、JSFの場合、Formに相当するフィールドはmutable(プロパティにset)。

Persistent

厳密な意味でのPersistent(永続化)とは違うけど、クライアント(ブラウザ)上のメモリに保持されているという意味では 永続化しているともいえる。

DBはストレージに存在しつづける限りは生存するのと同じように、Formもブラウザを閉じるまでは生存しているという見方もできなくはない。

Aggregate

Formの AggregateのRoot Entityに相当するものが、画面。

考察(1)

考えるきっかけは、Formにおけるエラーツールチップの実装を考えている中で、対象セル(フォーム)の位置を特定するための情報を持っておかないといけないなぁ、と考えたところから。

フォームにおけるidentificationをどうしたものかな、と考えていて なんとなく 類似点を感じた。

ただ、全く同じかというと それは違って IDの採番はFormとEntityでは異なる。そして、それが現状悩み中のところ。

EntityはTable単位でIDが一意であれば問題ないけど、Formの場合は集約(画面)単位で一意じゃないといけない。 *1

なので、全く同じというわけでもない。

検証は、クライアント側だけではなく、ビジネスロジックによる検証(既存データの存在確認など)もあって、その結果をイイ感じに集約して 最終的にクライアントの入力フォームへ反映させる という仕組みが見いだせず。

クライアントだけならUIComponent使ったら 出来る方法が無いわけではないみたいだけれど、そのためにはxhtmlに記述がいるみたいで(ComponentIdとValidationの対象を明示するため)、個人的には そういうxhtmlに何かを記述するという やり方も出来れば避けたい。

単項目単位でDBへアクセスして検証する、というのもありといえば ありかもしれないけれど、やはり N+1問題が絶対に発生する方式というのは 個人的に許容したくない。

以前、それっぽいものを自作はしたけれど、結局 Formの情報をビジネスロジックまで蜜結合させた実現方式であったため(フォームのプロパティ名をEntity(またはDxo)と一致させる)、今から振り返ると かなり良くない。

単純なCRUDだったら、それでも良いかもしれないけれど、単純なCRUDにのみ限定されない仕組みを考えたい。

また、単項目の関連制御だけではなくて、一覧のような繰り返しも 適用範囲にしたい*2

現状、落としどころが見いだせておらず悩みは尽きず。

なーんか、パチッとくるアイディアが閃かないものか。。

考察(2)

クライアントサイドのモデルとは何か 後編 ~ 単方向データフローと参照透過性 - mizchi's blog

正直なところ、ちゃんと理解はできていないけど 何かヒントになりそうな気がする。

部分更新をするんじゃなくて、FormのRootへのCRUDとなるactionを通じてのみ 詳細への更新をすれば 良いのかも?

つまり常にRootから作り直すイメージ。

これくらい割り切ったら、最悪 FormIDも毎回 採番しなおしても問題なさそうかも。

変に「前回のIDとの関連付け」を やろうとしているから悩みが深くなっていたような気がする。

ただ、そうするとクライアント側でJacaScriptで表示順を変えたり、動的に要素を追加したりしたら どうなるかな?

JSFだと、そのあたりのズレが地味に困ったりするんだよなぁ。

ただそれもSubmitでAggregate全体を更新すれば大丈夫かな?

手を動かしつつ、更に色々考えてみよう。

*1:Entityもシステム全体でIDをUUIDにするとか、すれば それは一意になるかもしれないけど、普通 そういうことはしないように思うので、できる と やる は別で考えるべき

*2:これも過去の実装では、なんとか実現していたけれど、結局 方式は上述の「プロパティ名を一致させる」という方式なのでダメ

環境変数で設定したい情報のメモ

環境変数で設定したい情報

開発とプロダクトで異なる値になるものは環境変数(もしくは それに準ずる仕組み)で設定したいところです。

まずは、何を対象にするのか整理。

JSF

  • javax.faces.PROJECT_STAGE

  • javax.faces.FACELETS_SKIP_COMMENTS

  • javax.faces.FACELETS_REFRESH_PERIOD

パラメータ

Standard context parameters

設定のやり方

JSF 2.0のPROJECT_STAGEをアプリケーションの外部から指定する - penultimate diary

公式は、このやり方みたいだけど 個人的には もっと汎用的というか環境変数から値を取得するような仕組みが良いかぁ。

この手のものは、あんまり独自実装をしない方が良さそうに思ってはいるけれど どうせ環境変数から値を取得する仕組みについては 何かしら準備が*1必要だと思うので、その時に考えよう。 *2

DB

接続先毎に指定

  • 接続先URL

  • ログイン名

  • パスワード

  • SQLログの出力の有無

ログ

  • 出力ログレベル

  • 出力先

*1:ありものを使うにしても

*2:良く知らないけど、Config 1.0 とか そういうものを使うとか

JSFのIDあれこれ

JSFで生成されるidあれこれ - Challenge Java EE !

に加えて pass throughを実際に使ってみた実装サンプル

目的は JSFが生成するidnameについて確認する事。

定義

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:jsf="http://xmlns.jcp.org/jsf"
      xmlns:h="http://xmlns.jcp.org/jsf/html"
      xmlns:pt="http://xmlns.jcp.org/jsf/passthrough">
    <head>
        <title>JSF ID</title>
    </head>
    <body>
        <h1>Id</h1>

        <h:form>
            <div jsf:id="jsfId" pt:id="ptId10" id="divId">
                jsfIdあり、passthroughIdあり、Idあり=passthroughIdが適用
            </div>
        </h:form>

        <h:form>
            <div jsf:id="jsfId" id="divId">
                jsfIdあり、passthroughIdなし、Idあり=divIdが適用
            </div>
        </h:form>

        <h:form>
            <div jsf:id="jsfId">
                jsfIdあり、passthroughIdなし、Idなし=jsfIdが適用(ルートは自動生成)
            </div>
        </h:form>



        <p>input type="text"</p>

        <h:form>
            <input type="text" jsf:id="jsfId" pt:id="ptId10" id="divId" value="jsfIdあり、passthroughIdあり、Idあり=passthroughIdが適用"  style="width:400px" />
        </h:form>

        <h:form>
            <input type="text" jsf:id="jsfId" id="divId" value="jsfIdあり、passthroughIdなし、Idあり=divIdが適用"  style="width:400px" />
        </h:form>

        <h:form>
            <input type="text" jsf:id="jsfId" value="jsfIdあり、passthroughIdなし、Idなし=jsfIdが適用(ルートは自動生成)"  style="width:500px" />
        </h:form>



        <p>h:inputText</p>

        <h:form>
            <h:inputText id="jsfId" pt:id="ptId10" value="jsfIdあり、passthroughIdあり=passthroughIdが適用" style="width:400px"/>
        </h:form>

        <h:form>
            <h:inputText id="jsfId"  value="jsfIdあり、passthroughIdなし=jsfIdが適用(ルートは自動生成)"  style="width:400px" />
        </h:form>



    </body>
</html>

生成後のxhtmlのコード

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
    <head>
        <title>JSF ID</title>
    </head>
    <body>
        <h1>Id</h1>
        <form id="j_idt2" name="j_idt2" method="post" action="/jsf-id/index.xhtml" enctype="application/x-www-form-urlencoded">
            <input type="hidden" name="j_idt2" value="j_idt2" />
            <div id="ptId10">
                jsfIdあり、passthroughIdあり、Idあり=passthroughIdが適用
            </div>
            <input type="hidden" name="javax.faces.ViewState" id="j_id1-javax.faces.ViewState-0" value="-6059247821844920063:3663652348388936230" autocomplete="off" />
        </form>
        <form id="j_idt4" name="j_idt4" method="post" action="/jsf-id/index.xhtml" enctype="application/x-www-form-urlencoded">
            <input type="hidden" name="j_idt4" value="j_idt4" />
            <div id="divId">
                jsfIdあり、passthroughIdなし、Idあり=divIdが適用
            </div>
            <input type="hidden" name="javax.faces.ViewState" id="j_id1-javax.faces.ViewState-1" value="-6059247821844920063:3663652348388936230" autocomplete="off" />
        </form>
        <form id="j_idt6" name="j_idt6" method="post" action="/jsf-id/index.xhtml" enctype="application/x-www-form-urlencoded">
            <input type="hidden" name="j_idt6" value="j_idt6" />
            <div id="j_idt6-jsfId">
                jsfIdあり、passthroughIdなし、Idなし=jsfIdが適用(ルートは自動生成)
            </div>
            <input type="hidden" name="javax.faces.ViewState" id="j_id1-javax.faces.ViewState-2" value="-6059247821844920063:3663652348388936230" autocomplete="off" />
        </form>



        <p>input type="text"</p>
        <form id="j_idt9" name="j_idt9" method="post" action="/jsf-id/index.xhtml" enctype="application/x-www-form-urlencoded">
            <input type="hidden" name="j_idt9" value="j_idt9" />
            <input name="j_idt9-jsfId" style="width:400px" id="ptId10" type="text" value="jsfIdあり、passthroughIdあり、Idあり=passthroughIdが適用" />
            <input type="hidden" name="javax.faces.ViewState" id="j_id1-javax.faces.ViewState-3" value="-6059247821844920063:3663652348388936230" autocomplete="off" />
        </form>
        <form id="j_idt10" name="j_idt10" method="post" action="/jsf-id/index.xhtml" enctype="application/x-www-form-urlencoded">
            <input type="hidden" name="j_idt10" value="j_idt10" />
            <input name="j_idt10-jsfId" style="width:400px" id="divId" type="text" value="jsfIdあり、passthroughIdなし、Idあり=divIdが適用" />
            <input type="hidden" name="javax.faces.ViewState" id="j_id1-javax.faces.ViewState-4" value="-6059247821844920063:3663652348388936230" autocomplete="off" />
        </form>
        <form id="j_idt11" name="j_idt11" method="post" action="/jsf-id/index.xhtml" enctype="application/x-www-form-urlencoded">
            <input type="hidden" name="j_idt11" value="j_idt11" />
            <input id="j_idt11-jsfId" name="j_idt11-jsfId" style="width:500px" type="text" value="jsfIdあり、passthroughIdなし、Idなし=jsfIdが適用(ルートは自動生成)" />
            <input type="hidden" name="javax.faces.ViewState" id="j_id1-javax.faces.ViewState-5" value="-6059247821844920063:3663652348388936230" autocomplete="off" />
        </form>



        <p>h:inputText</p>
        <form id="j_idt13" name="j_idt13" method="post" action="/jsf-id/index.xhtml" enctype="application/x-www-form-urlencoded">
            <input type="hidden" name="j_idt13" value="j_idt13" />
            <input type="text" name="j_idt13-jsfId" value="jsfIdあり、passthroughIdあり=passthroughIdが適用" style="width:400px" id="ptId10" />
            <input type="hidden" name="javax.faces.ViewState" id="j_id1-javax.faces.ViewState-6" value="-6059247821844920063:3663652348388936230" autocomplete="off" />
        </form>
        <form id="j_idt14" name="j_idt14" method="post" action="/jsf-id/index.xhtml" enctype="application/x-www-form-urlencoded">
            <input type="hidden" name="j_idt14" value="j_idt14" />
            <input id="j_idt14-jsfId" type="text" name="j_idt14-jsfId" value="jsfIdあり、passthroughIdなし=jsfIdが適用(ルートは自動生成)" style="width:400px" />
            <input type="hidden" name="javax.faces.ViewState" id="j_id1-javax.faces.ViewState-7" value="-6059247821844920063:3663652348388936230" autocomplete="off" />
        </form>



    </body>
</html>

プレゼンテーション層で例外を扱う(私の例)

への私なりの回答です。

Javaの王道ではないと思いますし、過去のオレオレライブラリでは、こういう風にしたよってだけの話です。*1

正直、現時点では「うーん、発想は悪くなさそうだけど、見直ししたいな」と思っています。

とりあえず

「HTML返す場合で一律のエラーページに飛ばさずに画面の表示をエラー文言にしたい、とかの場合はtry-catchするしかないのか?」

への、私なりの回答です。

ちなみに、元々はトランザクションの話だったと思いますが、最終的なところはプレゼンテーション層との連携、ということとして回答します。

細かい前提は抜きにして、Interceptor(イメージ)を示します。

実際のコードは、もっと変なことを色々しているので、過去のコードを横目に それっぽい感じにしたものです。

私はJSFを使っているので、SpringMVCなどフレームワークだと の処理画面の情報の取得のやり方について、参考にならないかもしれませんが。。

public class ActionInterceptor {

    @Inject
    MessageItem messageItem;

    @AroundInvoke
    public Object invoke(InvocationContext ic) throws Exception {

        // 処理実行開始時点の画面IDを取得する。
        Object result = FacesContext.getCurrentInstance().getViewRoot().getViewId();

        try {
            result = ic.proceed();
            return result;
        } catch (CustomException ex) {
            /*
            実行時例外に独自の情報(メッセージIDとか、その他の付加情報)を持たせておいて、
            それを元に画面のメッセージを設定する
             */
            this.messageItem.setMessage(ex);

            /*
            例外があった場合は、実行時の画面IDを返却する。
            (もし、CustomException で保持している内容次第で、遷移先を変えたかったらここで何か実装する。)
             */
            return result;
        }
    }
}

なお、ExceptionHandler でシステム全体で共通してやっても良いように思います。

この実装を やったときは Interceptorで実装する方式しか イメージできなかったけれど、今なら、、どうかなぁ ちょっと分かりません。

ExceptionHandlerにするかもしれないです。

遷移前の情報など、その他の参照したい情報や適用されるタイミング次第だと思います。

ちなみに、当初は Controller で try-catch してメッセージ用のインスタンスに値を設定するUtilをCallする方式にしていました。

そのときは「レイヤー間を跨る例外は、検査例外にして 明示化するべきではないか?」と思っていたからです。

ですが、実際にやってみて 独自例外はメッセージIDが違うだけで型は1つ、いちいち try-catch を書くのが面倒くさくなって、上述の実行時例外での方式に編集しました。

おかげ様で、Controllerのメソッドに例外ハンドリングの記述が無くなった分だけ、スッキリしました。

*1:逃げ腰

プログラムから文字列指定を無くす

メッセージをEnumにすることでタイプセーフにする仕組みを以前作りました。

vermeer.hatenablog.jp

これと同じように、色々な要素から「文字列で直接指定している」というところを、ちょっとずつ無くしていきたいと思っていて そのためのネタを整理です。

できるか、できないか、どこまで必要そうか、については 実際に取り組む際に改めて検討するので、とにかく「文字列」を直接使っているものを 洗い出してみました。

入力補完する、検証だけして直すのは自分でやる、など具体的なやり方は全く決めていないです。

気が付いたときに、追加したり削除したりしようかぁ、という感じです。

xhtmlのID

xhtml などViewで使用するID

xhtmlのラベル

表示項目。
国際化対応を鑑みてプロパティ化はしたい。

xhtmlのパス

遷移先のパスとなるxhtml

Validation用のメッセージ

EL式をベースにしたBeanValidation用のもの。

JPA Entityのプロパティ

setParameterのKey値

JPAのNamedのQuery名

NamedのQuery名。

NetBeansだったら 自動補完するから要らないかもしれないけど、明確にコンパイルエラーにするのなら準備した方がいいかな。

JPAのプロパティ名

Queryに使用するプロパティ名

SQLファイルパス

NativeQueryのSQLとして使用しているSQLファイルのパス。 (これは、独自に参照用のライブラリを作成したもののファイルパス)

環境変数

これは別扱いで整理

JSFで2重Submit対策(続)

前回やっていないこととした「入力画面まで戻ったFormの更新抑止」について対応をしてみました。

vermeer.hatenablog.jp

やっていないことで2つ目に挙げた案である

immediate を使うやり方です。

出来れば xhtmlに手を加えないやり方の方が良いかなぁ、と思っていたのですが 実際に記述してみて

  • まぁ、これくらいなら そこまで手が込んでいるわけではない

  • immediateの挙動は 基本的なJSFの仕様の範疇なので何が起こるのか想像しやすい

ということで、このやり方で良いだろう、と落ち着きました。

使用方法

情報を反映したくないアクションのトリガーに

immediate="#{doubleSubmitLifecycle.submitted}"

を記述します。

<p><input type="submit" value="確認"  jsf:action="#{doubleSubmitAction.fwConfirm()}" jsf:immediate="#{doubleSubmitLifecycle.submitted}"/></p>

実装

前回の時点で、すでに当該メソッドがありました。

public boolean isSubmitted() {
    return this.doubleSubmitState.isSubmitted();
}

実行結果

f:id:vermeer-1977:20180513101845p:plain

f:id:vermeer-1977:20180513101924p:plain

f:id:vermeer-1977:20180513102005p:plain

「戻る」→「戻る」で、入力画面まで戻ってユーザー名を更新します。

f:id:vermeer-1977:20180513102136p:plain

f:id:vermeer-1977:20180513102206p:plain

確認画面で、ユーザー名が更新されていないことが確認できました。

Code

Bitbucket

さいごに

これくらいなら、前回の記事にまとめても良かったかも。。

JSFで2重Submit対策

一般的な2重Submitは、hiddenで保持したトークン と サーバのセッションで保持しているトークンを比較する、というやり方だと思います。

さいきょうの二重サブミット対策 - Qiita

これは、これで正しいと思いますし、多くの人が知っているであろう やり方なので検索性も良いと思います。

ですが、Jakarta EE には ConversationScoped があります。

画面単位(タブ単位)でイイ感じに情報を管理してくれるスコープがありますので、それを前提としたやり方で試してみたいと思います。

「そもそもConversation自体が面倒なんですが」ということについては、以前の記事の「フォルダ単位で会話スコープを制御する」を使用する前提です。

vermeer.hatenablog.jp

やりたいこと

  • 2重Submit不可のトリガーとなるアクションを操作したら、以降 同アクションを操作したらエラーにする
  • エラー判定後の操作として、メッセージ出力 または 画面遷移先指定(もしくは 2つの組み合わせ)を指定できるようにする
  • デフォルトは「何もしない」
  • アノテーションによって宣言的にする

仕様の補足

エラー判定後の操作

エラー画面への遷移だけでなく、メッセージ出力とした理由は、確認画面をそのまま流用できるようにして実装負担を減らせる仕組みを設けた方が良いだろうと思ったためです。

もちろん、エラーではなく「登録は完了しています」というようなメッセージにしてユーザー自身による2重登録防止にする、というやり方もあるでしょう。

画面遷移は、単純にエラー画面への遷移というだけでなくて f:viewActionで初期表示イベントによる操作が出来るので複雑なことも可能です。

具体的には、f:viewActionでアクションと紐づけをして、ケース分けから自画面とそれ以外の画面への振り分けや、ログ出力やエラー通知などをするイメージです。

アノテーションで宣言的

上述のように、複雑なロジックが必要なことは「遷移先画面の指定」で実現すると割り切って、あくまでアプリケーションの実装としてはアノテーションによる宣言のみです。

当初、細かい操作ができるようにした方が良いかなぁ、と考えて HandlerClassを設けて、それをInjetする方法も考えたのですが 遷移先画面を任意で指定できるようにする 上述のやり方であれば、結果として 好きなように実装できると考えて このやり方で落ち着きました。

また2重Submit対策はドメイン知識とは別の話なので、そういうものは極力「ロジック」に紛れ込まないようにする、つまり宣言的にしておく方が最適だと考えました。

デフォルトの挙動

2回目の操作時に「何もしない」をデフォルトとした理由は、プロダクトによって指針が大きく異なると考えたためです。

つまり、お試し実装の範疇では、固定的な実装はしない、としました。

もし、エラーメッセージを出力することをデフォルトにしたければ、@ExcuteOncemessageのデフォルト値でメッセージIDを指定するなり、ExecuteOnceInterceptorのメッセージ編集で定数値をしていするなりすれば良いでしょう。

エラー画面IDの指定も同様の対応で対処可能です。

方式の補足

状態保持と更新

ExecuteOnceLifecycleでは、内部で状態を保持しています。
その場合のマルチスレッドによる本方式の安全性ですが これは本クラスをConversationScopedとすることで考慮しています。

後発のリクエスト時にjavax.enterprise.context.BusyConversationExceptionがスローされる仕様になっています。(略)スレッドセーフの考慮は不要です。
パーフェクト Java EE P69

とあるように、EEの仕組みとして状態を安全に保持できる仕組みが設けられています。*1

したがって 過度ではないSubmit と ブラウザによる 戻る で発生しうる2重Submitへの対応とは別に、過度なSubmit(極端な連打)による異常は、BusyConversationExceptionとスローするため 不正な登録の実行は発生しません。 *2

会話スコープを任意のタイミングで終了する

上述の過去の記事の実装では、xhtmlのフォルダが変わった時「だけ」がConversationScopeの終了条件でした。

今回のように、同一フローを繰り返したい場合に困ってしまうということで @EndConversationというアノテーションで、任意のタイミングで終了できるようにしました。

これで、入力→確認→完了 をサイクリックに行えるようになります。

ExceptionHandlerWrapper を使わないでも出来た

当初、メソッドに複数のInterceptorが適用される場合、ExecuteOnceInterceptorで対象メソッドを実行しなくても、別のInterceptorによって実行されるかもしれないと考えて、実行時例外によるハンドリングを検討しました。

結論としては、それは杞憂でした。InvocationContext#proceed()によって、Interceptorがchainされるので、優先度上位のInterceptorで InvocationContext#proceed()をcallしなければ、それ以降のInterceptorが適用されることはありませんでした。 *3

使用方法

2重Submitのトリガーとなるアクションに@ExecuteOnceを付与。

以降、@ExecuteOnceが付与されているメソッドは 会話スコープが終了するまで 実行できなくなります。

できることは @ExecuteOnceによるメッセージ制御か 画面遷移のみ です。

各パラメータを使用したパターンについては、実行結果で示します。

@ExecuteOnce
public String fwComplete() {
    return "complete.xhtml";
}

実装と実行画面とログ

2回目の操作を無視

何もしません。

デフォルト時の挙動です。このままだとユーザーは困るので、通常は遷移先画面かメッセージ出力をして、何をしたらよいのか誘導した方が良いでしょう。

@ExecuteOnce
public String fwCompleteWithDefault() {
    System.out.println(this.getClass().toString() + "::登録処理を実施");
    return "complete.xhtml";
}

f:id:vermeer-1977:20180512232455p:plain

f:id:vermeer-1977:20180512232150p:plain

f:id:vermeer-1977:20180512232645p:plain

f:id:vermeer-1977:20180512232555p:plain

1回目の「登録(何もしない)」では、問題なく 完了画面へ遷移します。

f:id:vermeer-1977:20180512232810p:plain

f:id:vermeer-1977:20180512232834p:plain

「戻る」で前の画面に戻ります。

f:id:vermeer-1977:20180512233018p:plain

f:id:vermeer-1977:20180512233032p:plain

2回目の「登録(何もしない)」をクリックすると 登録ログは更新されていきますが、画面は一切変化しないことが確認できます。

f:id:vermeer-1977:20180512233146p:plain

f:id:vermeer-1977:20180512233201p:plain

2回目の操作で完了画面へ遷移

これが一番オーソドックスだと思います。

2度目以降の入力は無視して、とにかく完了画面へ遷移します。

特にエラーメッセージも出力しません。

@ExecuteOnce(forwardPage = "complete.xhtml")
public String fwComplete() {
    System.out.println(this.getClass().toString() + "::登録処理を実施");
    return "complete.xhtml";
}

<1回目の登録までは省略します>

f:id:vermeer-1977:20180513083541p:plain

f:id:vermeer-1977:20180513083617p:plain

f:id:vermeer-1977:20180513083302p:plain

エラーメッセージあり

エラーメッセージのみを指定した場合、自分画面へ遷移してメッセージを出力します。

画面上に、継続して処理したい場合のルートの情報をメッセージに出力しないとユーザーは不安になるでしょう。

@ExecuteOnce(message = "メッセージ出力:既に登録済みです。重複登録を抑止しました。トップページへ戻ってください。")
public String fwCompleteWithMessage() {
    System.out.println(this.getClass().toString() + "::登録処理を実施");
    return "complete.xhtml";
}

<1回目の登録までは省略します>

f:id:vermeer-1977:20180513083808p:plain

f:id:vermeer-1977:20180513083929p:plain

f:id:vermeer-1977:20180513083956p:plain

エラー画面遷移

本例だと、完了画面とエラー画面の違いは少ないですが、メッセージ出力タグがあったりして少しだけ違いがあります。

完了画面とは別のシーケンスだったことを示したい場合は、メッセージで通知するよりも エラー画面を設けておいた方が 1画面に複数仕様が混在しないので良いかもしれません。

デメリットは、画面レイアウトの変更をした際、完了画面とエラー画面の両方をメンテナンスしないといけないところでしょうか?

この辺りはJSFコンポーネントなり、テンプレートなりを使えば手間は減らせるかもしれません。

@ExecuteOnce(forwardPage = "error.xhtml", message = "エラー画面へ遷移:既に登録済みです。重複登録を抑止しました。新たに別の入力するか、トップページへ戻ってください。")
public String fwError() {
    System.out.println(this.getClass().toString() + "::登録処理を実施");
    return "complete.xhtml";
}

<1回目の登録までは省略します>

f:id:vermeer-1977:20180513084247p:plain

f:id:vermeer-1977:20180513084326p:plain

f:id:vermeer-1977:20180513084337p:plain

会話スコープが終わったらSubmit可能

本記事のテーマからは少しずれていますが、同一フォルダ内の遷移でも メソッドに@EndConversationを付与する事で会話スコープを終了できます。

通常フローの完了画面で「さらに入力」で、入力画面に戻ったら会話スコープが終了した後、新たに開始されます。

この時、同一フォルダの index.xhtmlですが @EndConversation の制御により、処理終了時に会話スコープは終了し、index.xhtmlの初期表示時に新たに会話スコープが開始されています。

これで、入力→確認→完了 のシーケンスをサイクリックに行えます。

@EndConversation
public String fwIndex() {
    return "index.xhtml";
}

<完了画面の登録までは省略します>

f:id:vermeer-1977:20180513084548p:plain

f:id:vermeer-1977:20180513084620p:plain

f:id:vermeer-1977:20180513084628p:plain

実装と仕組み

最優先のInterceptorで処理を行うことで、後続のInterceotorによって対象メソッドがcallされないようにします。

Annotation

デフォルト値は、空白とします。

意味としてはNullの方が良いのですが、Nullは設定できないので空白です。

@Target(value = {ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@InterceptorBinding
public @interface ExecuteOnce {

    @Nonbinding
    public String message() default "";

    @Nonbinding
    public String forwardPage() default "";
}

Interceptor

2回目以降の状態の場合は、通常フローの画面遷移をしないようにします。

ポイントになりそうなところには コメントを入れました。

@Interceptor
@Priority(Interceptor.Priority.APPLICATION)
@ExecuteOnce
public class ExecuteOnceInterceptor {

    private final DoubleSubmitLifecycle doubleSubmitLifecycle;

    @Inject
    public ExecuteOnceInterceptor(DoubleSubmitLifecycle doubleSubmitLifecycle) {
        this.doubleSubmitLifecycle = doubleSubmitLifecycle;
    }

    @AroundInvoke
    public Object invoke(InvocationContext ic) throws Exception {
        System.out.println(this.getClass().toString() + "::開始");

        try {
            if (this.doubleSubmitLifecycle.isSubmitted()) {
                ExecuteOnce annotation = ic.getMethod().getAnnotation(ExecuteOnce.class);
                return this.toErrorPage(annotation);
            }
            Object result = ic.proceed();
            return result;
        } finally {
            this.doubleSubmitLifecycle.nextState();
            System.out.println(this.getClass().toString() + "::終了");
        }

    }

    //
    Object toErrorPage(ExecuteOnce annotation) {
        System.out.println(this.getClass().toString() + "::2重Submit処理");
        String message = annotation.message();
        if (Objects.equals(message, "") == false) {
            System.out.println("::2重Submit処理のメッセージ::" + message);

            //<h:messages /> にメッセージを出力
            FacesContext.getCurrentInstance().addMessage(null, new FacesMessage(message));

            //リダイレクトしたときにメッセージがクリアされないようにする
            FacesContext.getCurrentInstance().getExternalContext().getFlash().setKeepMessages(true);
        }

        // エラーページを指定していない場合は自画面へ遷移
        String result = Objects.equals(annotation.forwardPage(), "")
                        ? FacesContext.getCurrentInstance().getViewRoot().getViewId()
                        : annotation.forwardPage();

        System.out.println("::2重Submit処理の遷移先::" + result);
        return result + "?faces-redirect=true";
    }

}

今回やっていないこと

入力画面まで戻ったFormの更新抑止

2重Submitであって、入力抑止ではないので 完了画面からブラウザの戻るなどを使って 入力画面まで戻って 入力をすると 確認画面の表示内容も更新されてしまいます。

もし完了画面でも その情報を使って「〇〇で処理しました」というような表示をすると どうでしょうか?

最終的な更新は抑止できていますが、ユーザーからすると「あれ?結局どっちで登録されたの?」という状況になってしまいます。もちろん、確認画面であっても 同じ印象を受けるでしょう。

ここまでを含めて対処しようとすると、例えば 入力画面で使用したFormと確認画面以降で使用するFormを別にして、DoubleSubmitLifecycleの状態を使って複製する、しないという制御をする、ということが案として考えられます。

もしくは、入力から確認への遷移アクションをimmediate=trueにするような仕組みを実装する、という案でも良いかもしれません。

重要な制御であるとは思っていますが、いったん、本記事では「2回登録されないようにする」という意味での2重Submit対策に留めておき 今後 検討したいと思います。

BusyConversationException対策

BusyConversationException(ConversationScopedにおける重複処理時で発生するException)に関する処理は考慮していません。

これについては、また別記事で やりたいと思っています。

理由はBusyConversationExceptionに限らず、例外発生時に行う画面遷移制御について、もう少し自分なりに考えたいと思っているためです。

クライアントのボタン抑止

JavaScriptによる操作抑止はやっていません。

まず、EEの範疇外かなぁというのと、サーバー側の実装がクライアント側の制御について 過度に干渉するのは好ましくないと考えたためです。

勿論、やった方がユーザーには親切です。

いずれにしても、クライアント側の制御はクライアント側で採用したフレームワークに則って対応すると良いと思います。

Ajaxでの2重Submit対策

Ajaxでの2重Submit対策はやっていません。

そもそもAjaxにおける2重Submit対策は、通常のWebアプリの2重Submitとは少し状況が違うように考えています。

少なくともユーザーに不利益を生み出しかねない「注文」や「登録」のようなメインとなるユースケースAjaxでSubmitする方式を適用するのは適切ではないと考えます。

この前提を踏まえつつも、実施する理由があるとしたら どうしても通信量を減らしたい、という要求があった場合でしょうか?

それであっても、確認画面では すでに入力情報はサーバー側で保持しており、クライアントから送るのは「これで良いですよ」という通知となるSubmitだけです。

Formの範囲を狭くさえすれば なんら問題はないと考えます。

いずれにせよ、Ajaxを絡めた制御については、今回のテーマからは一旦除外しておき Ajaxもしくは JSFでクライアントと密に連携をとる事例として必要性を感じたときに試してみたいと思います。

参照情報

2重Submit

JSF2.0でボタンの2度押しチェックをする - 見習いプログラミング日記

JSF2.0のエラーハンドリング - 見習いプログラミング日記

Struts2 - タグ - tokenタグ -2重送信防止 - liguofeng29’s blog

[Java]struts - 機能リファレンス - 2度押し防止トークン【PGBox】

SAStrutsで二重サブミット防止 - メモ

2007-09-05 - 出羽ブログ

Kumu Html Disabled

doOnceメソッドによる二重登録防止 - 出羽ブログ

[TEEDA-393] doOnce~() で二重サブミットされた場合,遷移前の画面に戻って新しいトランザクショントークンが発行されるため,結果的に二重登録が可能となる問題を修正しました. - The Seasar Foundation Issues (Deprecated)

TeedaのdoOnce関連

Teedaの考え方は命名で規約を設けていて、全く同じということはないのですが 沢山の参考知見がありました。

TeedaDoubleSubmittedExceptionをもって色々とハンドリングをしているようで、それを見たので私もExceptionHandlerWrapperを使わないと出来ないのかも、、と思いましたが、とりあえず そこまでしなくて実現はできました。

[Seasar-user:12783] [Teeda]doOnceボタンでDoubleSubmittedExceptionが発生

[Seasar-user:19342] [Teeda] DoubleSubmittedException をキャッチした後の遷移先を振り分けるには?

Code

Bitbucket

さいごに

何が嬉しいの?というところですが、トークンのためのタグをクライアントに実装しなくて良いというところでしょうか?

そもそもとしては 会話スコープのためにタグを追記しているのでは?というツッコミは容易に想像できますが、これは基本的に全てのページに共通して使用するテンプレートに実装するものであって、2重Submit防止制御をするページに都度適用するものとは違うと考えています。

とはいえ、結局のところ コードによる制御であるため 場合によっては見通しは良いかなぁ、とか トークンタグの実装をしなくても良い、くらいかもしれません。

一般的なやり方(タグでクライアントにトークンをhiddenで保持する)であっても、基本ルールを設けてしまえば「そういうものか」ということで特に困ることは無い気もします。

コード内で直接制御するという方法*4のだって別に悪い手だとは思っていません。

まぁ、ConversationScopeのある EE ならではの実装例 という意義くらいかもしれませんが、こういう やり方もありますよ、というところでしょうか。

とりあえず、いつものように右往左往しつつ 出来上がったものをみたら「あれ?こんなもん?」という感じですが、個人的にはアプリケーションコードとして、アノテーションによる宣言的な実装で 目的を達成できる方式が試せたので満足です。

*1:正しくは状態不正を発生させない仕組み、ですが

*2:ただし、BusyConversationException に関する対応は 本記事の対象外です

*3:あやうく、ExceptionHandlerWrapperによる制御をしなくてはいけないかと思って「やばい、想像以上に面倒な方式を選択したかも・・」と思ったのですが、とりあえずInterceptorで実装してみて確認しようと思ってよかったです

*4:Strutsのような