检查应用jar冲突的脚本

这个脚本用于定位应用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 http://hongjiang.info/cpcheck.sh) libdir

检查应用jar冲突的脚本》上有4条评论

  1. hy

    感谢您的脚本。
    我用java 的java.util.jar写了一个类似的jar tvf功能的jar.用一个4419多个文件的jar包在Ubuntu下试了下,对比unzip -l 耗时不相上下。而且jar tvf 虽然比unzip -l 慢但是没差到一个数量级。但是换一个比较小的jar包,tomcat/lib下的catalina.jar 711个文件 后 unzip -l的就很快了,差距达到了一个数量级。

    回复
  2. hongjiang 文章作者

    我不知道jdk里自带的jar 命令是怎么实现的,按说应该也是个c/c++实现的,可能跟unzip的实现方式不同,或者因为sun/oracle考虑各个平台的差异实现的时候为了最大的兼容(没有调用一些平台特有的api)而不追求效率。

    用java实现jar tvf的话,不知道你有没有统计jvm启动和销毁的时间?
    如果jar里面的文件特别多的话,整个过程的瓶颈就是IO操作了,用Java实现的和C实现的应该是一个级别的(在还不足引起gc的情况下)

    回复
  3. TYT

    非常实用的脚本,很赞。请教博主个问题:
    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啊,求教啦。

    回复
  4. hongjiang 文章作者

    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个的(也就是有重复的)才打印,并截掉了开头的那个逗号连接符。

    回复

发表评论

电子邮件地址不会被公开。 必填项已用*标注