Javaプロジェクト内でバージョン違いの重複jarを探し出す方法


mavenなどでいろいろなフレームワークを試してみるときに
依存するプロジェクトがそれぞれ違うバージョンのlog4jなどに依存している場合があります。
単一バージョンのlog4jのみを使用するように、うまくpom.xmlファイルでexlcudeされていれば問題ないです。
しかしそうなっていない場合、コンパイルは通るものの、実行時に難解な例外やエラーが出てしまいます。
そのときに一度試してみて頂きたいのが下記の方法で、複数のjarに重複するパッケージ・クラスを持ったものがいないか探し出す方法です。
もっとよい方法があれば教えてください。お願いします。

###テスト環境

Java 17
eclipse 2022-06


###サンプル環境構成

```xml:project
testprj
│
├─libs
│  └─log4j
│      ├─1.2.12
│      │      log4j-1.2.12.jar
│      │
│      ├─1.2.14
│      │      log4j-1.2.14.jar
│      │
│      ├─1.2.16
│      │      log4j-1.2.16.jar
│      │
│      └─1.2.17
│              log4j-1.2.17.jar
│
└─src
    └─jp
        └─co
            └─panda
                └─testprj
                        Main.java
```

バージョン違いのLog4jのjarをlib以下に配置し、クラスパスに追加します。


###サンプルコード

実行するOSによりクラスパスの区切り文字を変更してください。
エラー処理などは手を抜いています。あしからず。

```Java:Main.java

package jp.co.panda.testprj;

import java.io.File;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.jar.JarFile;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class Main {

	private static final String CLASS_PATH_SEPARATOR = ";";//windows

	public static void main(String[] args) {
		// クラスパスを取得する
		final String[] classPathArray = System.getProperties()
				.getProperty("java.class.path", null)
				.split(CLASS_PATH_SEPARATOR);

		// jar名称とjarに含まれるクラスを調べてﾘｽﾄにする
		List<ClassPathData> classpathDataList = Stream.of(classPathArray)
			.filter(classpath -> classpath.endsWith(".jar"))
			.map(classpath -> {
				try (JarFile jarFile = new JarFile(new File(classpath))) {
					// jar名を取得する
					String[] sp = classpath.split("\\\\");
					final String jarName = sp[sp.length - 1];

					// jar内に含まれているクラス情報を取得する
					// META-INFとかは除外している
					List<ClassPathData> dataList = Collections.list(jarFile.entries()).stream()
							.map(jarEntry -> jarEntry.toString())
							.filter(jarEntry -> jarEntry.endsWith(".class"))
							.map(jarEntry -> {
								return new ClassPathData(
										jarName,
										jarEntry
								);
							})
							.collect(Collectors.toList());
					
					return dataList.stream();
				} catch (IOException e) {
					throw new RuntimeException();
				}
			})
			.flatMap(stream -> stream)
			.collect(Collectors.toList());

		// フルクラス名でグルーピングし、2個以上重複しているものを出力する
		classpathDataList.stream()
			.collect(Collectors.groupingBy(ClassPathData::getClasspath))
			.entrySet()
			.stream()
			.filter(entry -> entry.getValue().size() > 1)
			.sorted((a, b) -> a.getValue().size() - b.getValue().size())
			.sorted((a, b) -> a.getKey().compareTo(b.getKey()))
			.forEach(entry -> {
				System.out.println(entry.getKey());
				entry.getValue().stream()
					.map(data -> data.getJarname())
					.map(str -> "\t" + str)
					.sorted()
					.forEach(System.out::println);
			});
	}

	/**
	 * jar名称とjarに含まれるクラスを保持する
	 */
	static class ClassPathData {
		private String jarname;
		private String classpath;
		
		public ClassPathData(String jarname, String classpath) {
			this.jarname = jarname;
			this.classpath = classpath;
		}
		
		public String getJarname() {
			return jarname;
		}
		public void setJarname(String jarname) {
			this.jarname = jarname;
		}
		public String getClasspath() {
			return classpath;
		}
		public void setClasspath(String classpath) {
			this.classpath = classpath;
		}
	}

}

```

このコードを実行することで、出力されたログにはクラスが重複しているjar名をクラス単位に表記します。

```console
org/apache/log4j/Appender.class
	log4j-1.2.12.jar
	log4j-1.2.14.jar
	log4j-1.2.16.jar
	log4j-1.2.17.jar
org/apache/log4j/AppenderSkeleton.class
	log4j-1.2.12.jar
	log4j-1.2.14.jar
	log4j-1.2.16.jar
	log4j-1.2.17.jar
org/apache/log4j/AsyncAppender$DiscardSummary.class
	log4j-1.2.14.jar
	log4j-1.2.16.jar
	log4j-1.2.17.jar
org/apache/log4j/AsyncAppender$Dispatcher.class
	log4j-1.2.14.jar
	log4j-1.2.16.jar
	log4j-1.2.17.jar
 
...以降省略

```