Java(Java SE)で、プログラム起動時にクラスパスでJARファイルを指定するのではなく、実行後にJARファイルを指定してクラスパスに追加したいことがあります。C/C++では、動的リンクライブラリをシステムコール(Win32 APIならLoadLibrary、UNIX系ならdlopen)で利用することができます。
Javaでは、クラスローダー(例えばjava.net.URLClassLoader)のインスタンスを作ってロードさせるのが順当な手段ですが、クラスローダーが異なる場合、staticの扱いが同じクラスローダーでロードした場合と異なるので、できればアプリケーションをロードしたシステムクラスローダーでロードさせたいところです。(staticに依存しないようアプリケーションが作られていればよいのですが、そうもいかないことが多いため)
Java SEのプログラムを実行するときは、以下のクラスローダーが動きます。
- ブートストラップ・クラスローダー
- インストールド・エクステンション・クラスローダー
- システム・クラスローダー
ブートストラップ・クラスローダーは、Javaのコア・クラス群(rt.jarなどに含まれるJava SE標準クラスライブラリ)をロードするために使われます。
インストールド・エクステンション・クラスローダーは、所定のインストールド・エクステンション・ディレクトリに置かれたJARファイルに含まれるクラスをロードします。所定のディレクトリとは、Java SE 6 JDKの場合、
システム・クラスローダーは、クラスパス(環境変数CLASSPATHまたはコマンドラインオプションのclasspath)で指定されたパスにあるクラス、ならびにJARファイルに含まれるクラスをロードします。
インストールド・エクステンション・ディレクトリに置いたクラスから、システム・クラスローダーがロードするクラスを見ることができないので、アプリケーションに対する追加機能を後付で提供するような用途では、システム・クラスローダーにクラスをロードさせる必要があります。
システム・クラスローダーをURLClassLoaderにキャストして操る
に、システム・クラスローダー(sun.misc.Launcher.AppClassLoader)がURLClassLoaderのサブクラスであることを利用し、URLClassLoaderにキャスト、さらにprotectedメソッドであるaddURLにアクセスするため、リフレクションを使うという技が紹介されています。
URLClassLoader sysloader = (URLClassLoader)ClassLoader.getSystemClassLoader();
まず、システム・クラスローダーをURLClassLoaderにキャストして*1、次に、
Class sysclass = URLClassLoader.class;
とURLClassLoaderのクラス型インスタンスを取得しておいてから、
try { Method method = sysclass.getDeclaredMethod("addURL", new Class[]{URL.class}); method.setAccessible(true); method.invoke(sysloader, new Object[]{fooFile.toURI().toURL()}); } catch ( 以下略
リフレクションでURLClassLoaderのaddURLメソッドを取得し、アクセスを許可してから、JARファイルのURLを指定してメソッドを実行します。
なるほど〜。
注意点
JARファイルのロードを記述するクラスではJARファイル内のクラスを見ない
上記のJARファイルのロードを記述したクラス(クラスA)で、JARファイルに含まれるクラス(クラスB)を使用していると、クラスAを実行する(クラスAをJavaVMにロード・リンクする)段階でクラスBをロードしようとしてNoClassDefFoundErrorが発生することがあります。
当たり前のことですが、ついやってしまうことがあります。JARファイル内のクラス(クラスB)にアクセスするクラス(クラスC)を作成し、クラスAでJARファイルのロード処理後、クラスAからクラスCを呼び、クラスCからクラスBを使用するようにします。
*1:SunのJavaVM実装依存なので、この部分はクロスプラットフォーム安全ではない