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

2019年1月3日

これはJavaの実行時にクラスパスを追加するの続きである。

前回の問題

前回の前提としては以下のようなものであった。

  • 一つのメインとなるjarファイルがダブルクリックで起動され、あるクラスが実行される(これをエントリクラスと呼ぶ)。
  • しかし、本当はlibフォルダにあるすべてのjarファイルをクラスパスとして指定して、エントリクラス以外のクラスを実行したい(エントリクラスにはクラスパスが指定されていない)。
  • しかし、このクラスパスはコマンドラインにも、メイン.jarファイルの中にも指定したくない。
  • 上の補足としては、実際には環境(Windows, Linux)の違いにより、クラスパスとするjarを変更したい事情もある。つまり、エントリクラス内で環境を判断し、それによってクラスパスを構築し、エントリクラス以外のクラスをそのクラスパスで実行したいわけである。

解決策としては、エントリクラスが含まれるメイン.jarと共に、lib中のすべてのjarファイルを指定した新たなクラスローダを作成し、エントリクラス以外のクラスを呼び出すというものであった。

このケースの場合、もともとアプリとして起動されるjarファイルは唯一つであり、そこに現在実行中のエントリクラスが含まれることから、それ自身の.jarファイルパスを取得できる。他のものとしてはJREシステムのjarファイル群と、新たに加えられるlib中のjarファイルである。

今回の問題

今回の問題は、上とは全く異なる状況であり、以下である。

  • あらかじめ実行に必要なほとんどすべてのjarファイルはクラスパスとして与えられている。
  • つまり、JREシステムはもちろんだが、ほぼすべてのjarファイルがクラスパスとして指定済である。
  • が、一つか二つのわずかなjarファイルのみ(仮にextra.jarとする)を、そのクラスパスとして加えたい。このjarファイルをその他のjarファイルと区別無く利用したい(つまり、相互に呼び出し可能な状態とする)。

この場合、前回の戦略は使えない。なぜなら、実行中のエントリクラスの含まれるjarはわかるのだが、それ以外のjarファイルの場所がわからないからだ。

そして、Java9以降の場合、システムクラスローダがURLClassLoaderではないので、メイン.jar以外のjarファイルをクラスローダから知ることはできない。

システムクラスローダを親とするURLClassLoaderを作成する方法

すぐに思いつくのは、追加したいextra.jarを含むURLClassLoaderを作成し、親クラスローダとしてデフォルトのクラスローダ(システムクラスローダ)を指定するというものである。

    File file = new File("extra.jar");
    ClassLoader defaultLoader = EntryClass.class.getClassLoader();
    // ClassLoader defaultLoader = ClassLoader.getSystemClassLoader()でも同じらしい
    ClassLoader newLoader = new URLClassLoader(new URL[] { file.toURI().toURL() }, defaultLoader);

しかしこれでは全くうまく行かないのだ。その理由はこういうことらしい。defaultLoaderでロードされたクラスはnewLoaderに指定されたクラスを見つけ出せないらしい。

もちろんnewLoaderを受け渡し、それを使い回すようにすれば、その中のクラスをロードすることはできるが、このようなことはいちいちやってられない。

つまりは、「クラスを別け隔てなく扱うには単一のクラスローダの中に含まれていなければならない」ということだ。

すべてが含まれる単一のクラスローダを作るにはどうするか?

結局のところ、extra.jarをもともとあるプログラムの一部として別け隔てなく扱うには、extra.jarと他すべてのjarファイルを含む単一のクラスローダを作成する他はない。

しかし、Java9以降のシステムクラスローダはURLClassLoaderではないため、クラスローダからクラスパスを取得することはできないのである。しかし、手立てはあった。System.getProperty(“java.class.path”)を使う方法である。

こんなふうになる。

public class EntryClass {
  public static void main(String[]args) throws Exception { 

    List<URL>pathList = 
      Arrays.stream(System.getProperty("java.class.path").split(File.pathSeparator))
      .map(s->getURL(new File(s))).collect(Collectors.toList());
    pathList.add(getURL(new File("extra.jar")));
    pathList.stream().forEach(System.out::println);
    URLClassLoader classLoader = new URLClassLoader(pathList.toArray(new URL[0]), null);
    .....
  }

  static URL getURL(File file) {
    try {
      return file.toURI().toURL();
    } catch (Exception ex) {
      throw new RuntimeException(ex);
    }
  }

あとはこのクラスローダを使ってEntryClass以降を実行すればよい。

うまく行かないケース

しかし、これでもうまく行かなくなるケースが出てくる。手こずっているのはこれだ。

log4j:ERROR A "org.apache.log4j.ConsoleAppender" object is not assignable to a "org.apache.log4j.Appender" variable.
log4j:ERROR The class "org.apache.log4j.Appender" was loaded by 
log4j:ERROR [java.net.URLClassLoader@6979e8cb] whereas object of type 
log4j:ERROR "org.apache.log4j.ConsoleAppender" was loaded by [sun.misc.Launcher$AppClassLoader@2a139a55].
log4j:ERROR Could not instantiate appender named "CONSOLE".

com.cm55.clog.Log4jException: A "org.apache.log4j.ConsoleAppender" object is not assignable to a "org.apache.log4j.Appender" variable. (and others)
    at com.cm55.clog.Log4jConfigurator.analyzeError(Log4jConfigurator.java:60)
    at com.cm55.clog.Log4jConfigurator.setConfiguration(Log4jConfigurator.java:45)
    at com.cm55.clog.CLogFactory.<clinit>(CLogFactory.java:25)
    at com.cm55.gs.server.main.ServerMainEntry.<clinit>(ServerMainEntry.java:55)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)

こうある、「”org.apache.log4j.ConsoleAppender” was loaded by [sun.misc.Launcher$AppClassLoader@2a139a55]」と。

つまり、org.apache.log4j.ConsoleAppenderがシステムクラスローダでロードされているというのである。しかし、EntryClassでは一切Log4jは使っていないし、そもそも使っていてロードされていたとしても、EntryClass以降は全く別のクラスローダなので、こういった問題が発生するはずがないのである。

おそらく問題はLog4J(あるいはLog4Jを使う他のライブラリ)の中で、与えられたクラスローダを使わずに「ClassLoader.getSystemClassLoader()」を呼び出し、org.apache.log4j.ConsoleAppenderをロードしているからと思われる。

具体的にどのライブラリがこのようなことをしているのかはまだ特定していない。今後の課題である。

参考