CLOVER🍀

That was when it all began.

実行時にクラスパスを追加する

仕事中に、ユニットテストをやっていて実行時に動的にクラスパスを追加したくなる状況がありまして、その時に使った処置。

通常なら、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のロードに失敗しますが、クラスパス追加後はロードに成功し、インスタンス化およびメソッド呼び出しまで成功します。

普段使うことはないと思いますが、何かのヒントになることがあるかも…?でもまあ、これを正規のコードで使うことはないでしょうけど。