コンピュータクワガタ

かっぱのかっぱによるコンピュータ関連のサイトです

トークンをアノテーションでいじってみる。

SAStrutsで自前AjaxアノテーションAOPを使って

超簡単にAjaxできてしまうプログラムを書いてみた。

出羽ブログ 〜はてな版〜

から、パクリで。Ajaxではなくトークンですが。
トークンは毎度ほとんど同じ処理なので、コードを書きたくないからInterceptorでできないかなと考えていたところだったので、非常に参考になりました。
とりあえず、方針は以下。
Actionのメソッドに以下のアノテーションをつける。

  • @Token トークンの発行。
  • @TokenCheck トークンのチェック 実行メソッドにつける。

トークンのチェックに失敗した場合には、今は例外を投げる。

まず、@Tokenアノテーション

package test.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Token {
}

そして、@TokenCheckアノテーション

package test.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface TokenCheck {
}

それで、トークンの発行をするInterceptor。

package test.interceptor;

import javax.servlet.http.HttpServletRequest;

import org.aopalliance.intercept.MethodInvocation;
import org.apache.struts.util.TokenProcessor;
import org.seasar.framework.aop.interceptors.AbstractInterceptor;
import org.seasar.framework.container.S2Container;

import test.annotation.Token;

public class TokenInterceptor extends AbstractInterceptor {
    private S2Container container;

    public Object invoke(MethodInvocation invocation) throws Throwable {
        Object ret = invocation.proceed();

        Token token = invocation.getMethod().getAnnotation(Token.class);
        if (token != null) {
            HttpServletRequest request = (HttpServletRequest) container
                    .getExternalContext().getRequest();
            TokenProcessor.getInstance().saveToken(request);
        }

        return ret;
    }

    public S2Container getContainer() {
        return this.container;
    }

    public void setContainer(S2Container container) {
        this.container = container.getRoot();
    }
}

トークンチェックするInterceptor。

package test.interceptor;

import javax.servlet.http.HttpServletRequest;

import org.aopalliance.intercept.MethodInvocation;
import org.apache.struts.util.TokenProcessor;
import org.seasar.framework.aop.interceptors.AbstractInterceptor;
import org.seasar.framework.container.S2Container;

import test.annotation.TokenCheck;

public class TokenCheckInterceptor extends AbstractInterceptor {
    private S2Container container;

    public Object invoke(MethodInvocation invocation) throws Throwable {

        TokenCheck tokenCheck = invocation.getMethod().getAnnotation(
                TokenCheck.class);
        if (tokenCheck != null) {
            HttpServletRequest request = (HttpServletRequest) container
                    .getExternalContext().getRequest();
            if (!TokenProcessor.getInstance().isTokenValid(request, true)) {
                throw new RuntimeException("二重処理");
            }
        }

        return invocation.proceed();
    }

    public S2Container getContainer() {
        return this.container;
    }

    public void setContainer(S2Container container) {
        this.container = container.getRoot();
    }
}

今は、適当な例外を投げているだけ。一応、これだけで動きました。

これはこれでいいのですが、2重処理チェック自体どのように処理するかいつも迷うのでいろいろパターンを考えています。
だいたい以下のいずれかかなと思います。

  • 2度押しで、例外発生、エラー画面に遷移。(ちょっと厳しい)
  • 2度押しで、2回目のリクエストは無視。
  • 2度押しで、入力画面に戻りメッセージ表示。(1度目の処理が処理されたかわかりずらい?)

たいていは、JavaScriptの2重処理チェックと併せて行うので、エラー画面でもいいかなと思いつつも、
更新ボタン押下→遷移した画面でブラウザの「更新」ボタンでも2重処理となるので、ブラウザの「更新」ができないというのはちょっと。。という気もするので。
上記の例は一番最初の実装です。
2番目は、Interceptorでやるのはちょっと難しいかなと。インターセプトしてトークンのチェックが失敗した場合に、「実行しようとしているメソッドの処理結果」をメソッドを実行せずにreturnしないといけないから。あ、アノテーションのパラメータに遷移先を書けばできるかも。もうちょっと考えよう。
3番目もinterceptorでは。。できるな。
とりあえず、以下のような感じ。

package test.interceptor;

import javax.servlet.http.HttpServletRequest;

import org.aopalliance.intercept.MethodInvocation;
import org.apache.struts.action.ActionMessage;
import org.apache.struts.action.ActionMessages;
import org.apache.struts.util.TokenProcessor;
import org.seasar.framework.aop.interceptors.AbstractInterceptor;
import org.seasar.framework.container.S2Container;

import test.annotation.TokenCheck;

public class TokenCheckMessageInterceptor extends AbstractInterceptor {
    private S2Container container;

    public Object invoke(MethodInvocation invocation) throws Throwable {

        Object ret = invocation.proceed();
        TokenCheck tokenCheck = invocation.getMethod().getAnnotation(
                TokenCheck.class);
        if (tokenCheck != null) {
            HttpServletRequest request = (HttpServletRequest) container
                    .getExternalContext().getRequest();
            if (!TokenProcessor.getInstance().isTokenValid(request, true)) {
                ActionMessages errors = (ActionMessages) ret;
                if (errors == null || errors.isEmpty()) {
                    if (errors == null) {
                        errors = new ActionMessages();
                    }
                    errors.add(ActionMessages.GLOBAL_MESSAGE,
                            new ActionMessage("2重処理だよ!", false));
                    return errors;
                }
            }
        }

        return ret;
    }

    public S2Container getContainer() {
        return this.container;
    }

    public void setContainer(S2Container container) {
        this.container = container.getRoot();
    }
}

トークンは、validateメソッドにつけて、通常のチェックでエラーがない場合にのみトークンのチェックをする。
ただし、ブラウザの更新を押すと、遷移前の画面に戻ってメッセージが出るからちょっと違和感があるなあ。