【概要】
クラスローダーを自作する方法の紹介。クラスローダーの自作が有効なケースとして代表的なものは次の通り。

・同一 FQN で異なるクラスを扱いたい(複数バージョンのライブラリを同一 JVM 上で取り扱いたいときなど)
・クラスの定義を Jar ファイルからの読み込み / HTTP からのクラス定義のロードなどの標準的な方法ではなく、独自の方法で動的にロードできるようにしたい。

【キーワード】
クラスローダー、ClassLoader、自作、つくり方

【詳細】
1. DirectoryClassLoader の作成

今回はクラスローダー作成のはじめの一歩として、極めてシンプルで原始的、かつ不完全なクラスローダーを作成する。具体的には指定されたディレクトリに格納されているクラスファイルを動的にロードするだけの DirectoryClassLoader を作成する。

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

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;
  }
  
  protected Class findClass(String name) throws ClassNotFoundException {
    try {
      byte[] data = read(new File(targetDirectory, name + ".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();
    }
  }
}

2. HelloWorld クラスの作成

次に、2種類の HelloWorld クラスを作成する。

public class HelloWorld {
  public static void shout() {
    // 小文字でメッセージを出力
    System.out.println("hello, world!");
  }
}
public class HelloWorld {
  public static void shout() {
    // 大文字でメッセージを出力
    System.out.println("HELLO, WORLD!");
  }
}

2種類の HelloWorld クラスの作成が終わったら、それぞれコンパイルし、生成された class ファイルを別々のディレクトリに置く。ここでは例として、c:\1 と c:\2 に置く。

3. テストプログラムの作成と実行

public class Test {
  public static void main(String[] args) {
    // c:\\1 内のクラスを読み込むクラスローダー
    ClassLoader loader1 = new DirectoryClassLoader("c:\\1");

    // c:\\2 内のクラスを読み込むクラスローダー
    ClassLoader loader2 = new DirectoryClassLoader("c:\\2");
    
    Class.forName("HelloWorld", true, loader1)
      .getMethod("shout")
      .invoke(null, new Object[0]);
    Class.forName("HelloWorld", true, loader2)
      .getMethod("shout")
      .invoke(null, new Object[0]);
  }
}
[実行結果]
hello, world!
HELLO, WORLD!

4. 補足

冒頭に書いたように、DirectoryClassLoader はこの状態では指定されたディレクトリ直下のクラスファイルを動的に読み込めるだけなので、不完全である。が、基本的にこの要領で必要に応じてメソッドをオーバーライドしていけば、完全な形のクラスローダーを作成することができる。また、シリアライズを使用したディープコピーと併用すると注意しなければならない点があるので、そちらはシリアライズを利用したディープコピーを参照のこと。