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

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

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

```java
    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 のリストを試しに出力してみました。

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

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

- 本来ほしい内容

```bash
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 した時の結果

```bash
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`](https://maven.apache.org/surefire/maven-surefire-plugin/index.html) というのが、たくさんある jar を `surefirebooter4604658542841964121.jar` にまとめているためです。
なぜまとめるかというと、 OS やコマンドライン環境の違いにより実行できるコマンドの長さに限界があるため java コマンドを実行する際にすべての jar を classpath 指定できない場合があるためです。
で、この surefirebooter の jar の中身は特にオプションが明示されてない場合は実際の jar ではなく manifest ファイルになっています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/68978/609b297e-e02f-7d96-0d1c-2f9e20a22ce9.png)

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

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

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

```java
    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`](https://maven.apache.org/surefire/maven-failsafe-plugin/plugin-info.html) を使用しており、その中で surefire も有効になっていました。