仕事中に、ユニットテストをやっていて実行時に動的にクラスパスを追加したくなる状況がありまして、その時に使った処置。
通常なら、ClassLoaderを作ってそこへのロード対象のURLを与えてどうにかするところでしょうが、既存のClassLoaderに追加したいということがありまして…。
これを行うこと自体が良いことだとは思っていないので、あくまでテストコードとかちょっとしたスクリプト的な使い方をする時のみの利用になるんでしょうけどね。
例として、別々のディレクトリに配置されたClassから、他のディレクトリに配置したクラスを呼び出すとしましょう。
まず、起動元となるmainメソッドを持つクラス。
src/a/AClass.java
package a; import java.io.File; import java.lang.reflect.Method; import java.net.MalformedURLException; import java.net.URL; import java.net.URLClassLoader; public class AClass { public static void main(String... args) { // 定義は後で } }
ここで、「src」はソースディレクトリです。
続いて、動的にロードされるクラス。
add-src/b/BClass.java
package b; public class BClass { public void echo() { System.out.println("Hello BClass!!"); } }
ここで、「add-src」はこのソースコードが配置されたソースディレクトリです。
それぞれをコンパイルします。まずは、a.AClassのコンパイル。
$ javac -sourcepath src src/a/AClass.java -d classes
クラスファイルの出力先は、「classes」ディレクトリとします。
続いて、b.BClassのコンパイル。
$ javac -sourcepath add-src add-src/b/BClass.java -d add-classes
クラスファイルの出力先は、「add-classes」とします。
ここで、a.AClassを以下のコマンドで実行しようとします。
$ java -cp classes a.AClass
見ての通り、クラスパスを通しているのは「classes」ディレクトリのみです。ここで、「add-classes」ディレクトリに出力されたb.BClassを実行時にクラスパスを追加する処理を、a.AClassに埋め込んでみます。
まず、mainメソッドは以下の様に実装しました。
public static void main(String... args) { try { ClassLoader classLoader = AClass.class.getClassLoader(); try { // クラスロードを試みる classLoader.loadClass("b.BClass"); } catch (ClassNotFoundException e) { // 最初は失敗する System.out.println("Got Exception => " + e); } // ClassLoaderにクラスパスを追加する addClassPath(classLoader, "add-classes"); // もう1度クラスロードしてみる Class<?> bClass = classLoader.loadClass("b.BClass"); // 今度は成功し、インスタンス化およびメソッドの呼び出しも可能に Method echo = bClass.getDeclaredMethod("echo"); echo.invoke(bClass.newInstance()); } catch (ReflectiveOperationException | MalformedURLException e) { e.printStackTrace(); } }
最初は当然b.BClassは見つからないので、クラスパスに「add-classes」を追加した後に再度ロードとメソッド呼び出しを試みています。
ここで、addClassPathメソッドの実装は以下の様になっています。
private static void addClassPath(ClassLoader classLoader, String path) throws ReflectiveOperationException, MalformedURLException { if (classLoader instanceof URLClassLoader) { // URLClassLoaderであることが前提 Method method = URLClassLoader.class.getDeclaredMethod("addURL", URL.class); method.setAccessible(true); // ロードするURLを追加する method.invoke(classLoader, new File(path).toURI().toURL()); } }
ClassLoaderは、URLClassLoaderクラスまたはそのサブクラスであることが前提になっています。
URLClassLoaderクラスのprotectedなメソッドである、addURLメソッドを呼び出します。
Method method = URLClassLoader.class.getDeclaredMethod("addURL", URL.class); method.setAccessible(true); // ロードするURLを追加する method.invoke(classLoader, new File(path).toURI().toURL());
引数は、URLです。今回はディレクトリを追加するイメージでしたが、JARファイルの配置先などURLの形式にさえできれば何でも良いと思います。
java.net.URLClassLoaderのaddURLメソッドは、以下の様に宣言されています。
protected void addURL(URL url) { ucp.addURL(url); }
ucpというのは、sun.misc.URLClassPath型の変数です。
/* The search path for classes and resources */ private final URLClassPath ucp;
では、先ほどのjavaコマンドの実行結果を表示してみます。
$ java -cp classes a.AClass Got Exception => java.lang.ClassNotFoundException: b.BClass Hello BClass!!
1度目はb.BClassのロードに失敗しますが、クラスパス追加後はロードに成功し、インスタンス化およびメソッド呼び出しまで成功します。
普段使うことはないと思いますが、何かのヒントになることがあるかも…?でもまあ、これを正規のコードで使うことはないでしょうけど。