特定の maven task を実行したときだけ ClassLoader で目的のクラスがスキャンできない

公開:2021/01/08
更新:2021/01/09
5 min読了の目安(約4500字TECH技術記事

TL;DR

  • maven-surefire-plugin というのが、 classpath 内のたくさんの jar ファイルを surefirebooter~~~.jar というものにまとめてしまう
  • ClasspathHelper.forClassLoader() の後に ClasspathHelper.forManifest() を挟むことで内部の URL 一覧を取得できる

詳細

実行時に特定の interface を持つサブクラスをスキャンするために、次のようなコードを書いてました。

    List<ClassLoader> classLoadersList = new LinkedList<>();
    classLoadersList.add(ClasspathHelper.contextClassLoader());
    classLoadersList.add(ClasspathHelper.staticClassLoader());

    Reflections reflections = new Reflections(new ConfigurationBuilder()
        .setScanners(new SubTypesScanner(false), new ResourcesScanner())
        .setUrls(ClasspathHelper.forClassLoader(classLoadersList.toArray(new ClassLoader[0])))
        // Should append suffix \..* since filter is applied to package+class name.
        .filterInputsBy(new FilterBuilder().include(packageRegex + "\\..*")));

    // Get subtypes you want.
    reflections.getSubTypesOf(YourInterface.class).stream()...

IDE (IntelliJ IDEA) で GUI でテストしている間は正常に動作していましたが、 mvn verify をした時だけほしいクラスが見つからないということがありました。
そこで classpath のリストを試しに出力してみました。

    for (URL url : ClasspathHelper.forClassLoader(classLoadersList.toArray(new ClassLoader[0]))) {
      System.out.println(String.format("%s", url.toString()));
    }

本来なら使用しているライブラリの jar やプロジェクト内のモジュールへのフォルダパスなどが出力されるはずです。

  • 本来ほしい内容
file:/C:/Users/.../build/classes/java/test
file:/C:/Users/.../build/libs/mylibrary.jar
file:/C:/Users/.../.m2/repository/javax/servlet/javax.servlet-api/3.1.0/javax.servlet-api-3.1.0.jar
...
file:/C:/Program%20Files/Amazon%20Corretto/jdk1.8.0_265/jre/lib/ext/access-bridge-64.jar
file:/C:/Program%20Files/Amazon%20Corretto/jdk1.8.0_265/jre/lib/ext/cldrdata.jar
file:/C:/Program%20Files/Amazon%20Corretto/jdk1.8.0_265/jre/lib/ext/dnsns.jar
...

ですが mvn verify したときは次のような結果になりました。

  • mvn verify した時の結果
file:/C:/Users/.../target/surefire/surefirebooter4604658542841964121.jar
file:/C:/Users/.../.m2/repository/org/jacoco/org.jacoco.agent/0.8.2/org.jacoco.agent-0.8.2-runtime.jar
file:/C:/Program%20Files/Amazon%20Corretto/jdk1.8.0_265/jre/lib/ext/access-bridge-64.jar
file:/C:/Program%20Files/Amazon%20Corretto/jdk1.8.0_265/jre/lib/ext/cldrdata.jar
file:/C:/Program%20Files/Amazon%20Corretto/jdk1.8.0_265/jre/lib/ext/dnsns.jar
...

これは maven-surefire-plugin というのが、たくさんある jar を surefirebooter4604658542841964121.jar にまとめているためです。
なぜまとめるかというと、 OS やコマンドライン環境の違いにより実行できるコマンドの長さに限界があるため java コマンドを実行する際にすべての jar を classpath 指定できない場合があるためです。
で、この surefirebooter の jar の中身は特にオプションが明示されてない場合は実際の jar ではなく manifest ファイルになっています。
image.png

manifest.mf については、ClasspathHelper.forClassLoader() ではなく ClasspathHelper.forManifest() を使うことで、 manifest.mf 内の URL を展開して受け取ることができます。また ClasspathHelper.forManifest() は渡されたものが manifest ファイルでなかった場合もそのまま jar の URL を返してくれます。なので特に manifest ファイルであるかどうかを判断することなく、次のように forClassLoader()forManifest() を重ね掛けすることですべての jar の URL を獲得することができます。

    final Collection<URL> effectiveClassUrls =
        // Resolve manifest in Surefirebooter jar in classpath.
        ClasspathHelper.forManifest(
            ClasspathHelper.forClassLoader(classLoadersList.toArray(new ClassLoader[0])));

最終的に、必要なサブクラスをスキャンする処理は次のようになります。

    List<ClassLoader> classLoadersList = new LinkedList<>();
    classLoadersList.add(ClasspathHelper.contextClassLoader());
    classLoadersList.add(ClasspathHelper.staticClassLoader());

    final Collection<URL> effectiveClassUrls =
        // Resolve manifest in Surefirebooter jar in classpath.
        ClasspathHelper.forManifest(
            ClasspathHelper.forClassLoader(classLoadersList.toArray(new ClassLoader[0])));

    Reflections reflections = new Reflections(new ConfigurationBuilder()
        .setScanners(new SubTypesScanner(false), new ResourcesScanner())
        .setUrls(effectiveClassUrls)
        // Should append suffix \..* since filter is applied to package+class name.
        .filterInputsBy(new FilterBuilder().include(packageRegex + "\\..*")));

    // Get subtypes you want.
    reflections.getSubTypesOf(YourInterface.class).stream()...

補足

maven-surefire-plugin が明示的に pom.xml に追加されてない場合も、依存ライブラリ内で定義されている場合があるのでご注意ください。
今回私の場合は maven-failsafe-plugin を使用しており、その中で surefire も有効になっていました。