開発者が独自のクラス・ローダーを作成する理由の1つに、JVMのクラス・ローディング動作を制御するという目的があります。 Javaでは、クラスはそのパッケージ名とクラス名によって識別されますが、 java.io.Serializableを実装しているクラスの場合、serialVersionUIDがクラスをバージョニングするおもな役割を果たします。 このストリーム固有識別子は、クラス名、インタフェース・クラス名、メソッド、フィールドに対する64ビットのハッシュです。 これ以外に、簡単にクラスをバージョニングできるメカニズムは存在しません。 技術的に見ると、上述の要素が一致した場合、クラスは"同じバージョン"です。
ここで、特定のインタフェースを実装した任意のタスクを実行する機能を備えた、汎用の実行エンジンを開発するシナリオについて考えてみましょう。 タスクがエンジンに送信されると、はじめにエンジンはそのタスクのコードをロードする必要があります。 さまざまなクライアントからさまざまなタスク(異なるコード)がエンジンに送信されるとすると、偶然に、これらすべてのタスクで同じクラス名とパッケージ名が使用される可能性もあります。 ここで疑問になるのは、このエンジンが、さまざまなクライアントの起動コンテキストに対して、異なるタスクのクライアント・バージョンを別々にロードした結果、クライアントに期待される出力を返すのかどうか、という点です。 この現象は、後半の参考資料セクションからダウンロードできるサンプル・コードで実証されています。 samepathとdifferentversionsという2つのディレクトリには、この概念を実証するための例がそれぞれ別々に含まれています。
この例が3つの別個のサブフォルダ(samepath、differentversions、differentversionspush)内にどのように配置されているかを、図2に示します。
図2. サンプル・フォルダの構造配置
samepathでは、version.Versionクラスがv1とv2という2つのサブディレクトリに含まれています。 両方のクラスに対して、同じ名前とパッケージが指定されています。 この2つのクラス間にある唯一の違いは、次の3行です。
public void fx(){
log("this = " + this + "; Version.fx(1).");
}
v1ではログ・ステートメントにVersion.fx(1)が含まれるのに対し、v2ではVersion.fx(2)が含まれています。 わずかに異なるこれらのクラス・バージョンを同じクラスパスに置き、Testクラスを実行します。
set CLASSPATH=.;%CURRENT_ROOT%\v1;%CURRENT_ROOT%\v2 %JAVA_HOME%\bin\java Test
その結果、図3に示した内容がコンソールに出力され、Version.fx(1)に該当するコードがロードされたことが分かります。これは、クラス・ローダーがクラスパス内で最初にこのバージョンのコードを見つけたためです。
図3. クラスパス内で最初に置かれているVersion 1のsamepathテスト
クラスパス内のパス要素の順序を少しだけ変更して、実行を繰り返します。
set CLASSPATH=.;%CURRENT_ROOT%\v2;%CURRENT_ROOT%\v1 %JAVA_HOME%\bin\java Test
コンソール出力は図4のように変わり、Version.fx(2)に該当するコードがロードされたことが分かります。これは、クラス・ローダーがクラスパス内で最初にこのバージョンのコードを見つけたためです。
図4. ククラスパス内で最初に置かれているVersion 2のsamepathテスト
上の例から、クラス・ローダーは、最初に見つかったパス要素を使用してクラスをロードすることが分かります。 また、v1とv2からversion.Versionクラスを削除し、version.Versionが無くなってから.jar(myextension.jar)ファイルを作成してjava.ext.dirsに対応するパスに置き、テストを繰り返すと、図5に示したように、version.VersionがAppClassLoaderではなく、拡張クラス・ローダーによってロードされるようになります。
図5. AppClassLoaderとExtClassLoader
例をさらに進めると、differentversionsフォルダにはRMI拡張エンジンが含まれています。 クライアントは、common.TaskIntfを実装した任意のタスクを実行エンジンに渡すことができます。 サブフォルダclient1とclient2には、わずかに異なるclient.TaskImplクラスのバージョンが含まれています。 この2つのクラス間にある違いは、次の行です。
static{
log("client.TaskImpl.class.getClassLoader
(v1) : " + TaskImpl.class.getClassLoader());
}
public void execute(){
log("this = " + this + "; execute(1)");
}
client1のログ・ステートメントには、getClassLoader(v1)とexecute()内のexecute(1)が含まれていますが、client2のログ・ステートメントにはgetClassLoader(v2)とexecute(2)が含まれています。 また、実行エンジンのRMIサーバーを起動するスクリプト内で、タスク実装クラスのclient2がクラスパス内で最初に置かれています。
CLASSPATH=%CURRENT_ROOT%\common;%CURRENT_ROOT%\server;
%CURRENT_ROOT%\client2;%CURRENT_ROOT%\client1
%JAVA_HOME%\bin\java server.Server
図6、7、8のスクリーンショットは、内部処理の様子を示しています。 クライアントVMでは、別々のclient.TaskImplクラスがロードされ、インスタンス化されて、実行エンジン・サーバーのVMでの実行向けに送信されています。 サーバー・コンソールから、client.TaskImplコードはサーバーVMに1度しかロードされていないことが分かります。 この単一"バージョン"のコードを使用して、多数のclient.TaskImplインスタンスがサーバーVM内で再生成され、タスクが実行されています。
図6. 実行エンジン・サーバーのコンソール
図6は、実行エンジン・サーバーのコンソールを示しています。このサーバーは、図7と図8に示した2つの異なるクライアント・リクエストに応じて、コードをロードし、実行しています。ここで注目すべきは、コードは1度しかロードされていない(静的初期化ブロック内のログ・ステートメントから明らか)にもかかわらず、メソッドはクライアントが起動したコンテキストごとに1回ずつ、合計2回実行されているという点です。
図7. 実行エンジンのクライアント1のコンソール
図7では、client.TaskImpl.class.getClassLoader(v1)というログ・ステートメントを含むTaskImplクラスのコードがクライアントVMによってロードされ、実行エンジン・サーバーに渡されています。 図8のクライアントVMでは、client.TaskImpl.class.getClassLoader(v2)というログ・ステートメントを含む、TaskImplクラスの別のコードがロードされ、サーバーVMに渡されています。
図8. 実行エンジンのクライアント2のコンソール
クライアントVMでは、別々のclient.TaskImplクラスがロードされ、インスタンス化されて、実行エンジン・サーバーのVMでの実行向けに送信されています。 しかし、図6のサーバー・コンソールをもう1度確認すると、client.TaskImplコードはサーバーVMに1度しかロードされていないことが分かります。 この単一"バージョン"のコードを使用してclient.TaskImplインスタンスがサーバーVM内で再生成され、タスクが実行されています。 クライアント1の起動に対してサーバーで実行されているのは、クライアント1"バージョン"のclient.TaskImpl(v1)ではなく別のコードであるため、これはクライアント1の要件を満たしていません。 では、このようなシナリオにはどう対処すれば良いのでしょうか。 その答えが、カスタム・クラス・ローダーの実装です。
