这个脚本用于定位应用classpath下有哪些jar包冲突,列出它们的相似度,以及冲突的class个数,执行效果如下:
$ ./cp-check.sh .
Similarity DuplicateClasses File1 File2
%100 502 jackson-mapper-asl-1.9.13.jar jackson-mapper-lgpl-1.9.6.jar
%100 21 org.slf4j.slf4j-api-1.5.6.jar slf4j-api-1.5.8.jar
%100 9 jcl-over-slf4j-1.5.8.jar org.slf4j.jcl-over-slf4j-1.5.6.jar
%100 6 org.slf4j.slf4j-log4j12-1.5.6.jar slf4j-log4j12-1.5.8.jar
%99 120 jackson-core-asl-1.9.13.jar jackson-core-lgpl-1.9.6.jar
%98 513 jboss.jboss-netty-3.2.5.Final.jar netty-3.2.2.Final.jar
%98 256 jakarta.log4j-1.2.15.jar log4j-1.2.14.jar
%98 97 json-lib-2.2.3.jar json-lib-2.4-jdk15.jar
%87 186 fastjson-1.1.15.jar fastjson-1.1.30.jar
%85 215 cglib-nodep-3.1.jar sourceforge.cglib-0.0.0.jar
%83 93 commons-beanutils-1.7.0.jar commons-beanutils-core-1.7.0.jar
%21 6 commons-logging-1.1.1.jar org.slf4j.jcl-over-slf4j-1.5.6.jar
%21 6 commons-logging-1.1.1.jar jcl-over-slf4j-1.5.8.jar
%16 18 commons-beanutils-1.7.0.jar commons-beanutils-bean-collections-1.7.0.jar
%04 8 batik-ext-1.7.jar xml-apis-1.0.b2.jar
%02 10 commons-beanutils-core-1.7.0.jar commons-collections-3.2.1.jar
%02 10 commons-beanutils-1.7.0.jar commons-collections-3.2.1.jar
See /tmp/cp-verbose.log for more details.
脚本同时会输出一个包含所有冲突的class文件:/tmp/cp-verbose.log
这个verbose文件内容大致如下,记录每个有冲突的class位于哪些jar包,定位问题时可以去查:
org/jboss/netty/util/internal/SharedResourceMisuseDetector.class
jboss.jboss-netty-3.2.5.Final.jar,netty-3.2.2.Final.jar
org/jboss/netty/util/internal/StackTraceSimplifier.class
jboss.jboss-netty-3.2.5.Final.jar,netty-3.2.2.Final.jar
org/jboss/netty/util/internal/StringUtil.class
jboss.jboss-netty-3.2.5.Final.jar,netty-3.2.2.Final.jar
脚本内容:
#!/bin/bash
if [ $# -eq 0 ];then
echo "please enter classpath dir"
exit -1
fi
if [ ! -d "$1" ]; then
echo "not a directory"
exit -2
fi
tmpfile="/tmp/.cp$(date +%s)"
tmphash="/tmp/.hash$(date +%s)"
verbose="/tmp/cp-verbose.log"
declare -a files=(`find "$1" -name "*.jar"`)
for ((i=0; i < ${#files[@]}; i++)); do
jarName=`basename ${files[$i]}`
list=`unzip -l ${files[$i]} | awk -v fn=$jarName '/\.class$/{print $NF,fn}'`
size=`echo "$list" | wc -l`
echo $jarName $size >> $tmphash
echo "$list"
done | sort | awk 'NF{
a[$1]++;m[$1]=m[$1]","$2}END{for(i in a) if(a[i] > 1) print i,substr(m[i],2)
}' > $tmpfile
awk '{print $2}' $tmpfile |
awk -F',' '{i=1;for(;i<=NF;i++) for(j=i+1;j<=NF;j++) print $i,$j}' |
sort | uniq -c | sort -nrk1 | while read line; do
dup=${line%% *}
jars=${line#* }
jar1=${jars% *}
jar2=${jars#* }
len_jar1=`grep -F "$jar1" $tmphash | grep ^"$jar1" | awk '{print $2}'`
len_jar2=`grep -F "$jar2" $tmphash | grep ^"$jar2" | awk '{print $2}'`
len=$(($len_jar1 > $len_jar2 ? $len_jar1 : $len_jar2))
per=$(echo "scale=2; $dup/$len" | bc -l)
echo ${per/./} $dup $jar1 $jar2
done | sort -nr -k1 -k2 |
awk 'NR==1{print "Similarity DuplicateClasses File1 File2"}{print "%"$0}'| column -t
sort $tmpfile | awk '{print $1,"\n\t\t",$2}' > $verbose
echo "See $verbose for more details."
rm -f $tmpfile
rm -f $tmphash
这个是改良过的脚本;第一次实现的时候是采用常规思路,用冒泡的方式比较两个jar文件的相似度,测试一二十个jar包的时候没有问题,找一个有180多个jar包的应用来跑的时候发现非常慢,上面改良后的脚本在我的mac上检查这个应用大概3秒左右,在linux上检测一个300个jar左右的应用4~5秒,基本上够用了。
为了兼容mac(有些命令在linux与mac/bsd上方式不同),大部分情况下采用awk来处理,不过我对awk也不太熟,只好采用逐步拼接的方式,如果通过一个awk脚本来实现或许性能可以高一些,但也比较有限,大头还是在获取jar里的class列表那块。几个tips:
- 测试发现
unzip -l
要比jar tvf
快出一个数量级以上 - shell里的字符串拼接尤其是比较大的字符串拼接非常耗性能
- mac/bsd下的sed非常难用,linux上的sed用法无法在mac上运行,跨平台脚本尽量避免sed,除非都安装的是gnu-sed
- bash4.0版本才支持map,而实际环境还运行的还是低版本的,只能自己模拟一个map,简单的做法可以基于临时文件
脚本已放到服务器上,可以通过下面的方式运行:
$ bash <(curl -s https://hongjiang.info/cpcheck.sh) libdir
感谢您的脚本。
我用java 的java.util.jar写了一个类似的jar tvf功能的jar.用一个4419多个文件的jar包在Ubuntu下试了下,对比unzip -l 耗时不相上下。而且jar tvf 虽然比unzip -l 慢但是没差到一个数量级。但是换一个比较小的jar包,tomcat/lib下的catalina.jar 711个文件 后 unzip -l的就很快了,差距达到了一个数量级。
我不知道jdk里自带的jar 命令是怎么实现的,按说应该也是个c/c++实现的,可能跟unzip的实现方式不同,或者因为sun/oracle考虑各个平台的差异实现的时候为了最大的兼容(没有调用一些平台特有的api)而不追求效率。
用java实现jar tvf的话,不知道你有没有统计jvm启动和销毁的时间?
如果jar里面的文件特别多的话,整个过程的瓶颈就是IO操作了,用Java实现的和C实现的应该是一个级别的(在还不足引起gc的情况下)
非常实用的脚本,很赞。请教博主个问题:
code在生成tmpfile的时候,在awk的块前面使用的NF,这种用法我没看懂。具体就是这句
awk ‘NF{
a[$1]++;m[$1]=m[$1]”,”$2}END{for(i in a) if(a[i] > 1) print i,substr(m[i],2)
}’ > $tmpfile
我只知道通常awk是通过block前面的pattern或者表达式来判断是否需要处理某行,但这里的NF也不是pattern啊,求教啦。
NF是awk的内置变量,表示列数,比如:echo “a b c” | awk ‘{print $NF}’ 会得到3.
但这个变量如果用于条件,它表示至少有一列元素;这个条件可以过滤掉空行,比如 echo “a\n\nb” | awk ‘NF’ 会去掉第二行的空行。
awk里的数组可以当map来用(关联数组),所以后边的 a[$1]++ 表示把 $1 这一列放到一个map里(数组a),并对相同的key出现的次数累加;再用另一个数组把第一列$1后边的 $2 的内容记录且拼接起来,举个例子就好理解了。
比如下面的文本,第一例表示key,第二列表示value:
$ cat file
p a
p b
p c
我们期望输出的结果是:p: a,b,c 即第一例相同的key所有的value合并起来。可以用一行awk来实现:
$ awk ‘{a[$1];m[$1]=m[$1]”,”$2}END{for(i in a) print i”: “m[i]}’ file
p: ,a,b,c
我原脚本里比上面的关联数组多做了一下累加,并判断value值至少有2个的(也就是有重复的)才打印,并截掉了开头的那个逗号连接符。
神器啊,,,,mark了
用Python实现了(重复率/相似度的实现方式不相同) 😁
Python实现的性能还行,测试了一下扫描4000个文件10秒内;
一般的应用不会有这么多Jar文件。
PS:脚本 show-duplicate-java-classes:
https://github.com/oldratlee/useful-scripts/blob/dev-2.x/docs/java.md#-show-duplicate-java-classes
赞