クラスローダーではまったメモ †
先日書いたFizzBuzz用プログラムで、「あるパッケージ内の、Functionというクラスを継承したクラスをすべて取得したい」という事をやりたくなった。該当箇所はこれ Functions.java (←このコード自体は改修済みなので意図通り動きます。)
ここでつまずいた。クラスを動的にロードしたときに、読み込んだクラスが継承元の親クラスと"is a"関係がなりたっていない。アップキャストは失敗するし、その他チェックメソッドも失敗する。テストコードを書いてみた。HogeInterfaceという空インターフェイス、AbstractHogeという空クラス、それらを実装したHogeImplというクラスがあると思いねえ。
public class TestMain {
public static void main(final String[] args) {
try {
final Class<?> clazz = ClassLoader.getSystemClassLoader().loadClass("jp.hutcraft.test.HogeImpl");
System.out.println("check clazz is a AbstractHoge "+ isA(clazz, AbstractHoge.class));
System.out.println("check AbstractHoge is isAssignableFrom clazz "+ AbstractHoge.class.isAssignableFrom(clazz));
System.out.println("check HogeInterface is isAssignableFrom clazz "+ HogeInterface.class.isAssignableFrom(clazz));
System.out.println("check HogeImpl is a AbstractHoge "+ isA(HogeImpl.class, AbstractHoge.class));
System.out.println("check AbstractHoge is isAssignableFrom HogeImpl "+ AbstractHoge.class.isAssignableFrom(HogeImpl.class));
System.out.println("check HogeInterface is isAssignableFrom HogeImpl "+ HogeInterface.class.isAssignableFrom(HogeImpl.class));
try {
final HogeInterface hogeInterface = (HogeInterface)clazz.newInstance();
System.out.println("cast to HogeInstance was success");
} catch (final InstantiationException e) {
e.printStackTrace();
} catch (final IllegalAccessException e) {
e.printStackTrace();
}
} catch (final ClassNotFoundException e) {
e.printStackTrace();
}
}
private static boolean isA(final Class<?>A, final Class<?>B) {
if (A.equals(B)) return true;
if (A.equals(Object.class)) return false;
return isA(A.getSuperclass(), B);
}
}
問題は、このコードをEclipseから実行すると成功するけどJarから実行すると意図通り動かない。
// Eclipseでの実行結果 → All OK
check clazz is a AbstractHoge true
check AbstractHoge is isAssignableFrom clazz true
check HogeInterface is isAssignableFrom clazz true
check HogeImpl is a AbstractHoge true
check AbstractHoge is isAssignableFrom HogeImpl true
check HogeInterface is isAssignableFrom HogeImpl true
cast to HogeInstance was success
// Jarでの実行結果 → 動的ロードクラスとの継承関係比較に失敗、キャストも失敗
> java -jar cltest.jar
check clazz is a AbstractHoge false
check AbstractHoge is isAssignableFrom clazz false
check HogeInterface is isAssignableFrom clazz false
check HogeImpl is a AbstractHoge true
check AbstractHoge is isAssignableFrom HogeImpl true
check HogeInterface is isAssignableFrom HogeImpl true
Exception in thread "main" java.lang.reflect.InvocationTargetException
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
at java.lang.reflect.Method.invoke(Method.java:597)
at org.eclipse.jdt.internal.jarinjarloader.JarRsrcLoader.main(JarRsrcLoader.java:58)
Caused by: java.lang.ClassCastException: jp.hutcraft.test.HogeImpl cannot be cast to jp.hutcraft.test.HogeInterface
at jp.hutcraft.test.TestMain.main(TestMain.java:16)
... 5 more
Eclipseから実行するというのはClasspathにクラスファイルが展開されている状態で、Jarでの実行はクラスがJar内に格納されているという違いがある。Classpathに展開されているクラスファイルを精査するクラスローダーとJar内を捜査するクラスローダーが別物だからこういうことが起こるのだろうという助言をいただいて、ClassLoader.getSystemClassLoader().loadClass() を以下のように変更。
final Class<?> clazz = TestMain.class.getClassLoader().loadClass("jp.hutcraft.test.HogeImpl");
こうすることで、自分のクラスを読んでくれたのと同じクラスローダーに問い合わせることができたので、無事に解決。ただしお気付きの通り、Classpath内のクラスファイルとJar内のクラスファイルを混在させてこういうロードをさせようとするとやはり問題が残るので注意ですね。根本解決にはクラスローダーの詳しい知識がいりそうだなぁ。
この件で大きな助言をくれたがくぞー師匠に感謝!