Actionの共通処理をどこに実装するか?

Struts(1.2.x以下)からSAStrutsへの置き換えをするときに、Actionの共通前処理をどこに実装するかで迷った。
ここで言う共通前処理とは、個々のActionを実行する前に共通で実行したいような処理のこと。
Strutsでは、org.apache.struts.actions.DispatchActionを継承したクラスを作成し、dispatchMethodをオーバーライドして、そこに実装していたが、SAStrutsPOJO ActionなのでDispatchActionに該当するものがない。


まず、考えたのはAOPで実装する案。
個々のActionに共通のスーパークラスを作成し、共通前処理メソッドを実装した上で、Interceptorを作成し、共通前処理メソッドを呼び出すようにする。
せっかくPOJO Actionなのにスーパークラスの継承を強要させるのは微妙な気がしなくもないが、
request、responseといったDI用フィールドや、saveError、saveErrors、saveToken、isTokenValid、、、
といったようなメソッドは実装継承させちゃった方が便利。
コードはこんな感じ。
2009/5/13 追記
AbstractActionのrequest、responseがpublicになっていて、パラメータにより汚染される可能性があったためprotectedに修正しました。


Actionの共通スーパークラス

public abstract class AbstractAction {
    @Resource
    protected HttpServletRequest request;

    @Resource
    protected HttpServletResponse response;
    // 略

    public String beforeExecute(MethodInvocation invocation) throws Exception {
        // 共通前処理を実装
    }
}


Interceptor

public class ActionInterceptor extends AbstractInterceptor {
    private static final long serialVersionUID = 1L;
    private static final String BEFORE_METHOD = "beforeExecute";

    public Object invoke(MethodInvocation invocation) throws Throwable {
        if (!invocation.getMethod().isAnnotationPresent(Execute.class)) {
            return invocation.proceed();
        }
        Class<?>[] c = {MethodInvocation.class};
        Object[] args = {invocation};

        try {
            Method method = invocation.getThis().getClass().getMethod(BEFORE_METHOD, c);
            String path = (String) method.invoke(invocation.getThis(), args);
            if (path != null) {
                // 前処理の結果が null でない場合は、結果のパスに遷移する
                HttpServletResponse response = SingletonS2Container.getComponent(HttpServletResponse.class);
                if (response.isCommitted()) {
                    return null;
                }
                return path;
            }
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        }
        return invocation.proceed();
    }

}

ポイントカットは@Executeのついたメソッドだけでいいので、判定を入れている。
S2AOP標準で、ポイントカットをアノテーション名で指定できればうれしいんだけど、たぶん今のところメソッド名での指定しかできないみたい。
共通前処理内でforwardやリダイレクトさせたいこともあるので、パスを返せるようにした。


ただ、実装してみてちょっと問題が発生。

共通前処理でActionFormにアクセスしたいんだけど、難しい。
ActionFormもAction同様に共通のスーパークラスを持たせていて、個々のActionFormをスーパークラスにキャストして操作したい。さて、どうしよう?ActionにActionFormのゲッターをつくってやれば、ゲッター経由で取得可能だけど、各Actionにいちいちゲッターを定義するのは面倒。ActionFormはS2Containerが管理するコンポーネント名(例.XxxFormならxxxForm)でrequestにセットされるので、 request.getAtributeNames()でキー名がFormで終わるものを探して、しかもActionFormWrapperもxxxActionFormといったキー名でセットされているようなのでそれを除くみたいなことをすれば取得できそうだけど、ちょっと強引すぎる気がする。
どういう条件だったのか忘れてしまったけど、AOPの無限ループが発生したことがあった。
以前に試したときに発生したんだけど、よく覚えてない、、、
InterceptorからActionの共通前処理メソッド呼び出し時に、またインターセプトされるのが無駄。トラブったときにトレースしにくい。
beforeExecuteをポイントカットの対象外メソッド名にすればとりあえずは防げるが、共通前処理内部で別のメソッドを呼んだりもしているのでそこにはAOPがかかってしまう。根本的には、共通前処理をActionに書かなければいいんだけど、共通前処理内容にビジネスロジックが入る可能性もあるので、Interceptorに実装するのは適さないし、StrutsではDispatchActionに実装していた処理なので、Actionのスーパークラスに実装するというのが開発メンバーに馴染みやすい。


結局、AOPで実装するのはやめて、org.seasar.struts.action.ActionWrapperを継承したクラスを作成し、直接、共通前処理メソッドを呼び出すことにした。
コードはこんな感じ。


Actionの共通スーパークラス

public abstract class AbstractAction {
    @Resource
    protected HttpServletRequest request;

    @Resource
    protected HttpServletResponse response;
    // 略

    protected String beforeExecute(Method method, Object form) {
        // 共通前処理を実装
    }
}

共通前処理でメソッドアノテーションを取得したり、メソッド名を取得したりできるよう引数にMethodを入れている。
beforeExecuteの2つ目の引数にはActionFormが入るんだけど、呼び出し元のMyActionWrapper(下記)がAbstractFormに依存するのがなんとなく嫌だったので、それを防ぐためにObject型にしてみた。


ActionWrapperを継承したクラス

public class MyActionWrapper extends ActionWrapper {
    private static final String BEFORE_METHOD = "beforeExecute";

    public MyActionWrapper(S2ActionMapping actionMapping) {
        super(actionMapping);
    }

    @Override
    public ActionForward execute(ActionMapping mapping, ActionForm form,
            HttpServletRequest request, HttpServletResponse response)
            throws Exception {

        S2ExecuteConfig executeConfig = S2ExecuteConfigUtil.getExecuteConfig();
        if (executeConfig != null) {
            Class<?> clazz = action.getClass();
            Method method = null;
            method = getMethodIncludeSuperClass(clazz, BEFORE_METHOD, Method.class, Object.class);
            if (method != null) {
                if (!Modifier.isPublic(method.getModifiers())) {
                    method.setAccessible(true);
                }
                Object[] args = {executeConfig.getMethod(), actionForm};
                String next = (String) MethodUtil.invoke(method, action, args);
                if (next != null) {
                    // 前処理の結果が null でない場合は、結果のURLに遷移する
                    if (response.isCommitted()) {
                        return null;
                    }
                    return actionMapping.createForward(next);
                }
            }
            return execute(request, executeConfig);
        }
        return null;
    }

    public Method getMethodIncludeSuperClass(Class<?> clazz, String methodName, Class... args) {
        Class<?> type = clazz;
        Method method = null;
        while (type != null) {
            try {
                method = type.getDeclaredMethod(methodName, args);
                break;
            } catch (NoSuchMethodException e) {
                type = type.getSuperclass();
            }
        }
        return method;
    }
}


MyActionWrapperを使用させるために、org.seasar.struts.action.S2RequestProcessorを継承したクラス

public class MyRequestProcessor extends S2RequestProcessor {

    protected static final Log log = LogFactory.getLog(MyRequestProcessor.class);

    @Override
    protected Action processActionCreate(HttpServletRequest request,
            HttpServletResponse response, ActionMapping mapping)
            throws IOException {

        Action action = null;
        try {
            action = new MyActionWrapper(((S2ActionMapping) mapping));
        } catch (Exception e) {
            log.error(getInternal().getMessage("actionCreate", mapping.getPath()), e);
            response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
                            getInternal().getMessage("actionCreate", mapping.getPath()));
            return null;
        }
        action.setServlet(servlet);
        return action;
    }
}

インスタンスを生成するActionをActionWrapperからMyActionWrapperに変更しただけ。


struts-config.xml
タグのprocessorClassを変更。

    <controller
        maxFileSize="1024K"
        bufferSize="1024"
        processorClass="xxx.MyRequestProcessor"
        multipartClass="org.seasar.struts.upload.S2MultipartRequestHandler"/>


ただし、SAStrutsのコードをオーバーライドしているので、バージョンアップの都度、S2RequestProcessor#processActionCreateと
ActionWrapper#executeが変更されていないかのコードチェックは必要になる。まあ、とはいえ、ここが変わることはそんなにないだろうけど。
ちなみに、上記コードはSAStruts1.0.4-sp4がベース。