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

ClassLoader#loadClassのよく分からない非互換?問題

Java

Javaは過去バージョンからの互換性を重視しており、まあ普通のコードを書いていれば滅多に互換性が問題になることはありません。リリースノートの互換性事項をチェックしていれば、普通は大丈夫です。

Oracle Technology Network for Java Developers
Java SE 7 and JDK 7 Compatibility

とはいえ、仕事中にClassLoader#loadClassの動作について変な非互換問題に引っかかった(有名どころの某社某製品がJava6以降で動かなくなる)のでメモ。

とりあえず互換もヘッタクレもない普通のコード。単にクラス名を指定してクラスをロードするだけです。

public class MainClass {
	public static void main(String[] args) {
		
		//クラス名を指定
		String className = "test.Hoge";
		try {
			//クラスをロード
			Class c = MainClass.class.getClassLoader().
				loadClass(className);
			//クラス名を表示
			System.out.println("Class Name: " + c.getName());
		} catch (ClassNotFoundException e) {
			e.printStackTrace();
		}
	}
}

で、問題のコードはコレ(JavaバグDB6434149より)。

public class MainClass2 {
	public static void main(String[] args) {
		
		//String配列でクラス名を指定
		String[] classNameArrayStr = {"test.Hoge"};
		//String配列シンタックスを取得
		String className = classNameArrayStr.getClass().getName();
		try {
			//クラスをロード
			Class c = MainClass2.class.getClassLoader().
				loadClass(className);
			System.out.println("Class Name: " + c.getName());
		} catch (ClassNotFoundException e) {
			e.printStackTrace();
		}
	}
}

自分で書いてて意味不明なコードですが、通常Stringで指定するクラス名をString配列シンタックスで指定してロードしています・・・・・何がしたいんだこのコード。ちなみにクラスをClass#newInstance()を使用してインスタンス化しようとすると、存在しないクラス名をインスタンス化しようとするため、当然その時点で例外が出ます。
また、上記コードはJava5までは動作しますが、Java6以降ではClassNotFoundExceptionがスローされます・・・いやまあごもっともな動作で。

java.lang.ClassNotFoundException: [Ljava.lang.String;
at java.net.URLClassLoader$1.run(URLClassLoader.java:202)
at java.security.AccessController.doPrivileged(Native Method)
at java.net.URLClassLoader.findClass(URLClassLoader.java:190)
at java.lang.ClassLoader.loadClass(ClassLoader.java:307)
at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:301)
at java.lang.ClassLoader.loadClass(ClassLoader.java:248)

普通に見ると障害修正のようにしか見えませんが、JavaバグDB6434149を見ると、投票数164と結構な数の投票がされています。調べきれていませんが、関連バグ報告を合わせると相当な数の投票がされているようです。
で、結局Sun(当時)はJava実行時に下記のシステムプロパティを付けることで、上記コードが例外を出さないようにする互換性スイッチを追加しています。

-Dsun.lang.ClassLoader.allowArraySyntax=true

Javaバージョンごとに動作をまとめると下記のようになります。

互換性スイッチ Java1.4(v1.4.2_13) Java5((v1.5.0_21) Java6(v1.6.0_21) Java7(v1.7.0.4)
無し × ×
有り

◯: 実行可能(ClassNotFoundExceptionはスローされず)
×: 実行不可能(ClassNotFoundExceptionがスローされる)
−: 互換性スイッチが存在しない



この現象が問題となってまともに動かなくなるフレームワーク等が存在するらしいのですが、そもそもそういうコードが存在する(インスタンス化出来ないクラスをロードする)ことが理解不能です。とはいえそういうコードが存在することも事実。古いJava環境をJava7に更新する際に"java.lang.ClassNotFoundException: [Ljava.lang.String;"(Ljava.lang.Stringに限らず)が発生したら、上記システムプロパティを付けてみることで解決する可能性があるので、試してみてはどうでしょうか。(でもインスタンス化するときにコケるクラスをロードするようなコードってどんな状況で必要になるんだろうか。やっぱり分からない。)