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

この日記は私的なものであり所属会社の見解とは無関係です。 GitHub: takahashikzn

struts-2.3.7がやたら重い原因が判明



2013-01-15追記 struts-2.3.8では解消済みのようです。
https://issues.apache.org/jira/secure/ReleaseNote.jspa?projectId=12311041&version=12323480



最近、Struts-2.3.7がリリースされたわけですが、何故だかアプリの動作がやたら重くなる。
jarを更新した以外は何もしていないのに。


で、しょーがないので原因を調べてみた所、JBossFileManagerが犯人だということを突き止めました。

原因の解析

Struts2は、設定ファイルなどリロード対象となるリソースをFileManager経由でウォッチするわけですが、
どのFileManagerを使うか決定する処理に問題があります。以下の通り。

(DefaultFileManagerFactoryから抜粋)

    public FileManager getFileManager() {
        Set<String> names = container.getInstanceNames(FileManager.class);
        if (LOG.isDebugEnabled()) {
            LOG.debug("Found following implementations of FileManager interface: #0", names.toString());
        }
        Set<FileManager> internals = new HashSet<FileManager>();
        Set<FileManager> users = new HashSet<FileManager>();
        for (String fmName : names) {
            FileManager fm = container.getInstance(FileManager.class, fmName);
            if (fm.internal()) {
                internals.add(fm);
            } else {
                users.add(fm);
            }
        }
        for (FileManager fm : users) {
            if (fm.support()) { // 注意1
                if (LOG.isDebugEnabled()) {
                    LOG.debug("Using FileManager implementation [#0]", fm.getClass().getSimpleName());
                }
                fm.setReloadingConfigs(reloadingConfigs);
                return fm;
            }
        }
        if (LOG.isDebugEnabled()) {
            LOG.debug("No user defined FileManager, looking up for internal implementations!");
        }
        for (FileManager fm : internals) {
            if (fm.support()) { //注意2
                return fm;
            }
        }
        if (LOG.isDebugEnabled()) {
            LOG.debug("Using default implementation of FileManager provided under name [system]: #0", fileManager.getClass().getSimpleName());
        }
        fileManager.setReloadingConfigs(reloadingConfigs);
        return fileManager;
    }


このソースには2箇所、『注意』と書いてある場所がありますが、このFileManager#supportが問題。
どのFileManagerを使うか決定するのがココの処理の目的なので、すべてのFileManagerの実装に対してsupportが呼ばれます。


さて、JBossFileManager#supportの実装は次の通りです。

    @Override
    public boolean support() {
        boolean supports = isJBoss7() || isJBoss5();
        if (supports && LOG.isDebugEnabled()) {
            LOG.debug("JBoss server detected, Struts 2 will use [#0] to support file system operations!", JBossFileManager.class.getSimpleName());
        }
        return supports;
    }

    private boolean isJBoss5() {
        try {
            Class.forName(VFS_JBOSS5);
            return true;
        } catch (ClassNotFoundException e) {
            LOG.debug("Cannot load [#0] class, not a JBoss 5!", VFS_JBOSS7);
            return false;
        }
    }

    private boolean isJBoss7() {
        try {
            Class.forName(VFS_JBOSS7);
            return true;
        } catch (ClassNotFoundException e) {
            LOG.debug("Cannot load [#0] class, not a JBoss 7!", VFS_JBOSS7);
            return false;
        }
    }

見て分かる通り、Class#forNameでJBoss由来のクラスがロードできるか否かで判定しています。


ま、この程度ならよくある実装パターンですが、このメソッド、何とStrutsのタグを1つ実行する度に呼ばれるようです。
つまりタグを1つ実行する度に例外が発生することになり、そして例外の生成は非常に遅い処理です。
だから全体として非常に処理が重くなるわけ。

回避策

回避策としては、FileManagerの実装を決定する処理が1回だけ行われるようにすれば良いはず。
従って、先ずは次のようなクラスを用意します。

package foo;

import com.opensymphony.xwork2.FileManager;
import com.opensymphony.xwork2.util.fs.DefaultFileManagerFactory;

public class MyFileManagerFactory
    extends DefaultFileManagerFactory {

    private FileManager cache;


    @Override
    public FileManager getFileManager() {
        if (this.cache == null) {
            this.cache = super.getFileManager();
        }

        return this.cache;
    }
}

見て分かる通り、FileManagerの決定結果をキャッシュするだけです。
次に、struts.xmlに次のエントリを追加します。

<constant name="struts.fileManagerFactory" value="foo.MyFileManagerFactory" />

以上です。僕の環境ではこれで問題が解消しました。