S2Strutsの処理概要

ちょっと独自ルールで実行時のActionを切り替えたり、ActionMappingを差し替えたりしたいので、仕組みを追ってみました。
Struts経験が浅いので一部言っていることが怪しいかもしれないですが、大枠はそれほど外してないと思います。

目的

一応、以下を実現することを目的として処理を調査します。

  • (A)未登録なpathに対するリクエストを登録済みのアクションのいずれかに振り替える
  • (B)リクエスト時のpathによって、実行アクションに設定されているviewファイル以外のviewファイルを表示したい


たとえば、自動登録でもstruts-config.xmlでもどっちでもいいので、

path Actionクラス viewファイル
/hoge/hello.do hoge.HelloAction /pages/hello.html
/hoge/foo/hello.do hoge.foo.HelloAction /pages/foo/hello.html

となるようにAction定義をします。

で、(A)は、

みたいな処理をしたいわけです。
実行時のURL/パスによって動的にディスパッチ先を変更する、というところがポイントです。
静的に決定できるのであれば、ZeroConfig〜Ruleで十分対応できるんですが。

また、(B)は、さっきの例でいくと

  • http://xxxxxx/hoge/foo/hello.doというURLでリクエストすると、/pages/foo/hello.htmlがviewファイルとして使用される。
  • http://xxxxxx/hoge/bar/hello.doというURLでリクエストすると、アクションはHelloActionだけど、viewファイルとしてはあらためてリクエストURLを元に決定する。つまり、/pages/bar/hello.htmlが存在すればそれを使用し、存在しなければ/pages/hello.htmlを使用する。

ということを実現したいわけです。

そんな設計が必要なのか?という批判はおいといて、まあやりたいわけです。

調査結果

org.seasar.struts.plugin.RegistActionClassPlugInとは?

struts-config.xmlにplug-inとして登録して使う。

初期化時にstruts-config.xmlで明示的に記述されたActionの登録を行う。

org.seasar.struts.plugin.AutoStrutsConfigRegisterPlugInとは?

struts-config.xmlにplug-inとして登録して使う。
RegistActionClassPlugInの後に実行されるように、記述順序も後にしておく必要あり。

クラスパスからActionを検出して自動登録する。
path, name(form), forwardConfig等の決定にs2struts.diconで変更可能なZeroConfig〜Ruleが使われる。

(処理概略)
ClassFinder(直接内部でnewしてる)で検出した全ClassをAutoActionFormRegister, AutoActionRegisterに渡す。
それぞれの中ではすでに明示的指定により登録されているかをチェックして、未登録であれば登録処理を行う。アノテーションまたはZeroConfig〜Ruleによって属性値を決定して定義をModuleConfigに登録する。

つまり、struts-config.xmlで書くべきところを、アノテーションで指定したり、自動的に解決できたりする、という部分を担っている。実行時の動的な条件分岐などには関係しない。

org.seasar.struts.servlet.S2ActionServletとは?

web.xmlにFront Controllerサーブレットとして登録する。
RequestProcessorFactoryからRequestProcessorを取得して実行する。
RequestProcessorFactoryではどうやらstruts-config.xmlでcontrollerを登録してなくても、S2Containerから取得して見つかればそれを使うらしい。
s2struts.diconに登録してあるからstruts-config.xmlからは削除してもいいんでしょうか?
struts-config.xmlに書いてあったほうがStruts-erにはわかり良いかもしれませんが、RequestProcessorを差し替えたい場合に2箇所修正しないといけないのは、DRY原則的にはどうなんでしょうか。
むしろdicon側が要らない?

org.seasar.struts.processor.S2RequestProcessorとは?

struts-config.xmlにRequestProcessorとして登録して使う。
実行時にリクエストの制御を行う。

トレースログで追ってみる

s2struts.diconで制御可能な部分でだけトレースログを出して見ると↓となりました。
見やすいようにBEGIN,ENDの対で適当にインデント入れてみました。
処理を見てみます。

BEGIN org.seasar.struts.processor.RequestProcessorFactoryImpl#getRequestProcessor()
    BEGIN org.seasar.struts.ActionExecuteProcessorImpl#toString()
        BEGIN org.seasar.struts.ActionExecuteProcessorImpl#hashCode()
        END org.seasar.struts.ActionExecuteProcessorImpl#hashCode()
    END org.seasar.struts.ActionExecuteProcessorImpl#toString()
    BEGIN org.seasar.struts.processor.S2RequestProcessor#setExecuteProcessor()
    END org.seasar.struts.processor.S2RequestProcessor#setExecuteProcessor()
END org.seasar.struts.processor.RequestProcessorFactoryImpl#getRequestProcessor()

↑上で書いたS2ActionServletでRequestProcessor(S2RequestProcessor)を取得するところですね。
で、S2RequestProcessor#process()を実行↓。

BEGIN org.seasar.struts.processor.S2RequestProcessor#process()
    BEGIN org.seasar.struts.processor.AcceptorImpl#process()
        BEGIN org.seasar.struts.processor.S2RequestProcessor#processMultipart()
        END org.seasar.struts.processor.S2RequestProcessor#processMultipart()
        BEGIN org.seasar.struts.processor.S2RequestProcessor#processPath()
        END org.seasar.struts.processor.S2RequestProcessor#processPath()
        BEGIN org.seasar.struts.processor.S2RequestProcessor#processLocale()
        END org.seasar.struts.processor.S2RequestProcessor#processLocale()
        BEGIN org.seasar.struts.processor.S2RequestProcessor#processContent()
        END org.seasar.struts.processor.S2RequestProcessor#processContent()
        BEGIN org.seasar.struts.processor.S2RequestProcessor#processNoCache()
        END org.seasar.struts.processor.S2RequestProcessor#processNoCache()
        BEGIN org.seasar.struts.processor.S2RequestProcessor#processPreprocess()
        END org.seasar.struts.processor.S2RequestProcessor#processPreprocess()
        BEGIN org.seasar.struts.processor.S2RequestProcessor#processCachedMessages()
        END org.seasar.struts.processor.S2RequestProcessor#processCachedMessages()
        BEGIN org.seasar.struts.processor.S2RequestProcessor#processMapping()
        END org.seasar.struts.processor.S2RequestProcessor#processMapping()
        BEGIN org.seasar.struts.processor.S2RequestProcessor#processRoles()
        END org.seasar.struts.processor.S2RequestProcessor#processRoles()
        BEGIN org.seasar.struts.processor.S2RequestProcessor#processInputValueFormCreate()
            BEGIN org.seasar.struts.processor.S2RequestProcessor#processPopulate()
            END org.seasar.struts.processor.S2RequestProcessor#processPopulate()
        END org.seasar.struts.processor.S2RequestProcessor#processInputValueFormCreate()
        BEGIN org.seasar.struts.processor.S2RequestProcessor#processValidate()
            BEGIN org.seasar.struts.processor.S2RequestProcessor#processStrutsValidate()
            END org.seasar.struts.processor.S2RequestProcessor#processStrutsValidate()
            BEGIN org.seasar.struts.processor.S2RequestProcessor#getModuleConfig()
            END org.seasar.struts.processor.S2RequestProcessor#getModuleConfig()
            BEGIN org.seasar.struts.processor.S2RequestProcessor#getActionServlet()
            END org.seasar.struts.processor.S2RequestProcessor#getActionServlet()
            BEGIN org.seasar.struts.processor.S2RequestProcessor#processStrutsValidate()
            END org.seasar.struts.processor.S2RequestProcessor#processStrutsValidate()
        END org.seasar.struts.processor.S2RequestProcessor#processValidate()
        BEGIN org.seasar.struts.processor.S2RequestProcessor#processInputValueFormDelete()
        END org.seasar.struts.processor.S2RequestProcessor#processInputValueFormDelete()
        BEGIN org.seasar.struts.processor.S2RequestProcessor#getModuleConfig()
        END org.seasar.struts.processor.S2RequestProcessor#getModuleConfig()
        BEGIN org.seasar.struts.processor.S2RequestProcessor#getActionServlet()
        END org.seasar.struts.processor.S2RequestProcessor#getActionServlet()
        BEGIN org.seasar.struts.processor.S2RequestProcessor#processPopulate()
        END org.seasar.struts.processor.S2RequestProcessor#processPopulate()
        BEGIN org.seasar.struts.processor.S2RequestProcessor#processForward()
        END org.seasar.struts.processor.S2RequestProcessor#processForward()
        BEGIN org.seasar.struts.processor.S2RequestProcessor#processInclude()
        END org.seasar.struts.processor.S2RequestProcessor#processInclude()

AcceptorImplの役割はS2RequestProcessorにたくさんあるメソッド群のFacadeみたいなものだと思えばいいのかな?
いろいろメソッドを実行してますが、まあ割りと素直に読めばいいのかと。順番に色々やってるわけですね。
pathの取得やActionMappingの取得など色々あるので、この辺りをいじることで、やりたいことが実現できそうです。

次。

        BEGIN org.seasar.struts.processor.S2RequestProcessor#getActionInstance()
            BEGIN org.seasar.struts.action.ActionFactoryImpl#getActionInstance()
            END org.seasar.struts.action.ActionFactoryImpl#getActionInstance()
        END org.seasar.struts.processor.S2RequestProcessor#getActionInstance()

Actionインスタンスを取得してますね。
ActionFactoryImpl#getActionInstance()の処理内容がちょっとわかりにくかったです。

    public Object getActionInstance(HttpServletRequest request, HttpServletResponse response, ActionMapping mapping,
            Log log, MessageResources internal, ActionServlet servlet) throws IOException {
        Object actionInstance = null;
        S2Container container = SingletonS2ContainerFactory.getContainer();
        try {
            if (isCreateActionWithComponentName(mapping)) { // --[1]
                ComponentNameCreator componentNameCreator = getComponentNameCreator();
                String componentName = componentNameCreator.createComponentName(container, mapping);
                actionInstance = getActionWithComponentName(componentName, servlet);
            } else { // --[2]
                String actionClassName = mapping.getType();
                if (log.isDebugEnabled()) {
                    log.debug(" Looking for Action instance for class " + actionClassName);
                }
                Class componentKey = classRegister.getClass(actionClassName);
                actionInstance = container.getComponent(componentKey);
            }
        } catch (Exception e) {
            processExceptionActionCreate(container.getResponse(), mapping, log, internal, e);
            return null;
        }
        if (actionInstance instanceof Action) {
            setServlet((Action) actionInstance, servlet);
        }
        return actionInstance;
    }

[1]は、mapping内のtype,forward,includeがnullである場合にこの条件にはまるようです。
でも、トレースログをみるとtraceInterceptorがかかってるはずのComponentNameCreatorのログが出てないので、このログをとったときの実行時には[2]の方を通っているようです。
[2]ではクラス名をキーにClassRegisterからClassを取得して、S2ContainerからActionインスタンスを取得しています。無設定Strutsで自動登録した場合はこっち側を通るみたいです。AutoRegist〜の中で少なくともActionMapping#typeを指定しているのでそういうことになるんでしょうね。
じゃ、[1]って何のときに使うんでしょ?以前からある「struts-config内に記述しているactionの、type属性を記述せずにActionクラスを指定する」という機能用でしょうか。
まあ、これはおいておきましょう。

とにかく、リクエスト(path)に対応するActionインスタンスの選択処理はここで行われている、ということに注目しておきます。
未登録なpathに対するリクエストを登録済みのアクションのいずれかに振り替えるという処理は、ここの差し替えでも実現できそうです。

で、続きです。

        BEGIN org.seasar.struts.processor.S2RequestProcessor#processActionExecute()
            BEGIN org.seasar.struts.ActionExecuteProcessorImpl#processActionExecute()
                BEGIN org.seasar.struts.ActionExecuteProcessorImpl#processActionExecute()
                    BEGIN hoge.web.action.impl.HelloActionImpl#execute()
                    END hoge.web.action.impl.HelloActionImpl#execute()
                END org.seasar.struts.ActionExecuteProcessorImpl#processActionExecute()
            END org.seasar.struts.ActionExecuteProcessorImpl#processActionExecute()
        END org.seasar.struts.processor.S2RequestProcessor#processActionExecute()

さっき取得されたHelloActionImplが実行されています。アクションの実行はActionExecuteProcessorImplが担当するようです。

ポイントは、ActionExecuteProcessorImpl#processActionExecute()の中で

        BindingUtil.importProperties(action, container, beanDesc, mapping);
        forward = execute(request, actionInterface, action, mapping);
        BindingUtil.exportProperties(action, container, beanDesc, mapping);

        if (forward != null) {
            return mapping.findForward(forward);
        } else {
            return null;
        }
    }

のようにActionForwardをreturnするところ。
リクエスト時のpathによって、実行アクションに設定されているviewファイル以外のviewファイルを表示したい、という差し替え処理はここの置き換えでも実現できそうです。

        BEGIN org.seasar.struts.processor.S2RequestProcessor#processSetPath()
        END org.seasar.struts.processor.S2RequestProcessor#processSetPath()
        BEGIN org.seasar.struts.processor.S2RequestProcessor#processForwardConfig()
            BEGIN org.seasar.struts.processor.S2RequestProcessor#doForward()
            END org.seasar.struts.processor.S2RequestProcessor#doForward()
        END org.seasar.struts.processor.S2RequestProcessor#processForwardConfig()
    END org.seasar.struts.processor.AcceptorImpl#process()
END org.seasar.struts.processor.S2RequestProcessor#process()

アクション実行結果のActionForwardにしたがってフォワード処理を行うわけですが、この辺りの差し替えでもよさそうですね。

(A)(B)の目的を達成するために!

できるだけ修正箇所は小さいほうが好ましいです。
無理やり感も少ないほうがいいです。
という方針で考えるて、とりあえず2つの案を考えました。

Acceptor実装クラスを独自実装と差し替え
  • メリット
    • 1つのクラスを作るだけで(A)(B)が実現できそう
  • デメリット
    • 結構単調な呼び出しだけど、結構長い。一部の処理を書き換えるために全部をコピペするのってスマートじゃないよな…
S2RequestProcessorのメソッドに対するAOP
  • メリット
    • Interceptorを2つ実装するだけで良い
    • 必要な箇所だけをピンポイントで対処できる
  • デメリット
    • これってAOPで実現すべきようなcross-cutting concern?


眠くてあまり頭も働かないので、とりあえず後者でやってみようかと思います。
これでできたらいいなぁ。


[10/21追記]

  • S2RequestProcessorのサブクラスを実装しメソッドをオーバライドする

というのがStrutsの設計が推奨する対処方法な気がしてきました。
とりあえずはAOP方式でActionのデフォルト解決はできたんですが、ActionFowardはちょっと難しい(面倒)ですね。