実行時にクラスファイルを読み込むクラスローダー

Javaでは、クラスファイルのバイナリからClassオブジェクトを生成する処理がClassLoaderクラスで提供されているので、自前のクラスローダーを作るのはそんなに難しくない。
色々面白いことができそうなので、クラスファイルを読み込むクラスローダーを作ってみた。

package nat.champloo.classloader;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;

/**
 * クラスファイルを読み込むクラスローダー 
 * @author NAT
 */
public class FileClassLoader extends ClassLoader{
    private static final int BUF_SIZE = 1024;
    public Class loadClassFile(File classFile) throws IOException {
        FileInputStream in = new FileInputStream(classFile);
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        byte[] buf = new byte[BUF_SIZE];
        int len = in.read(buf);
        while(len != -1) {
            out.write(buf, 0, len);
            len = in.read(buf);
        }
        byte[] loadedData = out.toByteArray();
        Class loadedCLass = defineClass(null, loadedData, 0, loadedData.length);
        return loadedCLass;
    }
}

このFileClassLoaderクラスの使い方の説明も兼ねて、テストケースを以下に示します。

package nat.champloo.classloader;

import java.io.File;
import java.lang.reflect.Method;
import junit.framework.TestCase;

/**
 * FileClassLoaderのテストケース
 * @author NAT
 */
public class FileClassLoaderTest extends TestCase {
    public void testLoad() throws Exception {
        // クラスファイル"A.class"を読み込んで、クラスAのインスタンスを生成
        FileClassLoader loader = new FileClassLoader();
        Object a = loader.loadClassFile(new File("A.class")).newInstance();
        assertEquals("A", a.toString());
    }
    
    public void testFindLoadedClass() throws Exception {
        // 一回読み込んだクラスは、loadClass()でもう一度取得可能
        FileClassLoader loader = new FileClassLoader();
        Object a1 = loader.loadClassFile(new File("A.class")).newInstance();
        assertEquals("A", a1.toString());
        
        Object a2 = loader.loadClass("nat.champloo.classloader.A").newInstance();
        assertEquals("A", a2.toString());
        
        // a1とa2のClassは同じ
        assertSame(a1.getClass(), a2.getClass());
    }
    

    public void testInvokeMethodOfLoadedClass() throws Exception {
        // 読み込んだクラスのメソッドを呼ぶ
        // 読み込んだクラスAがさらにクラスAのインスタンスを生成するテストにもなっている
        FileClassLoader loader = new FileClassLoader();
        Object a = loader.loadClassFile(new File("A.class")).newInstance();
        Method createNewAMethod = a.getClass().getMethod("createNewA", new Class[0]);
        assertEquals("A", createNewAMethod.invoke(a, new Object[0]));
    }
    
    public void testCastToAFails() throws Exception {
        // このソース中のクラスAと、クラスファイル"A.class"のクラスAは
        // 別のクラスとして扱われるので、キャストに失敗する
        FileClassLoader loader = new FileClassLoader();
        boolean castFails = false;
        try {
            A a = (A)loader.loadClassFile(new File("A.class")).newInstance();
        } catch(ClassCastException e) {
            castFails = true;
        }
        assertTrue(castFails);
    }

    public void testDifferentClassA() throws Exception {
        // このソース中のクラスAと、クラスファイル"A.class"のクラスAが、
        // クラス名は同じだが、クラスの実装が異なることの確認
        
        Object newA = new A(); // クラスAのtoString()は"NewA"を返す
        
        FileClassLoader loader = new FileClassLoader();
        Object a = loader.loadClassFile(new File("A.class")).newInstance();
        
        // Classは異なる
        assertNotSame(newA.getClass(), a.getClass());
        
        // toString()の戻り値も異なる
        assertEquals("NewA", newA.toString());
        assertEquals("A", a.toString());
        
        // でもクラス名は同じ
        assertEquals(newA.getClass().getName(), a.getClass().getName());
    }
}

このテストケースを動かすには、以下のクラスAをコンパイルしたクラスファイル"A.class"も必要。テストケースを実行するときのカレントディレクトリ(eclipseのデフォルト設定ならプロジェクトを作ったディレクトリ)に、このクラスファイルを置いて下さい。

package nat.champloo.classloader;

/**
 * FileClassLoaderTestで使う、テスト用のクラス。
 * toString()を実装する。
 * @author NAT
 */
public class A {
    public String toString() {
        return "A";
    }
    public String createNewA() {
        return new A().toString();
    }
}

クラスファイルを置いたら、クラスAのtoString()メソッドを以下のように変更して下さい。

    public String toString() {
        return "NewA";
    }

ちょっと面白いというか混乱するのが、テストメソッドtestCastToAFails()とtestDifferentClassA()でやっているように、同じクラス名なのに異なるクラスとして扱われる2つのクラスが、1つのプログラム実行中に同時に存在すること。コード中に直接クラス名を書いているクラスAは、(一般的な実行の仕方なら)クラスパスからクラスファイルを探すシステムクラスローダーによってロードされたクラスで、クラスファイル"A.class"を指定して読み込んだクラスAは、FileClassLoaderによってロードされたクラスで、クラスをロードしたクラスローダーが異なれば、クラス名が同じでもクラスとしては別物になる。詳しいことは、JavaAPI仕様書のClassLoaderの説明あたりを参照して下さい。
ちなみにこの単純なクラスローダーの欠点は、クラスファイルを1つ1つ指定しないと複数のクラスを読み込めないこと。指定されたディレクトリやjarファイルなどからクラスファイルを探せたりできると立派なクラスローダーになるけど、色々と面倒なのでここまでにしときます。