実行している範囲で実行に必要な依存しているJarファイルを調べたい

OSSのプロダクトのあるライブラリの挙動がおかしい、と調査をしていたのだけど、
調査の経過の共有のためにSVNとかにあげてほしい、といわれた。

しかし何も考えずwarに含まれていたjarをコピペしてclasspathを通して調査していた。
全部で50ファイル80MBぐらい。これをひょいっと共有するわけにもいかなかった(われわれのPCは貧弱なのである。)

そもそも、"ある部分だけ動けばよい"のであって、全部の機能を動かす必要はなかった。

でもどのJarに依存しているか分からない。1個1個jarをclasspathに通して調べるのもだるい。どうにかなりませんか!

思いついた答え

JavaVM引数に"-verbose:class"を与えて得られた出力をうまく読み取る。

java -cp "[classpath...]" -verbose:class survey.Main | perl -pe 's/\[Loaded (.*?) from (.*)\]$/$2/g' | sort | uniq

サンプルとして、"-verbose:class"を与えたときのあるテストコードを実行した時の標準出力を置いておきます。

http://pastebin.com/AQ7TG6Xj


この辺に流すと以下のような結果が得られます。

http://www.compileonline.com/execute_bash_online.php

$bash -f main.sh

C:\Program Files\Java\jdk1.7.0_07\jre\lib\charsets.jar
C:\Program Files\Java\jdk1.7.0_07\jre\lib\jsse.jar
C:\Program Files\Java\jdk1.7.0_07\jre\lib\rt.jar
file:/C:/Program%20Files/Java/jdk1.7.0_07/jre/lib/ext/localedata.jar
file:/C:/usr/local/share/pleiades/eclipse/plugins/org.hamcrest.core_1.1.0.v20090501071000.jar
file:/C:/usr/local/share/pleiades/eclipse/plugins/org.junit_4.10.0.v4_10_0_v20120426-0900/junit.jar

こんな感じに必要最低限そうなライブラリが見て取れます。

この場合なら、hamcrestのcoreと、junitの2つのjarが必須ということが分かります。(他はランタイムライブラリ?とかなので勝手に読まれる)

注意

もちろん、これはある条件での実行で必要になったjarが分かっただけで、

実際はifで分岐に入らなかった、動的に呼び出した、等で呼ばれなかったclassを持つJarが存在した場合、

"-verbose:class"では出力されません。これは実行しないと分からないので、仕方が無いと思います。

タイトルに「実行している範囲で」というプレフィクスをつけているにはそういうわけがあります。

もっと簡単な方法は無いか...

まとめ

結果として、これでうまく抽出でき、5ファイル30MBぐらいに済みました

...ん?

追記

JavaAgentを使っても簡単に出来そうなので、試してみた。


ClassFileTransformerを定義することで、クラスをロードする直前にフックをかける事ができるらしい。


参考


./myapp/agent/JarLoadLoggerAgent.java

package myapp.agent;

import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.lang.instrument.Instrumentation;
import java.net.URL;
import java.security.CodeSource;
import java.security.ProtectionDomain;
import java.util.LinkedHashSet;
import java.util.Set;

public class JarLoadLoggerAgent {

	public static void premain(String agentArgs, Instrumentation instrumentation) {
		instrumentation.addTransformer(
				new ClassFileTransformer() {
					private final Set<URL> locationSet = new LinkedHashSet<URL>();

					@Override
					public byte[] transform(java.lang.ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
						CodeSource codeSource = protectionDomain.getCodeSource();
						URL codeLocation = codeSource.getLocation();
						if (codeLocation.getFile().endsWith(".jar") && !locationSet.contains(codeLocation)) {
							locationSet.add(codeLocation);
							System.out.println(codeLocation);
						}
						return null;
					}
				});
	}
}

./bin/myapp/agent/JarLoadLoggerAgent.class へ出力する。

実際はLoggerとか使ったほうが良いけど。


./build.xml (ant)

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<project basedir="." default="jar_build" name="JarLoadLoggerAgent">
    <target name="jar_build">
    	<jar destfile="./JarLoadLoggerAgent.jar">
    		<manifest>
				<attribute name="Premain-Class" value="myapp.agent.JarLoadLoggerAgent" />
    		</manifest>
    		<fileset dir="./bin" includes="myapp/agent/*.class"/>
    	</jar>
    </target>
</project>

実行

java -javaagent:JarLoadLoggerAgent.jar "実行したいJavaクラス名"

出力

file:/C:/usr/local/share/pleiades/workspace/JUnitExample/junitlib/junit-dep-4.12-SNAPSHOT.jar
file:/C:/usr/local/share/pleiades/workspace/JUnitExample/junitlib/hamcrest-all-1.3.jar
file:/C:/Program%20Files/Java/jdk1.7.0_07/jre/lib/ext/localedata.jar
...