クラスローダーが異なる場合、パッケージプライベートへのアクセスがIllegalAccessとなることがある。

クラスローダーがたくさんあるような状況は余りないのですが、沢山あることも多くあり、そうするといろいろ気になることがあります。
クラスローダーが異なる場合同一パッケージのパッケージプライベートへのアクセスが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 でなかったり、別のパッケージに入っていたりするために、実行中のメソッドが指定されたクラスの定義にアクセスできない場合にスローされる例外です。