Wicket はステートフルなフレームワークで、ページの状態を version 番号をふって
管理しています。

ログインを求める会員制のサイトなどの場合この性質が良かったりしますが、
誰でも閲覧が可能な公開ページの場合は、URL にごみがついていたり、
状態を保持していることが、SEO 観点で災いをもたらすと考えます。

個人的に作成しているアプリケーションは公開アプリケーションのため、
認証な必要なページ以外は、Stateless にしたいと考えています。
Stateless にするのに必要なことを調べつつ、
Stateful になっていたページを Stateless にしていく過程を記載します。


前提 Wicket の Version

7.6.0です。

    <dependency>
        <groupId>org.apache.wicket</groupId>
        <artifactId>wicket-core</artifactId>
        <version>7.6.0</version>
    </dependency>


参考記事


Wicket の コンポーネント についての個人的な理解

  1. 基本的に大概のコンポーネントは ステートレス だが、子のコンポーネントが ステートフル だと
    ステートフル になる。

  2. Link と、Form ステートフル なので、 Link と、Form持つコンポーネントは ステートフルになる。

  3. 上記、1. 、2. により、何にも考えないで作っていると、結構 ステートフル になりやすい。


  1. Link には、StatelessLink BookmarkablePageLink使うと、ステートレスになる。1
    [1] 当たり前かもですが、 ExternalLink の ステートレスです。

  2. Form には、StatelessForm用いると ステートレスになる。

  3. 上記、1.、2.より Form と、Linkそれぞれ、StatelessForm BookmarkablePageLink変えていくと、結構 ステートレスになりやすい。


ステートレスなページかどうかを確認するには

wicket/wicket-devutils at master · apache/wicket
StatelessChecker使うと、ステートレスなページでないページを検知できます。
前に、こちら記事にしましたので、そちらのリンクを貼り付けておきます。

StatelessChecker デフォルトで例外をスローするので、以下の通り、
例外がスローされたら、ログに書き出すようにしました。

    // add StatelessChecker
    getComponentPostOnBeforeRenderListeners().add(new StatelessChecker() {
        public void onBeforeRender(final Component component) {
            try {
                super.onBeforeRender(component);
            } catch (IllegalArgumentException | IllegalStateException e) {
                log.warn("Exception occurred..", e);
            }
        }
    });


ステートレスな拡張コンポーネント

wicketstuff に stateless な コンポーネントのライブラリがあります。
Wicket 8 では 本体に含まれる? のか @deprecatedついていますが、
Wicket 7 に含まれてはなさそうなので、こちらを使用していきます。

以下、ライブラリの github のリンクを貼り付けておきます。

pom.xml には、以下のように記載しました。

    <!-- https://mvnrepository.com/artifact/org.wicketstuff/wicketstuff-stateless -->
    <dependency>
        <groupId>org.wicketstuff</groupId>
        <artifactId>wicketstuff-stateless</artifactId>
        <version>7.6.0</version>
    </dependency>


ステートフルな Page を、 ステートレスなページに書き換えていく

以下のような手順で書き換えていきました。

  1. LinkForm は、BookmarkablePageLinkStatelessForm書き換える。

  2. AjaxButton は、StatelessAjaxButton書き換える

  3. StatelessChecker ComponentPostOnBeforeRenderListenerして追加。

  4. 動作確認して、StatelessCheckerログが出力された実装の書き換えを実施。

4.ログが出力され実装の書き換えをした内容について以下記載します。

AjaxPreventSubmitBehavior がステートフル

AjaxPreventSubmitBehaviorステートフルだったため、
以下の通り、変更しました。
StatelessAjaxSubmitBehavior継承して、以下のようなクラスを作成しました。

ですが、AjaxCallListener ステートフルのようで?、
相変わらずエラーログが消えません。
AjaxCallListener使わずに、AttributeModifier使って以下の通り実装しました。

    private AttributeModifier getOnkeydownPreventSubmitAttributeModifer() {
        return AttributeModifier.replace("onkeydown", "if(event.keyCode==13 || window.event.keyCode==13){return false;} else {return true;}");
    }

AjaxFormValidatingBehavior が ステートフル

AjaxFormValidatingBehavior が、ステートフルのため、以下のログが出力されました。

java.lang.IllegalStateException: '[SynchTokenField [Component id = token]]' claims to be stateless but isn't. Stateful behaviors: org.apache.wicket.ajax.form.AjaxFormSubmitBehavior
    at org.apache.wicket.devutils.stateless.StatelessChecker.onBeforeRender(StatelessChecker.java:114) ~[festivals4partypeople-web-0.0.1.jar:0.0.1]

AjaxFormSubmitBehaviorログに出ているのですが、 インナークラス AjaxFormValidatingBehavior.FormValidateVisitor実装内で、AjaxFormSubmitBehavior使用しているのが、
原因のようです。

  • コード抜粋

    public void component(final FormComponent component, IVisit<Void> visit) {
        AjaxFormSubmitBehavior behavior = new AjaxFormSubmitBehavior(AjaxFormValidatingBehavior.this.form, AjaxFormValidatingBehavior.this.event) {
        //............略

AjaxFormSubmitBehavior ステートレス版の、StatelessAjaxFormSubmitBehaviorいうクラスがありましたので、
StatelessAjaxFormSubmitBehavior書き換えて StatelessAjaxFormValidatingBehavior作成してみます。

StatelessAjaxFormSubmitBehavior には、Formを引数に持つコンストラクタないため、単純な置き換えができず、event引数に取る コンストラスタに置き換えましたが、
それが影響しているのか、FeedbackPanel動作しなくなりました。
MarkupId定義がないという javascript エラーだったので、試しに、wicket:id同じ id を付与するようにしたところ、
上手く動作しました。2 [2] 上手く動作している理由は現状よくわかっておりません。

    /**
     * Constructor
     *
     * @param id
     */
    public ApplicationFeedbackPanel(String id) {
        super(id);
        setMarkupId(id);
    }

Transaction token 発行用に作成した Hidden コンポーネントが動かない。

上記の対応で、作成していたページは ステートレスになりましたが、
Transaction token 発行に以下のような Class を作成して、
2度押しチェックを行っていた箇所が、 StatelessForm に変更後、
通常送信でも2度押しチェックエラーが発生するようになりました。3
[3] Form が Post 送信のたびに、再生成されて Token が変わってしまっているのが原因かと思われます。

以下が対象のクラスになります。

Wicket 6から CsrfPreventionRequestCycleListenerいう RequestCycleListener が追加されています。
Origin Refererよるチェックであれば、これを使うことで実施できるので、
Application クラスに追加します。

    CsrfPreventionRequestCycleListener csrfPreventionListener = new CsrfPreventionRequestCycleListener() {
        @Override
        protected boolean isLocalOrigin(HttpServletRequest containerRequest, String originHeader) {
            return false;
        }
    };
    csrfPreventionListener.addAcceptedOrigin("www.yourdomain.com");
    getRequestCycleListeners().add(csrfPreventionListener);

これで Referer が、www.yourdomain.com場合は、デフォルト設定では、リクエストは破棄され、400エラーが返るようになります。

  • 説明1 isLocalOrigin()falseを返すのは、testのためです。プロダクション環境で使用する場合は、設定は不要です。

  • 説明2
    csrfPreventionListener.addAcceptedOrigin("www.yourdomain.com");アクセスを認める、hostを設定します。

まとめ

Stateful な Page を Stateless にしてみました。 以下まとめます。

  • Form が存在しないページのStateless化は容易だが、Form が存在して、且つ、Ajax を使用していると途端にStateless にするのが難しくなる。

  • Stateful だとWicket 側の もともと用意されている部品群でいろいろできるが、Sateless にすると自前で実装する必要が出てくる。4
    [4] stateless-parent使うと、いい感じでできるが、それでも多少不便

  • SEO 対策が必要なページは Stateless だが、それ以外のページは、実装が簡単なのでStateful にしておきたい。

  • 思わぬところに、Stateful な コンポーネント、ビヘイビアがいる。 StatelessChecker は使ったほうが、作業が早く進む。

  • CsrfPreventionRequestCycleListener のだけでなく、同期tokenを用いたチェックもしたい。

まだ実装しきったとは言えず、手探りですが理解はできた気がします。
以上です。

コメント