クラスローダーがたくさんあるような状況は余りないのですが、沢山あることも多くあり、そうするといろいろ気になることがあります。
クラスローダーが異なる場合同一パッケージのパッケージプライベートへのアクセスがIllegalAccessExceptionもしくはIllegalAccessErrorとなってしまうので要注意です。
発生した時びっくりしたので自分でクラスローダーを作って検証してみた。
まず、適当なクラスローダーを作ってみる。
これを参考に、指定のディレクトリ配下をクラスパスとして読み込むクラスローダーを作る。
lalhaの例だとデフォルトパッケージのものしか読み込めなかったのとローダーの親子関係をもてなかったのでそこを追加。
jp.co.gara.classloader.DirectoryClassLoader
package jp.co.gara.classloader; import java.io.BufferedInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; /** * @author Garapon 2010/04/06 */ public class DirectoryClassLoader extends ClassLoader { private static final int BUFFER_SIZE = 1024; private final String targetDirectory; DirectoryClassLoader(String targetDirectory) { if (targetDirectory == null) { throw new IllegalArgumentException("TargetDirectory should not be null."); } if (targetDirectory.equals("")) { throw new IllegalArgumentException("TargetDirectory should not be blank."); } this.targetDirectory = targetDirectory; } DirectoryClassLoader(ClassLoader parent, String targetDirectory) { super(parent); if (targetDirectory == null) { throw new IllegalArgumentException("TargetDirectory should not be null."); } if (targetDirectory.equals("")) { throw new IllegalArgumentException("TargetDirectory should not be blank."); } this.targetDirectory = targetDirectory; } @SuppressWarnings("unchecked") protected Class findClass(String name) throws ClassNotFoundException { System.out.println(this + " >>> " + name); try { byte[] data = read(new File(targetDirectory, name.replaceAll("\\.", "/") + ".class")); return defineClass(name, data, 0, data.length); } catch (Throwable t) { throw new ClassNotFoundException(name, t); } } private static byte[] read(File file) throws IOException { InputStream in = null; try { in = new BufferedInputStream(new FileInputStream(file), BUFFER_SIZE); ByteArrayOutputStream out = new ByteArrayOutputStream(); byte[] buf = new byte[BUFFER_SIZE]; for (int readBytes = in.read(buf); readBytes != -1; readBytes = in.read(buf)) { out.write(buf, 0, readBytes); } return out.toByteArray(); } finally { if (in != null) in.close(); } } }
呼び出しクラスと呼ばれるクラスの用意
呼ぶ人はこんな感じ。
package jp.co.gara.classloader.target; /** * @author Garapon 2010/04/06 */ public class Caller { @SuppressWarnings("unchecked") public static void createTargetObject() throws Exception { System.out.println("これからTargetObjectをNewします。"); new TargetObject(); System.out.println("できたー"); System.out.println("これからTargetObjectBをNewします。"); new TargetObjectB(); System.out.println("できたー"); System.out.println("これからTargetObjectCをNewします。"); //java.lang.IllegalAccessExceptionwを起す場合はリフレクションでコール(もしくはNavie Methodからコールするとよい) Class target = Class.forName("jp.co.gara.classloader.target.TargetObjectC"); target.newInstance(); //java.lang.IllegalAccessErrorを起すならそのままNewする new TargetObjectC(); System.out.println("できたー"); } }
呼び出される側はこんな感じ
AとBは同一パッケージでコンストラクタはパブリック。Cは同一パッケージでコンストラクタがパッケージプライベート。
普通同一のクラスローダー配下にあれば全部正しくNewできる構成です。
package jp.co.gara.classloader.target; public class TargetObject { public TargetObject() { System.out.println("上手にTargetObjectをnew出来ました"); } }
package jp.co.gara.classloader.target; public class TargetObjectB { public TargetObjectB() { System.out.println("上手にTargetObjectBをnew出来ました"); } }
package jp.co.gara.classloader.target; public class TargetObjectC { TargetObjectC() { System.out.println("上手にTargetObjectCをnew出来ました"); } }
複数のクラスローダーに別けてクラスを読み込ませる
適当にコンパイルしたらクラスを以下のように配置する。
C:temp ├─0 │ └─jp │ └─co │ └─gara │ └─classloader │ DirectoryClassLoader.class │ TestDriver.class │ ├─1 │ └─jp │ └─co │ └─gara │ └─classloader │ └─target │ TargetObjectB.class │ TargetObjectC.class │ ├─2 └─jp └─co └─gara └─classloader └─target Caller.class TargetObject.class
テストドライバーの作成
上記のような配置のクラスを良い感じに読み込むようにテストドライバーを作る
package jp.co.gara.classloader; /** * @author Garapon 2010/04/06 */ public class TestDriver { public static void main(String[] args) throws Exception { // c:\temp\0 内のクラスを読み込むクラスローダー (親クラスローダー) ClassLoader loader0 = new DirectoryClassLoader("c:\\temp\\0"); // c:\temp\1 内のクラスを読み込むクラスローダー (子供クラスローダー) // TargetObjectBはここに含まれる。 // Callerと同じパッケージだけど別のクラスローダーに含まれる。 ClassLoader loader1 = new DirectoryClassLoader(loader0,"c:\\temp\\1"); // c:\temp\2 内のクラスを読み込むクラスローダー (孫クラスローダー) // CallerとTargetObjectはここに含まれる。 ClassLoader loader2 = new DirectoryClassLoader(loader1,"c:\\temp\\2"); System.out.println("loader0 = " + loader0); System.out.println("loader1 = " + loader1); System.out.println("loader2 = " + loader2); Class.forName("jp.co.gara.classloader.target.Caller", true, loader2).getMethod("createTargetObject").invoke(null, new Object[0]); } }
さて動かしてみる
C:\temp\0>java jp.co.gara.classloader.TestDriver loader0 = jp.co.gara.classloader.DirectoryClassLoader@1a758cb loader1 = jp.co.gara.classloader.DirectoryClassLoader@1b67f74 loader2 = jp.co.gara.classloader.DirectoryClassLoader@69b332 START jp.co.gara.classloader.DirectoryClassLoader@1a758cb >>> jp.co.gara.classloader.target.Caller jp.co.gara.classloader.DirectoryClassLoader@1b67f74 >>> jp.co.gara.classloader.target.Caller jp.co.gara.classloader.DirectoryClassLoader@69b332 >>> jp.co.gara.classloader.target.Caller これからTargetObjectをNewします。 jp.co.gara.classloader.DirectoryClassLoader@1a758cb >>> jp.co.gara.classloader.target.TargetObject jp.co.gara.classloader.DirectoryClassLoader@1b67f74 >>> jp.co.gara.classloader.target.TargetObject jp.co.gara.classloader.DirectoryClassLoader@69b332 >>> jp.co.gara.classloader.target.TargetObject 上手にTargetObjectをnew出来ました できたー これからTargetObjectBをNewします。 jp.co.gara.classloader.DirectoryClassLoader@1a758cb >>> jp.co.gara.classloader.target.TargetObjectB jp.co.gara.classloader.DirectoryClassLoader@1b67f74 >>> jp.co.gara.classloader.target.TargetObjectB 上手にTargetObjectBをnew出来ました できたー これからTargetObjectCをNewします。 jp.co.gara.classloader.DirectoryClassLoader@1a758cb >>> jp.co.gara.classloader.target.TargetObjectC jp.co.gara.classloader.DirectoryClassLoader@1b67f74 >>> jp.co.gara.classloader.target.TargetObjectC Exception in thread "main" java.lang.reflect.InvocationTargetException at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(Unknown Source) at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source) at java.lang.reflect.Method.invoke(Unknown Source) at jp.co.gara.classloader.TestDriver.main(TestDriver.java:36) Caused by: java.lang.IllegalAccessException: Class jp.co.gara.classloader.target.Caller can not access a member of class jp.co.gara.classloader.target.TargetObjectC with modifiers "" at sun.reflect.Reflection.ensureMemberAccess(Unknown Source) at java.lang.Class.newInstance0(Unknown Source) at java.lang.Class.newInstance(Unknown Source) at jp.co.gara.classloader.target.Caller.createTargetObject(Caller.java:23) ... 5 more
おお!起きた!!
同一パッケージのクラスなのにnewInstance()しようとしてIllegalAccessExceptionが発生していますね。
TargetObjectCを探す時にloader0 ⇒ loader1 と検索してloader1で見つけたのでインスタンスを作成しようとしましたが、クラスローダーが異なるのでエラーとなる。
ちなみにloader2がTargetObjectCをクラスパス内に持っていても同様のエラーとなります。(親が優先される為)
この状況を回避するにはクラスローダーの順番を変更するか呼び出されるクラス呼び出し元と同じクラスローダーに含める必要があります。
尚、リフレクションではなく通常の new すればIllegalAccessErrorが発生します。
補足java.lang.IllegalAccessErrorとjava.lang.IllegalAccessExceptionの違い
- java.lang.IllegalAccessError
アクセスできないフィールドへのアクセスや変更、あるいはアクセスできないメソッドの呼出しをアプリケーションが試みた場合にスローされます。
- java.lang.IllegalAccessException
アプリケーションがクラスをロードしようとしたとき、そのクラスが public でなかったり、別のパッケージに入っていたりするために、実行中のメソッドが指定されたクラスの定義にアクセスできない場合にスローされる例外です。