ReloadingClassLoaderがクラスファイル読んでくれないことがある件

はじめに

 org.apache.wicket.protocol.http.ReloadingWicketFilterを使うと、クラスファイルの変更を監視してくれます。
 そしてReloadingWicketFilterを使用した際に使われるクラスローダがorg.apache.wicket.application.ReloadingClassLoaderです。
 で、ReloadingClassLoaderはクラスファイルのパスに空白があると、ファイルを監視対象にしてくれないのです。

環境

実験方法

  1. eclipseのworkspaceを空白が含まれるパスに配置します。
  2. wicketで何か書きます。
  3. ReloadingWicketFilterを継承したフィルタを作ります。
  4. web.xmlを編集して作成したフィルタを使用するようにします。
  5. log4j.propertiesにDEBUGレベルを指定します。
  6. eclipse上からTomcatを起動してログを観察します。

log4j.propertiesはたとえば以下のような感じ。冗長な気がしますが、まあ、それはそれということで。

log4j.appender.Stdout=org.apache.log4j.ConsoleAppender
log4j.appender.Stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.Stdout.layout.conversionPattern=%-5p - %-26.26c{1} - %m\n

log4j.rootLogger=DEBUG,Stdout

log4j.logger.org.apache.wicket=DEBUG
log4j.logger.org.apache.wicket.protocol.http.HttpSessionStore=DEBUG
log4j.logger.org.apache.wicket.version=DEBUG
log4j.logger.org.apache.wicket.RequestCycle=DEBUG

以下はReloadingWicketFilterを継承したフィルタの例

public class MyWicketFilter extends ReloadingWicketFilter {
    static
    {
        ReloadingClassLoader.includePattern("wicketReloading.*");
    }
}

実験結果

 ログを観察すると、どうやら読み込みに失敗していることが分かります。

DEBUG - ReloadingClassLoader       - clzLocation=/C:/Documents%20and%20Settings/seko/workspace/wicketReloading/WEB-INF/classes/wicketReloading/WicketApplication.class
DEBUG - ReloadingClassLoader       - Class file does not exist: C:\Documents%20and%20Settings\seko\workspace\wicketReloading\WEB-INF\classes\wicketReloading\WicketApplication.class
DEBUG - ReloadingClassLoader       - clzLocation=/C:/eclipse3/servers/tomcat-5.5/shared/classes/wicketReloading/WicketApplication.class
DEBUG - ReloadingClassLoader       - Class file does not exist: C:\eclipse3\servers\tomcat-5.5\shared\classes\wicketReloading\WicketApplication.class
DEBUG - ReloadingClassLoader       - clzLocation=/C:/eclipse3/servers/tomcat-5.5/common/classes/wicketReloading/WicketApplication.class
DEBUG - ReloadingClassLoader       - Class file does not exist: C:\eclipse3\servers\tomcat-5.5\common\classes\wicketReloading\WicketApplication.class
DEBUG - ReloadingClassLoader       - Could not locate class wicketReloading.WicketApplication

 ちゃんとクラスファイルを監視対象に出来た場合のログは、以下のような感じ。

DEBUG - ReloadingClassLoader       - clzLocation=/C:/eclipse3/servers/tomcat-5.5/shared/classes/wicketReloading/WicketApplication.class
DEBUG - ReloadingClassLoader       - Class file does not exist: C:\eclipse3\servers\tomcat-5.5\shared\classes\wicketReloading\WicketApplication.class
DEBUG - ReloadingClassLoader       - clzLocation=/C:/workspace/wicketReloading/WEB-INF/classes/wicketReloading/WicketApplication.class
INFO  - ReloadingClassLoader       - Watching changes of class C:\workspace\wicketReloading\WEB-INF\classes\wicketReloading\WicketApplication.class

でも、ReloadingClassLoaderがクラスファイルを監視対象にすることに失敗してもアプリは動きます

 ReloadingClassLoader.loadClassメソッドは以下のようになっております。
 クラスファイルを監視対象に出来なかった場合は、watchForModificationsメソッドが実行されなかった場合、またはwatchForModificationsメソッドの実行中にクラスファイルを発見できなかった場合です。
 で、クラスローダがファイルを読み込む方法とwatchForModificationsメソッドがファイルを読み込む方法は異なるようで、クラスファイルを読み込めてもwatchForModificationsメソッドはファイルを発見できなかったりするという。

    public final Class loadClass(String name, boolean resolve) throws ClassNotFoundException
    {
        // First check if it's already loaded
        Class clazz = findLoadedClass(name);

        if (clazz == null)
        {
            final ClassLoader parent = getParent();

            if (tryClassHere(name))
            {
                try
                {
                    clazz = findClass(name);
                    watchForModifications(clazz);
                }
                catch (ClassNotFoundException cnfe)
                {
                    if (parent == null)
                    {
                        // Propagate exception
                        throw cnfe;
                    }
                }
            }

            if (clazz == null)
            {
                if (parent == null)
                {
                    throw new ClassNotFoundException(name);
                }
                else
                {
                    // Will throw a CFNE if not found in parent
                    clazz = parent.loadClass(name);
                }
            }
        }

        if (resolve)
        {
            resolveClass(clazz);
        }

        return clazz;
    }

 では、workspaseを示すパスに空白が入っている場合、ソースをコンパイルするたびにTomcatの再起動が必要かというと実はそうでもなくて。
 Tomcatの場合、クラスローダが更新を認識して読み込んでくれたりすることもあるようです。どういう状況下で読み込まれるのかは調べてません。