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実行すればいけるはず。
word=this file exists in hoge.jar!!
word=this file doesn't exist in hoge.jar!!
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