読者です 読者をやめる 読者になる 読者になる

Develop with pleasure!

福岡でCloudとかBlockchainとか。

Seasar2のコンポーネント自動登録の際の順序

現在動作中のSeasar2TeedaのWebアプリケーションを別のサーバへ移し変えたところ、WARファイルの中身は全く替えていないのに、TeedaのLayout機能を利用して呼ばれるはずの、あるPageクラスのinitializeメソッドが呼ばれないという現象にハマッた。

全くアプリケーション自体には手を入れておらず、別のLinuxに移行しただけなのだが、動作しない…。環境依存?と思い、移行先のマシンに対してリモートデバッグを行う。

元々SpringユーザーでSeasar2Teeda自体にあまり詳しくないのでえらいデバッグに手間取る…。最初は、問題となる箇所が、TeedaExtensionなのかTeedaCoreなのか、それともS2Containerなのか中々掴めずえらい時間がかかったorz...。

で、どうも原因はS2ContainerのCoolDeploy時にコンポーネントを自動登録するCoolComponentAutoRegisterクラスのインナークラスでファイルシステム用のクラス走査を行うFileSystemStrategyクラスがあるのだが、このregisterAllメソッドが呼ばれると最終的にClassTraversalというクラスのtraverseFileSystemメソッドが呼ばれるが、この中で対象のクラスファイルを取得するために

    private static void traverseFileSystem(final File dir,
            final String packageName, final ClassHandler handler) {
        final File[] files = dir.listFiles();
        for (int i = 0; i < files.length; ++i) {
            final File file = files[i];
            final String fileName = file.getName();
            if (file.isDirectory()) {
                traverseFileSystem(file, ClassUtil.concatName(packageName,
                        fileName), handler);
            } else if (fileName.endsWith(".class")) {
                final String shortClassName = fileName.substring(0, fileName
                        .length()
                        - CLASS_SUFFIX.length());
                handler.processClass(packageName, shortClassName);
            }
        }
    }

と、JavaのFileクラスのlistFilesメソッドを使用している。この場合、listFilesメソッドが返すファイルの順序は特に規定が無いため、OSの環境によってファイルの順序が変わってしまう。

で、このファイルの順序が変わることでどうして現象が変わるのかというと、コンポーネントの登録時にS2ContainerImplのregisterByClassというメソッドがコールされるが

    protected void registerByClass(ComponentDef componentDef) {
        Class[] classes = S2ContainerUtil.getAssignableClasses(componentDef
                .getComponentClass());
        for (int i = 0; i < classes.length; ++i) {
            registerMap(classes[i], componentDef);
        }
    }

S2ContainerUtil#getAssignableClassesのメソッドを呼び出し、登録予定のコンポーネントの親クラス等のクラス情報を取得し、この親クラスもコンポーネントとして登録される。この際、registerMapメソッドで登録されるのだが、登録するコンポーネントのkeyはクラス名で、valueが登録予定のコンポーネントの情報を保持したComponentDefオブジェクトとなる。

そのため、例えばAPageというクラスとAPageを継承したBPageが存在したとして、コンポーネントの自動登録をする場合、

  • APageから先にコンポーネント登録される場合
    • key:APageのクラス名、value:APageのComponentDef
    • key:BPageのクラス名、value:BPageのComponentDef
  • BPageから先にコンポーネント登録される場合
    • key:BPageのクラス名、value:BPageのComponentDef
    • key:APageのクラス名、value:BPageのComponentDef


という風になり、どちらが先にコンポーネント登録されるかによって登録されるvalueが変わる。この結果、そのコンポーネント使用時に動作が変わってしまう。

TeedaのPageクラスの場合は、基本的に1ページ=1Pageクラスというルールに則れば上記の様なケースは起きないが、Serviceクラス等、コンポーネント登録されるクラス全般に影響が出るので、ちょっと厄介。まぁ、Service系のクラスも共通処理を抽象クラスにしておけば起きないんだろうけど。

んー、でも何で、S2ContainerImpl#registerByClassでは、S2ContainerUtil#getAssignableClassesを呼んで親クラスとかもコンポーネント登録してるんだろう?(Seasar初心者なので謎。)

とりあえず根本解決ではないが、環境によって動作が違うと困るので、アルファベット順に登録するようにする。(Windowsの場合、File#listFilesで帰ってくるのがアルファベット順になってたので。ちなみにUbuntu9.0.4とOracleEnterpriseLinux5ではアルファベット順にはならず…。)ClassTraversal#traverseFileSystemで取得したクラスのリスト配列を自然順序付けでソートすれば良い。

final File[] files = dir.listFiles();
Arrays.sort(files);

一行加えるだけ。ただ、ClassTraversalを直接さわるとSeasar自体に手を入れることになるので、CoolComponentAutoRegisterを拡張することに。CoolComponentAutoRegisterとFileSystemStrategyを拡張し、FileSystemStrategy#registerAllをオーバーライドし、上記修正を施したClassTraversalを拡張したクラスを呼ぶことにする。(ClassTraversal自体はfinal宣言されているので継承することはできず。)

後は、CoolComponentAutoRegisterの代わりに拡張したCoolComponentAutoRegisterExクラスがコンポーネントの自動登録機能として呼び出されれば良いので、cooldeploy-autoregister.diconを作成し、コンポーネントとして拡張したCoolComponentAutoRegisterExを指定すれば動作した。

本当はServiceクラスやPageクラスをちゃんとしたクラス構成を取るように修正する方が良いのだろうけど、もう保守が始まってるサービスで、結構巨大なので、今回はこういう形を取った…。

んー、もっとスマートな方法があるのかも。