Java代码缺陷自动分析工具介绍
黄锡波
Java代码缺陷自动分析工具主要有:Findbugs、PMD和CheckStyle工具。这里重点介绍Findbugs的使用,简要提及PMD和CheckStyle工具的使用。
1 FindBugs是什么?
FindBugs 是一个java bytecode静态分析工具,它可以帮助java工程师提高代码质量以及排除隐含的缺陷。
FindBugs检查类或者 JAR 文件,将字节码与一组缺陷模式进行对比以发现可能的问题。
有了静态分析工具,就可以在不实际运行程序的情况对软件进行分析。FindBugs不是通过分析类文件的形式或结构来确定程序的意图,而是通常使用 Visitor 模式进行分析(Visitor 模式的更多信息)。
2 FindBugs可以做什么?
FindBugs提供了35个检测器来检测字节码中可能的缺陷。可以做的事情主要有:
2.1 找出 hash equals 不匹配
找与 equals()
和 hashCode()
的实现相关的几个问题。这两个方法非常重要,因为几乎所有基于集合的类---List、Map、Set 等都调用它们。一般来说,这个检测器寻找两种不同类型的问题:
①当一个类重写对象的 equals()
方法,但是没有重写它的 hashCode
方法,或者相反的情况时。
②定义一个 co-variant 版本的 equals()
或 compareTo()
方法。例如, Bob
类定义其 equals()
方法为布尔 equals(Bob)
,它覆盖了对象中定义的 equals()
方法。因为 Java 代码在编译时解析重载方法的方式,在运行时使用的几乎总是在对象中定义的这个版本的方法,而不是在 Bob
中定义的那一个(除非显式将 equals()
方法的参数强制转换为 Bob
类型)。因此,当这个类的一个实例放入到类集合中的任何一个中时,使用的是 Object.equals()
版本的方法,而不是在 Bob
中定义的版本。在这种情况下, Bob
类应当定义一个接受类型为 Object
的参数的 equals()
方法。
2.2 检测:忽略方法返回值
这个检测器查找代码中忽略了不应该忽略的方法返回值的地方。这种情况的一个常见例子是在调用 String
方法时,例如:
1 String aString = "bob";
2 b.replace('b', 'p');
3 if(b.equals("pop"))
这个错误很常见。在第 2 行,程序员认为他已经用 p 替换了字符串中的所有 b。确实是这样,但是他忘记了字符串是不可变的。所有这类方法都返回一个新字符串,而从来不会改变消息的接收者。
2.3 检测:Null 指针对 null 的解引用(dereference)和冗余比较
这个检测器查找两类问题。它查找代码路径将会或者可能造成 null 指针异常的情况,它还查找对 null 的冗余比较的情况。例如,如果两个比较值都为 null,那么它们就是冗余的并可能表明代码错误。FindBugs 在可以确定一个值为 null 而另一个值不为 null 时,检测类似的错误,例如:
1 Person person = aMap.get("bob");
2 if (person != null) {
3 person.updateAccessTime();
4 }
5 String name = person.getName();
在这个例子中,如果第 1 行的 Map
不包括一个名为“bob”的人,那么在第 5 行询问 person
的名字时就会出现 null 指针异常。因为 FindBugs 不知道 map 是否包含“bob”,所以它将第 5 行标记为可能 null 指针异常。
2.4 检测:初始化之前读取字段
这个检测器寻找在构造函数中初始化之前被读取的字段。这个错误通常是由使用字段名而不是构造函数参数引起的,例如在构造函数中读取未初始化的字段:
1 public class Thing {
2 private List actions;
3 public Thing(String startingActions) {
4 StringTokenizer tokenizer = new StringTokenizer(startingActions);
5 while (tokenizer.hasMoreTokens()) {
6 actions.add(tokenizer.nextToken());
7 }
8 }
9 }
在这个例子中,第 6 行将产生一个 null 指针异常,因为变量 actions
还没有初始化。
2.5 命名检查
对标准 Java 命令规范的测试:变量名称不应太短;方法名称不应过长;类名称应当以小写字母开头;方法和字段名应当以小写字母开头,等等。
2.6 未使用的代码检查
查找从未使用的私有字段和本地变量、执行不到的语句、从未调用的私有方法,等等。
2.7 嵌套检查
例如: switch 语句应当有 default 块,应当避免深度嵌套的 if 块,不应当给参数重新赋值,不应该对 double 值进行相等比较。
2.8 导入语句检查
检查 import 语句的问题,比如同一个类被导入两次或者被导入 java.lang 的类中。
2.9 JUnit 测试检查
查找测试用例和测试方法的特定问题,例如方法名称的正确拼写,以及 suite() 方法是不是 static 和 public。
2.10 字符串检查
找出处理字符串时遇到的常见问题,例如重复的字符串标量,调用 String 构造函数,对 String 变量调用 toString() 方法。
2.11 括号检查
检查 for、 if、 while 和 else 语句是否使用了括号。
2.12 代码尺寸检查
测试过长的方法、有太多方法的类以及重构方面的类似问题。
2.13 终结函数检查
因为在 Java 语言中, finalize() 方法不是那么普遍,它们的使用规则虽然很详细,但是人们对它们相对不是很熟悉。这类检查查找 finalize() 方法的各种问题,例如空的终结函数,调用其他方法的 finalize() 方法,对 finalize() 的显式调用,等等。
2.14 克隆检查
用于 clone() 方法的新规则。凡是重写 clone() 方法的类都必须实现 Cloneable, clone() 方法应该调用 super.clone(),而 clone() 方法应该声明抛出 CloneNotSupportedException 异常,即使实际上没有抛出异常,也要如此。
2.15 耦合检查
查找类之间过度耦合的迹象,比如导入内容太多;在超类型或接口就已经够用的时候使用子类的类型;类中的字段、变量和返回类型过多等。
2.16 异常检查
针对异常的检查:不应该声明该方法而抛出 java.lang.Exception 异常,不应当将异常用于流控制,不应该捕获 Throwable,等等。
2.17 日志检查
查找 java.util.logging.Logger 的不当使用,包括非终状态(nonfinal)、非静态的记录器,以及在一个类中有多个记录器。
2.18 Open—Close检查
检查文件或通讯方面,是否忘记Close的情况。
2.19 其它检查
其它缺陷清单可参见:缺陷清单
2.20 构建自己的规则集
可以构建自己的规则集
3 准备使用FindBugs
FindBugs目前最新版本是 0.9.3。文件名是:findbugs-0.9.3.zip(Windows)或findbugs-0.9.3.tar.gz(Unix)。(下载网页)
也可以到StarTeam取相应安装文件到本地,将它解压缩到所选的目录中,安装就完成了。
要运行 FindBugs,需要一个版本 1.4 或者更高的 Java Development Kit (JDK)。
安装完后,要增加两个环境变量:
FINDBUGS_HOME,例如:FINDBUGS_HOME =D:\findbugs-0.9.3
JAVA_HOME,例如:JAVA_HOME=D:\j2sdk1.4.2_06
在 FindBugs 主目录中,有几个值得注意的目录。文档在 doc 目录中,但是对我们来说更重要的是bin 目录,该包含了运行 FindBugs 的批处理文件。
4 运行 FindBugs
像如今的大多数数工具一样,可以以多种方式运行 FindBugs——从 GUI、从命令行、使用 Ant、作为 Eclipse 插件程序和使用 Maven。
这里将重点提及从 GUI 运行 FindBugs,简要提及使用Ant 和命令行运行。
4.1 FindBugs UI
使用 FindBugs UI 很直观。使用 FindBugs UI 的一个好处是对每一个检测到的问题提供了说明,图 1 显示了缺陷 Test的说明。
图一:项目Test的缺陷说明
对每一种缺陷模式提供了类似的说明。窗口下面的 Source code 选项卡很有用。如果告诉 FindBugs 在什么地方寻找代码,它就会在转换到相应的选项卡时突出显示有问题的那一行。
图一中的上部有根据不同类别来检查缺陷的选项,选项有(By Class、By Package、By Bug Type、By Bug Category)。
图二是【file】选项,主要功能有:创建新项目、打开/关闭项目、储存项目、储存Bugs信息、装载Buugs信息
图三是【View】选项,这些选项很有用,可以切换不同模式的视图。
图四是【Settings】选项,是对检测的缺陷模式进行选择。
图五是【创建新项目】选项
4.2 使用 FindBugs UI步骤
第一次使用
(1) 依照图五【创建新项目】选项说明,先创建新项目,输入被检查的class或jar路径、java源文件路径及程序运行依赖的class或jar路径
(2) 按【FingBugs】按钮开始检查
(3) 如果出现下图六的警告
说明程序运行依赖的class或jar路径不对或依赖文件不完整,应依据警告信息完善。
(4) 按【OK】后即进入上图一的错误信息显示
(5) 选择【file】储存新建的项目
(6) 选择【file】储存新扫描的Bugs信息(xml文件)
非第一次使用
(1) 选择【file】装载上次新建的项目并运行
(2) 也可以选择【file】装载上次储存的Bugs信息来查看
值得一提的还有在将 FinBugs 作为 Ant 任务或者在命令行中运行 FindBugs 时,选择 xml
作为 ouput
选项,可以将上一次运行的结果装载到 UI 中。这样做是同时利用基于命令行的工具和 UI 工具的优点的一个很好的方法。
4.3 FindBugs 作为 Ant 任务
如何在 Ant 编译脚本中使用 FindBugs?首先将 FindBugs Ant 任务拷贝到 Ant 的 lib 目录中,这样 Ant 就知道新的任务。将 FIND_BUGS_HOME\lib\FindBugs-ant.jar 拷贝到 ANT_HOME\lib。
现在看看在编译脚本中要加入什么才能使用 FindBugs 任务。因为 FindBugs 是一个自定义任务,将需要使用 taskdef
任务以使 Ant 知道装载哪一个类。通过在编译文件中加入以下一行:
<taskdef name="FindBugs" classname="edu.umd.cs.FindBugs.anttask.FindBugsTask"/>
在定义了 taskdef
后,可以用它的名字 FindBugs
引用它。下一步要在编译中加入使用新任务的目标,示例如下:
1 <target name="FindBugs" depends="compile">
2 <FindBugs
home="${FindBugs.home}" output="xml" outputFile="jedit-output.xml">
3 <class location="c:\apps\JEdit4.1\jedit.jar" />
4 <auxClasspath path="${basedir}/lib/Regex.jar" />
5 <sourcePath path="c:\tempcbg\jedit" />
6 </FindBugs>
7 </target>
第 1 行: 注意 target
取决于编译。一定要记住处理的是类文件而 不 是源文件,这样使 target
对应于编译目标保证了 FindBugs 可在最新的类文件运行。FindBugs 可以灵活地接受多种输入,包括一组类文件、JAR 文件、或者一组目录。
第 2 行:必须指定包含 FindBugs 的目录,我是用 Ant 的一个属性完成的,像这样:
<property name="FindBugs.home" value="C:\apps\FindBugs-0.7.3" />
可选属性 output
指定 FindBugs 的结果使用的输出格式。可能的值有 xml
、 text
或者 emacs
。如果没有指定 outputFile
,那么 FindBugs 会使用标准输出。如前所述,XML 格式有可以在 UI 中观看的额外好处。
第 3 行: class
元素用于指定要 FindBugs 分析哪些 JAR、类文件或者目录。分析多个 JAR 或者类文件时,要为每一个文件指定一个单独的 class
元素。除非加入了 projectFile
元素,否则需要 class
元素。
第 4 行: 用嵌套元素 auxClasspath
列出应用程序的依赖性。这些是应用程序需要但是不希望 FindBugs 分析的类。如果没有列出应用程序的依赖关系,那么 FindBugs 仍然会尽可能地分析类。与 class
元素一样,可以在 FindBugs 元素中指定多个 auxClasspath
元素。 auxClasspath
元素是可选的。
第 5 行: 如果指定了 sourcePath
元素,那么 path
属性应当表明一个包含应用程序源代码的目录。指定目录使 FindBugs 可以在 GUI 中查看 XML 结果时突出显示出错的源代码。这个元素是可选的。
5 其它分析工具
除FingBugs静态分析工具外,还有PMD和Checkstyle,FingBugs、PMD和Checkstyle三个工具各有不同的特点,联合使用有助于减少误报错误,提高报告的准确率。
这三个工具检查的侧重点各有不同:
工具 | 目的 | 主要检查内容 |
FindBugs | 基于Bug Patterns概念,查找java bytecode中的潜在bug。在目前版本中,它不检查java源文件。 | 主要检查bytecode中的bug patterns,也允许用户自定义特定的bug patterns。 |
PMD | 检查java源文件中的潜在问题。 | 主要包括: - 空try/catch/finally/switch语句块 - 未使用的局部变量、参数和private方法 - 空if/while语句 - 过于复杂的表达式,如不必要的if语句等 - 复杂类 |
CheckStyle | 检查java源文件是否与代码规范相符 | 主要包括 - Javadoc注释 - 命名规范 - Headers - Imports - Size冲突和度量,如过长的方法 - Whitespace - Modifiers - Blocks - Coding Problems - Class Design - 重复代码 - Miscellaneous Checks - Optional Checks |
5.1 PMD工具
PMD工具目前最新版本是V3.3(下载网页)。
PMD的运行环境是j2se1.3或以后版本,安装过程同样也是解压即可。
5.1.1 Ant task的使用
(1) 把lib中所有的jar复制到项目的classpath中。
(2) 将pmd-2.0.jar中的rulesets解压到指定目录,这里面定义了分析所需要的规则集合。
(3) 修改build.xml文件。在这一版本中,提供了2个ant task。一个是pmd使用规则集合进行分析;另一个是检查代码中Copy & Paste代码。这2个任务对应的ant task使用:
PMD任务:
<target name="pmd">
<!-- 定义任务和任务所属类所在的classpath引用 -->
<taskdef name="pmd" classname="net.sourceforge.pmd.ant.PMDTask"
classpathref="classpath"/>
<!-- 检查使用的规则文件 -->
<pmd rulesetfiles="junit_lib/rulesets/imports.xml">
<!-- 输出格式和文件名 -->
<formatter type="html" toFile="pmd_report.html"/>
<!-- pmd所需要依赖包的classpath引用 -->
<classpath refid="classpath"/>
<!-- 要检查的项目源文件根目录 -->
<fileset dir="src">
<include name="**/*.java"/>
</fileset>
</pmd>
</target>
CPD任务:
<target name="cpd">
<!-- 定义任务和任务所属类所在的classpath -->
<taskdef name="cpd" classname="net.sourceforge.pmd.cpd.CPDTask"
classpathref="classpath"/>
<!-- 指明输出文件和判断属于copy & paste的标准 -->
<cpd minimumTokenCount="100" outputFile="cpd.txtl">
<!-- 要检查的项目源文件根目录 -->
<fileset dir="src">
<include name="**/*.java"/>
</fileset>
</cpd>
</target>
(4) 运行ant pmd和ant cpd即可。
(5) 参数说明:
- formatter,指明输出格式和文件。
- rulesetfiles,指明分析所需的规则文件,不同文件使用逗号分隔。
- failonerror,pmd执行出错是否中止构建过程。
- failOnRuleViolation,如果与规则冲突,是否中止构建过程。
- classpath,pmd所需的classpath。
- printToConsole,在发现问题时是否打印到ant log或控制台。
- shortFilenames,在输出报告中是否使用短文件名。
- targetjdk13,是否把目标定为jdk13,如不能使用assert。
- failuresPropertyName,在任务结束时,插入违反规则的号码
- encoding,读源文件时所采用的编码,如utf-8。
5.1.2 Text的使用
把bin目录下的pmd.bat修改为:
java -jar ..\lib\pmd-3.3.jar D:\temp\hpMTS_2.jar[HUANGXB1] text[HUANGXB2] rulesets/basic.xml[HUANGXB3] ,rulesets/braces.xml,rulesets/clone.xml,rulesets/codesize.xml,rulesets/controversial.xml,rulesets/coupling.xml, rulesets/design.xml,rulesets/favorites.xml,rulesets/finalizers.xml,rulesets/unusedcode.xml,rulesets/sunsecure.xml,rulesets/strings.xml,rulesets/strictexception.xml,rulesets/scratchpad.xml,rulesets/optimizations.xml,rulesets/naming.xml,rulesets/my-rules.xml,rulesets/logging-java.xml,rulesets/logging-jakarta-commons.xml,rulesets/junit.xml,rulesets/javabeans.xml,rulesets/imports.xml
5.1 CheckStyle工具
CheckStyle目前最新版本是: checkstyle-4.0-beta6(下载网页)。
CheckStyle的运行环境是j2se1.3或以后版本,安装过程同样也是解压即可。
5.2.1 ant task的使用
(1) 复制checkstyle-4.0-beta6.jar到项目的classpath中。
(2) 修改build.xml文件:
<taskdef resource="checkstyletask.properties"
classpath="${weblib.dir}/checkstyle-all-3.3.jar"/>
<target name="checkstyle" depends="init">
<!-- 指明checkstyle的分析所需的规则文件 -->
<checkstyle config="checkstyle33.xml">
<!-- 要检查的文件 -->
<fileset dir="${src.code}" includes="**/*.java"/>
<!-- 指明输出格式和文件名 -->
<formatter type="xml" toFile="report.xml"/>
</checkstyle>
<!-- 将xml文件转换成html文件 -->
<style in="report.xml" out="report.html" style="checkstyle-frames"/>
</target>
(3) 运行ant checkstyle即可。
checkstyle的规则文件,即项目的代码规范,建议不要手工书写。可以使用checkstyle plug in在Eclipse配置后再导出。Checkstyle提供了缺省的xslt,用来进行xml的格式转换。它们都放在contrib目录中。Checkstyle同样也提供了自定义的check,但与PMD相比,书写要复杂。
6 志鸿公司使用自动分析工具案例
志鸿公司目前在HPC项目中使用Java代码缺陷自动分析工具。
该项目以使用FingBugs UI工具为主,PMD为辅助工具。自动分析输出的结果经过验证确认后,作为缺陷管理的组成部分之一。
经过一个多月来的运作,证实自动分析工具对改进代码的质量起到很大的作用。
[HUANGXB1]被检测的java源文件
[HUANGXB2]Text模式
[HUANGXB3]检测器名字,所有检测器在pmd-3.3.jar里面的rulesets目录