Javaクラスローダ

クラスローダ

Javaのクラスローダというのは、すなわちJavaのクラスをロードするものですが、従って非常に難しげなわけです。何たって「クラス」「ローダ」です。このかっこよさにはしびれますし、「参りました」というほかありません。

このクラスローダを定義しろと言われたら、私など「私めなどが、『クラスローダ』様を定義するなどと大それたことを試みまして悪うございました。まさか私が『クラスローダ』様のような高貴で難解な方の定義を差し上げるなど、恐悦至極に存じます。究極の栄誉でございますが、これはもう、お断り申し上げるしかございません。」とへりくだるしかありません。

しかし、今日はこのクラスローダについて書いてみます。何をどうしてよいか分からなくなったので。

クラスローダとは

Javaのクラスローダには「ブートストラップクラスローダ」と…などと色々あるわけですが、とにかくそれらのもとからある「クラスローダ」によって、Javaの基本的なクラス群とクラスパスにあるクラス群はロード可能、つまり使うことが可能なわけです。

何のためにクラスローダを我々が「定義」するのかといえば、それ以外のクラスを動的にロードしたいからです。例えば、クラスパス外にあるJarファイルやclassファイルをロードして使いたいのです。そんなものを使ってどうするのかと思うかもしれませんが、「例えば」プラグインを動的に追加したいなどといったときに使えます。

いい加減な説明

おそろしくいい加減な説明をすると、クラスローダとは、「実行時のクラスパスを動的に変更する」ものです。「クラスAを使う場合には、実行時にクラスAがクラスパスに入っていないといけませんね」という話をJavaの初心者は学びますが、「最初から入ってなくてもいいよ、いざ使うときに入れるから」というようなものです。

クラスローダの使い方

クラスローダにはloadClass()というメソッドが存在します。これを利用して、クラスを意味するClassオブジェクトを入手することができます。

Class clazz = loader.loadClass("a.b.c"); // a.b.cクラスをロード

スレッドごとのクラスローダ

しかし、クラスローダからClassオブジェクトを取得するという使い方は、ある種例外的なものです。

ABC abc = new ABC();

などと書かれていたとして、このABCクラスをそのクラスローダからロードしたいというのが普通の使い方です。スレッドに対してクラスローダを登録することでそれが可能です。Thread#setContextClassLoader()を利用。

従って、スレッド1で使っているクラスAとスレッド2で使っているクラスAが同名であっても実体としては別のクラスであることができます。(もっといえば、同じスレッドでもできますが、普通はやらないので。)

例えば、プラグインとしてクラス群を追加できる仕組みを作ったとして、プラグインどうしでライブラリがかち合ったりすることがあるかもしれませんが、プラグインを別スレッドで動作させ、それぞれのクラスローダを別個に定義しておけばライブラリのかちあいなども気にする必要はありません。

プラグインABCとプラグインPPPの二つがあるとします。それぞれの実体となるJarファイルは、plugin/ABCとplugin/PPP内に格納しておくとします。

plugin/ABC/abclib.jar
plugin/ABC/xyz-1.0.jar
plugin/PPP/javalib.jar
plugin/PPP/aaa.jar
plugin/PPP/xyz-1.1.jar

その時、偶然にABCとPPPで同一のライブラリの別バージョンを利用するなどして、同一名称だが異なるクラスを利用することは起こりえます。

従って、プラグイン機構を作成する際には、プラグインごとに別のクラスローダを作るのがよいでしょう。具体的にはプラグインABCはplugin/ABC以下を検索するようなクラスローダ、プラグインPPPはplugin/PPP以下を検索するようなクラスローダです。そうすれば、そのような問題は起きなくなります。

リフレクション

さて、私は動的に追加するプラグイン機構のようなものを作っています。

そして次の問題で悩んでいます。

プラグインと本体のインタフェースはリフレクションか、クラス名決めうちか」

本体から呼び出すプラグインのクラス名を最初から決めうちする

どのプラグインでも共通にその名前のクラスを定義することにして、本体はそのクラスのスタブのようなものを利用して開発します。

例えば、PluginInterfaceClassというクラスを全てのプラグインで定義するよう規約を作っておき、そこを介してプラグインを呼び出すことにします。

Thread t = new Thread(new Runnable(){
   public void run(){
      // セットしたクラスローダを利用して動作する
      PluginInterfaceClass p = new PluginInterfaceClass(); // スタブでコンパイル
      p.execute();
   }
});
t.setContextClassLoader(loader); // 適切なクラスローダをセット
t.start();

リフレクションがないので何となくきれいです。しかし、同名のクラスが各所で定義されるのが気持ちが悪いのではないかという気もしてきました。デバッグも面倒そう。

本体では、クラスローダから名前指定で特定のクラスを利用する

プラグインごとに、例えば「プラグイン名+InterfaceClass」というクラスを定義させるという規約を作り、そのクラスを呼び出します。リフレクションを使うのでスタブは不要となります。

String plugin_name = ...;
final Class clazz = loader.loadClass(plugin_name + "InterfaceClass");
Thread t = new Thread(new Runnable(){
   public void run(){
      Object o = clazz.newInstance();
      Method m = clazz.getMethod("execute", null);
      m.invoke(o, null);
   }
});
t.setContextClassLoader(loader); // 適切なクラスローダをセット
t.start();

これはこれでありな気もします。スタブもいりませんし。

当面はこちらの手法で進めることにしておこうかなと思いました。