Legacy Struts Best Practices~動的にActionの前後処理を変更~

今回は、Spring無しでStrutsのAction実装クラスに対し、

依存性を分離させ、かつ動的に処理内容を変更する方法です。

Spring無しでシンプルなAOPを実装する方法は下記ページを参考にしました。

最もシンプルにJavaのAOPを書いてみる->そしてJavaScriptへ at HouseTect, JavaScriptな情報をあなたに

要は、Proxy#newProxyInstanceを使え、という事。

しかし、Struts1.2のAction実装クラスに対しては2点、問題があります。

org.apache.struts.action.Action自体はPOJOである(インターフェイスを実装していない)ため、

Proxy#newProxyInstanceに渡すInvocationHandlerオブジェクトのinvokeメソッドで

IllegalArgumentExceptionが発生する。

Proxy#newProxyInstanceを実行するタイミングは、

必然的にRequestProcesser継承クラスを作成し、

オーバーライドしたprocessActionCreateメソッドかprocessActionPerformメソッドとなるが、

戻り値又は引数の型がorg.apache.struts.action.Actionである為、

Proxy#newProxyInstanceでプロキシインスタンスを生成できたとしても渡せない。

この問題点を解決する為に、

「違う型階層のクラスを共通の型で扱う」必要があります。

では、早速コードをば。

ActionInterface

まず、Actionの公開オペレーション(publicメソッド)と

同じシグネチャの抽象メソッドをもつインターフェイスを定義します。

public interface ActionInterface{
    ActionForward execute(ActionMapping mapping
      , ActionForm form
      , HttpServletRequest request
      , HttpServletResponse response)
      throws Exception;

    ActionForward execute(ActionMapping mapping
      , ActionForm form
      , ServletRequest request
      , ServletResponse response)
      throws Exception;

    ActionServlet getServlet();
    void setServlet(ActionServlet servlet);

    int hashCode();
    boolean equals(Object obj);
    String toString();
}

AbstractAction

Javaで「違う型階層のクラスを共通の型で扱う」方法その1、です。

ActionInterfaceを実装し、

org.apache.struts.action.Actionを継承した抽象クラスを定義します。

ActionInterfaceの抽象メソッドはorg.apache.struts.action.Actionが

暗黙的にオーバーライドしてくれるので、

全ての抽象メソッドを記述する必要はありません。

空でもOKですが、

Action実装クラスの共通処理を実装させるのがベターです。

public abstract class AbstractAction extends Action implements ActionInterface {
    /* (non-Javadoc)
    */
    public ActionForward execute(ActionMapping mapping
        , ActionForm form
        , HttpServletRequest request
        , HttpServletResponse response) throws Exception {
        //キャンセル時の対応
        if (isCancelled(request)) {
            ActionForward af = cancelled(mapping, form, request, response);
            if (af != null) {
                return af;
            } else {
                throw new ServletException();
            }
        }
        ActionForward af = null;
        af = this.executeAction(mapping, form, request, response);
        return af;
    }

    /*
    * サブクラス固有の処理を実装
    */
    public abstract ActionForward executeAction(ActionMapping mapping
        , ActionForm form
        , HttpServletRequest request
        , HttpServletResponse response) throws Exception;

    /*
    * キャンセルボタンが押された場合の共通処理
    */
    protected ActionForward cancelled(ActionMapping mapping
        , ActionForm form
        , HttpServletRequest request
        , HttpServletResponse response) throws Exception {
        return mapping.findForward("cancel");
    }
}

InvocationHandler実装クラス

ここではInvocationHandlerを直接実装していますが、

実際は抽象クラスを骨格実装して、

SpringのAdviceのBefore、After、Around、Throwように、

サブクラスで割り込むタイミングを選ぶようにした方が実践的かもしれません。

public class SampleActionHandler implements InvocationHandler {

    private final ActionInterface action;

    public SampleActionHandler(final ActionInterface action) {
        this.action = action;
    }

    public Object invoke(Object proxy
        , Method method
        , Object[] param) throws Throwable {
        // 前処理
        System.out.println("before:" + action.getClass().getName());
        // メソッド実行
        Object result = method.invoke(action, param);
        // 後処理
        System.out.println("after:" + method.getName());
        return result;
    }
}

これで、先ほど挙げた問題点1は解決します。

問題点2もAbstractActionにキャストしてやればOKなのですが、

AbstractActionクラスに記述した共通処理では無く、

別のActionInterface実装クラスを定義した場合、

RequestProcesserでAbstractActionにキャストしていると、

コードを修正しなければなりません。

色々とコード修正を防ぐ方法は考えられますが、

ここではインターフェイスベースのプログラミングを心掛けます。

ActionAdapter

Javaで「違う型階層のクラスに同じ振る舞いをさせる」方法その2、です。

GoFデザインパターンのAdapterパターンを使います。

public class ActionAdapter extends Action {

    private final ActionInterface action;

    public ActionAdapter(ActionInterface action) {
        this.action = action;
    }

    // 以下、転送メソッド
    public boolean equals(Object arg0) {
        return action.equals(arg0);
    }

    public ActionForward execute(ActionMapping mapping
        , ActionForm form
        , HttpServletRequest request
        , HttpServletResponse response) throws Exception {
        return action.execute(mapping, form, request, response);
    }

    public ActionForward execute(ActionMapping mapping
        , ActionForm form
        , ServletRequest request
        , ServletResponse response) throws Exception {
        return action.execute(mapping, form, request, response);
    }

    public ActionServlet getServlet() {
        return action.getServlet();
    }
    public int hashCode() {
        return action.hashCode();
    }
    public void setServlet(ActionServlet servlet) {
        action.setServlet(servlet);
    }
    public String toString() {
        return action.toString();
    }
}

ちなみに、保守案件ではAdapterパターンにはとても助けられています。

型関連が無い、ほぼ同じプロパティのJavaBeanが複数あった場合、

わざわざ引数や戻り値の型が違う、同じような実装のメソッドをコーディングする必要がありますが、

Adapterパターンを使うとラップしたインスタンスの型を意識しなくて済むため、

一つのメソッドをコーディングするだけでOKです。

IDEを使えば、ラップしたインスタンスの転送メソッドを記述するのも簡単ですし。

デザインパターンには、クラスやオブジェクト、振る舞いや構造といった分類がありますが、

設計・実装・保守の観点から各パターンを見てみるのも面白いと思います。

さて、最後は実際にプロキシインスタンスを生成する、

RequestProcessorサブクラスの実装です。

RequestProcessorサブクラス

ここではprocessActionCreateをオーバーライドしていますが、

processActionPerformでも可です。

public class RequestProcessorPlus extends RequestProcessor {

    /* (non-Javadoc)
    * スーパークラスのprocessActionCreateメソッドで生成されたActionが
    * ActionInterfaceと代入互換の関係である場合、
    * ActionInterfaceのプロキシインスタンスを生成、
    * ActionAdapterクラスでラップし、呼び出し元に返す。
    */
    protected Action processActionCreate(HttpServletRequest request
        , HttpServletResponse response
        , ActionMapping mapping) throws IOException {
        Action action = super.processActionCreate(request, response, mapping);
        // ActionInterfaceと代入互換の関係である場合のみ
        if (action instanceof ActionInterface){
            // Handlerインスタンスの生成
            InvocationHandler handler = new SampleActionHandler((ActionInterface)action);
            // プロキシインスタンスの生成
            ActionInterface actionInterface = (ActionInterface)Proxy.newProxyInstance(
            ActionInterface.class.getClassLoader()
            , new Class[]{ActionInterface.class}
            , handler);
            // Adapterクラスでラップ
            action = new ActionAdapter(actionInterface);
        }
        return action;
    }
}

Filter又はServletでHttpServletRequestに

InvocationHandler実装クラスやActionAdapter実装クラスの

Classオブジェクトを突っ込んでおき、

リフレクションでコンストラクタから各インスタンスを生成するようにすれば、

リクエストパラメータやセッションの値、

URLパスやモジュール別に振る舞いを柔軟に変更する事ができます。

ログ出力や、ビジネスロジックBeanのFactoryに渡すキー文字列の生成、

Actionのトランザクション制御などに利用できるかと思います。

汎用的なActionを作成する手助けとなるかもしれません。

まぁ、ぶっちゃけSpringを使った方が手っ取りばやいのですが。

SpringフレームワークのAOPを使いたいけど、

フル活用するわけでも無いし、教育コストをかけたくない場合等の

かなり限られた条件下なら有効です(笑)

このエントリーをはてなブックマークに追加
comments powered by Disqus