test実行時のリソースロードについて(検証編)

hibernateのコードを追いかけてみたところhibernate.cfg.xmlのロードが行われているのがorg.hibernate.util.ConfigHelper

	public static InputStream getResourceAsStream(String resource) {
		String stripped = resource.startsWith("/") ?
				resource.substring(1) : resource;

		InputStream stream = null;
		ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
		if (classLoader!=null) {
			stream = classLoader.getResourceAsStream( stripped );
		}
		if ( stream == null ) {
			stream = Environment.class.getResourceAsStream( resource );
		}
		if ( stream == null ) {
			stream = Environment.class.getClassLoader().getResourceAsStream( stripped );
		}
		if ( stream == null ) {
			throw new HibernateException( resource + " not found" );
		}
		return stream;
	}

という所でした。ClassLoaderの所がすげー怪しい気がしたんで簡単な検証プロジェクト作ってためしてみました。もしも試してみたい人がいれば(こんな所誰も来ないような気がしつつ)ここにおいておきますので。内容は以下のような感じなので、解凍してhoge-parentに移動してmvn installすれば実行できるはず。eclipseで動かす場合はmvn eclipse:eclipseしてからプロジェクトインポートしてtest実行すればいけるはず。

  • maven2のマルチモジュールで、親がhoge-parent。子供がhoge-jarとhoge
  • hoge-jarにsrc/main/resouces/hoge.propertiesを配備して内容が
word=this file exists in hoge.jar!!
  • hogeにsrc/test/resouces/hoge.propertiesを配備して内容が
word=this file doesn't exist in hoge.jar!!
  • hogeのsrc/test/java/org/ikoan/AppTest.javaに以下のメソッドを追加
	public void testLoadResource() throws Exception {
		Properties prop = new Properties();
		InputStream in = null;
		ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
		System.out.println("Classloader:" + classLoader.getClass().getName());
		try {
			String fileName = "hoge.properties";
			in = classLoader.getResourceAsStream(fileName);

			prop.load(in);
			String actual = prop.getProperty("word");
			System.out.println("actual:" + actual);
			String expected = "this file doesn't exist in hoge.jar!!";
			System.out.println("expected:" + expected);
			assertEquals(expected, actual);
		} finally {
			if (in != null)
				in.close();
		}
	}

で、実行した所eclipseだと

Classloader:sun.misc.Launcher$AppClassLoader
actual:this file doesn't exist in hoge.jar!!
expected:this file doesn't exist in hoge.jar!!

となって成功。mvn testだと

Classloader:org.apache.maven.surefire.booter.IsolatedClassLoader
actual:this file exists in hoge.jar!!
expected:this file doesn't exist in hoge.jar!!

となって失敗。なるほど、eclipseだとシステムクラスローダが使われているのに、mvn testだとmaven(というよりsurefire)内臓のクラスローダが使われるらしい、ということがわかりました。こいつが何か悪さしてそう…。じゃあどうしたらいいのよ?ってことでorg.apache.maven.surefire.booter.IsolatedClassLoaderだとかclassloader周りをキーにぐぐって見た所、どうやらmaven-surefire-pluginのオプションでuseSystemClassLoaderというのがバージョン2.3から導入されたらしいということがわかりました。http://maven.apache.org/plugins/maven-surefire-plugin/test-mojo.htmlに書いてありますね。というわけで早速さきほどのhogeプロジェクトのpomに設定してみます。

  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-surefire-plugin</artifactId>
        <configuration>
          <useSystemClassLoader>true</useSystemClassLoader>
        </configuration>
      </plugin>
    </plugins>
  </build>

これで再度mvn testすると…


失敗しました…orz

[INFO] Building jar: C:\DOCUME~1\Naoki\LOCALS~1\Temp\surefirebooter60176.jar
java.lang.NoClassDefFoundError: org/apache/maven/surefire/booter/SurefireBooter
Exception in thread "main"
[INFO] ------------------------------------------------------------------------
[ERROR] BUILD FAILURE
[INFO] ------------------------------------------------------------------------

NoClassDefFoundErrorですか…そうですか…。こういう時は-Xオプションつけて詳細な情報を参照するに限る!ということで

mvn -X test

してみると…こうなった。かいつまんでみると

中略
[DEBUG]   (f) useSystemClassLoader = true
中略
[DEBUG] Test Classpath :
[DEBUG]   C:\Docs\Eclipse\Workspaces\maven-parents\hoge-parent\hoge\target\classes
[DEBUG]   C:\Docs\Eclipse\Workspaces\maven-parents\hoge-parent\hoge\target\test-classes
[DEBUG]   C:\Resources\maven2\repository\junit\junit\3.8.1\junit-3.8.1.jar
[DEBUG]   C:\Docs\Eclipse\Workspaces\maven-parents\hoge-parent\hoge-jar\target\classes
中略
[INFO] Building jar: C:\DOCUME~1\Naoki\LOCALS~1\Temp\surefirebooter31539.jar
Forking command line: "C:\Program Files\Java\jdk1.5.0_05\jre\bin\java" -jar C:\DOCUME~1\Naoki\LOCALS~1\Temp\surefirebooter31539.jar C:\DOCUME~1\Naoki\LOCALS~1\Temp\surefire31537tmp C:\DOCUME~1\Naoki\LOCALS~1\Temp\surefire31538tmp
java.lang.NoClassDefFoundError: org/apache/maven/surefire/booter/SurefireBooter
Exception in thread "main" 
中略

わかることは、

  • useSystemClassLoaderオプションは有効になってるみたい。
  • hoge\target\test-classesにはクラスパス通ってるじゃん…
  • 一時的にsurefirebooter31539.jarっていうのを作って、それを実行しようとしてる。で、その時にorg/apache/maven/surefire/booter/SurefireBooterが無いって言われて落ちちゃってるらしい。

ということぐらいかなぁ。というわけでsurefireのバグなんじゃねーの?疑惑が出てきたのでIssue Tracking探してみたらそのものズバリな報告が上がってるじゃありませんか。

一旦Fixしたみたいだけど、バグが残ってた感じなのかな?うーむ。ここまで来るともうソースコード読むしかないですねー。というわけで既に読んでみて修正箇所も把握済みだったりします。先にdiffだけ晒してしまうと以下のような感じ。

Index: ForkConfiguration.java
===================================================================
--- ForkConfiguration.java	(revision 518438)
+++ ForkConfiguration.java	(working copy)
@@ -228,7 +228,10 @@
         for ( Iterator it = classPath.iterator(); it.hasNext(); )
         {
             String el = (String) it.next();
-            cp += " " + el + ( new File( el ).isDirectory() ? "/" : "" );
+            el = el.replaceAll(" ", "%20");
+            File f = new File( el );
+            
+            cp += " " + f.toURL() + ( f.isDirectory() ? "/" : "" );
         }
 
         Manifest.Attribute attr = new Manifest.Attribute( "Class-Path", cp.trim() );

詳しい解説は余力があったら後で解決編を書く…たぶん…。つかpatch投げたいけど英語どうやって書いたらいいのかわかりません…orz