某OSSのプロダクトのあるライブラリの挙動がおかしい、と調査をしていたのだけど、
調査の経過の共有のためにSVNとかにあげてほしい、といわれた。
しかし何も考えずwarに含まれていたjarをコピペしてclasspathを通して調査していた。
全部で50ファイル80MBぐらい。これをひょいっと共有するわけにもいかなかった(われわれのPCは貧弱なのである。)
そもそも、"ある部分だけ動けばよい"のであって、全部の機能を動かす必要はなかった。
でもどのJarに依存しているか分からない。1個1個jarをclasspathに通して調べるのもだるい。どうにかなりませんか!
思いついた答え
JavaのVM引数に"-verbose:class"を与えて得られた出力をうまく読み取る。
java -cp "[classpath...]" -verbose:class survey.Main | perl -pe 's/\[Loaded (.*?) from (.*)\]$/$2/g' | sort | uniq
サンプルとして、"-verbose:class"を与えたときのあるテストコードを実行した時の標準出力を置いておきます。
この辺に流すと以下のような結果が得られます。
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を定義することで、クラスをロードする直前にフックをかける事ができるらしい。
参考
- http://docs.oracle.com/javase/jp/7/api/java/lang/instrument/Instrumentation.html
- http://docs.oracle.com/javase/jp/7/api/java/lang/instrument/ClassFileTransformer.html
./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
...