代码质量工具的问题之一是,它们往往使开发人员陷入并非真正问题的问题,即误报 。 当出现误报时,开发人员将学会忽略该工具的输出或完全放弃它。 FindBugs的创建者David Hovemeyer和William Pugh对这个问题很敏感,并努力减少他们报告的误报数量。 与其他静态分析工具不同,FindBugs并不专注于样式或格式。 它专门尝试查找真正的错误或潜在的性能问题。
什么是FindBugs?
FindBugs是一种静态分析工具,它通过将您的字节码与错误模式列表匹配来检查您的类或JAR文件,以查找潜在的问题。 使用静态分析工具,您可以在不实际运行程序的情况下分析软件。 取而代之的是,通常使用Visitor模式分析类文件的形式或结构,以确定程序的意图(请参阅参考资料 )。 图1显示了分析匿名项目的结果(为了保护可怕的罪名,它的名称被保留):
图1. FindBugs UI
让我们看一下FindBugs可以检测到的一些问题。
发现问题的例子
以下列表未包含FindBug可能会发现的所有问题。 相反,我专注于一些更有趣的内容。
检测器:查找哈希等于不匹配
该检测器发现了几个相关问题,所有问题都围绕equals()
和hashCode()
。 这两种方法非常重要,因为几乎所有基于Collections的类(列表,地图,集合等)都调用它们。 通常,此检测器会发现两种不同类型的问题-当出现类时:
- 覆盖Object的
equals()
方法,但不覆盖其hashCode
,反之亦然。 - 定义
equals()
或compareTo()
方法的协变版本。 例如,Bob
类将其equals()
方法定义为booleanequals(Bob)
,这将重载Object中定义的equals()
方法。 由于Java代码在编译时解析重载方法的方式,Object中定义的方法版本几乎总是运行时使用的版本,而不是Bob
定义的版本(除非您将参数显式转换为equals()
方法输入Bob
)。 结果,将此类的一个实例放入任何集合类中时,将使用该方法的Object.equals()
版本,而不是Bob
定义的版本。 在这种情况下,Bob
类应定义一个equals()
方法,该方法接受Object
类型的参数。
检测器:方法的返回值被忽略
该检测器在代码中查找不应该返回方法返回值的位置。 调用String
方法时,可以找到此场景的最常见实例之一,如清单1所示:
清单1.忽略的返回值示例
1 String aString = "bob";
2 b.replace('b', 'p');
3 if(b.equals("pop"))
这个错误很常见。 在第2行,程序员认为他已经用p替换了字符串中的所有b。 他做到了,但他忘记了字符串是不变的。 所有这些类型的方法都返回一个新字符串,而不会更改消息的接收者。
检测器:空指针取消引用和对空值的冗余比较
该检测器查找两种类型的问题。 它查找代码路径将或可能导致空指针异常的情况,还查找与null有多余比较的情况。 例如,如果两个比较值都绝对为空,则它们是多余的,并且可能表示编码错误。 当能够确定其中一个值是null而另一个值不是时,FindBugs会检测到类似的问题,如清单2所示:
清单2.空指针示例
1 Person person = aMap.get("bob");
2 if (person != null) {
3 person.updateAccessTime();
4 }
5 String name = person.getName();
在此示例中,如果第1行上的Map
不包含名为“ bob”的person
则当询问该person
的姓名时,第5行将导致空指针异常。 因为FindBugs不知道映射是否包含“ bob”,所以它将第5行标记为可能的空指针异常。
检测器:在初始化之前读取的字段
该检测器查找在初始化之前在构造函数中读取的字段。 此错误通常是由于错误地使用字段名称而不是构造函数参数引起的,尽管并非总是如此,如清单3所示:
清单3.在初始化之前在构造函数中读取字段
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行将导致空指针异常,因为尚未初始化变量actions
。
这些示例只是FindBugs检测到的问题类型的一小部分( 有关更多信息,请参见参考资料)。 在撰写本文时,FindBugs附带了总共35个检测器。
FindBugs入门
要运行FindBugs,您将需要一个1.4版或更高版本的Java开发工具包(JDK),尽管它可以分析由较早的JDK创建的类文件。 要做的第一件事是下载并安装FindBugs的最新版本-目前0.7.1(参见相关主题 )。 幸运的是,下载和安装非常简单。 下载zip或tar后,将其解压缩到您选择的目录中。 就这样-安装完成。
现在已经安装了,让我们在一个示例类上运行它。 就像文章中经常提到的那样,我将与Windows用户交流,并假设Unix的说服力可以巧妙地翻译和跟进。 打开命令提示符,然后转到安装FindBugs的目录。 对我来说,这是C:\ apps \ FindBugs-0.7.3。
在FindBugs主目录中,有几个感兴趣的目录。 该文档位于doc目录中,但对我们而言更重要的是,bin目录包含用于运行FindBugs的批处理文件,这将使我进入下一部分。
运行FindBugs
像当今大多数工具一样,您可以通过多种方式运行FindBugs-从GUI,从命令行,使用Ant,作为Eclipse插件以及使用Maven。 我将简要提到从GUI运行FindBugs,但我主要集中于从Ant和命令行运行它。 部分原因是GUI尚未赶上所有命令行选项。 例如,当前您不能指定过滤器来包括或排除UI中的特定类。 但是更重要的原因是因为我认为FindBugs最好用作构建的集成部分,并且UI不属于自动化构建。
使用FindBugs UI
使用FindBugs UI很简单,但是有几点需要详细说明。 如图1所示,使用FindBugs UI的优点之一是为每种检测到的问题提供了描述。 图1显示了Naked notify in方法中的错误的描述。 针对每种错误模式都提供了类似的描述,当您初次熟悉该工具时,这将非常有用。 窗口下部窗格中的“源代码”选项卡同样有用。 如果您告诉FindBugs在哪里找到您的源代码,当您切换到适当的选项卡时,它将突出显示有问题的代码行。
还必须提及的是,如果在将FindBugs作为Ant任务运行或从命令行运行时选择xml
作为output
选项,则可以将上一次运行的结果加载到UI中。 这样做是同时利用基于命令行的工具和UI工具的优点的好方法。
将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
来引用它。 接下来,您将向使用新任务的构建中添加一个目标,如清单4所示:
清单4.创建一个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
取决于编译。 重要的是要记住,FindBugs是在类文件而不是源文件上工作的,因此使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
元素是必需的。 有关更多详细信息,请参见FindBugs手册。
第4行:使用嵌套元素auxClasspath
列出应用程序的依赖项。 这些是您的应用程序需要的类,但是您不希望FindBugs分析。 如果未列出应用程序的依赖项,FindBugs仍将尽其所能分析您的类,但是当找不到找不到的类之一时,它将发出抱怨。 与class
元素一样,您可以在FindBugs元素中指定多个auxClasspath
元素。 auxClasspath
元素是可选的。
第5行:如果指定了sourcePath
元素,则path
属性应指示包含应用程序源代码的目录。 指定目录允许FindBugs在GUI中查看XML结果时突出显示错误的源代码。 该元素是可选的。
这涵盖了基础知识。 让我们快进几个星期。
筛选器
您已将FindBugs介绍给您的团队,并已将其作为每小时/每晚构建过程的一部分进行运行。 随着团队越来越熟悉该工具,您已经决定,无论出于何种原因,所检测到的某些错误对于您的团队而言都不重要。 也许您不在乎某些类是否返回可以被恶意修改的对象,或者像JEdit一样,您有一个真正诚实的合理理由调用System.gc()
。
您始终可以选择“关闭”特定的检测器。 从更细微的层面上讲,您可以排除某些检测器,使其无法在一组指定的类甚至方法内查找问题。 FindBugs通过排除和包含过滤器提供了这种粒度控制。 当前仅在命令行或FindBugs的Ant版本中支持排除和包含过滤器。 顾名思义,您可以使用排除过滤器排除某些错误的报告。 较不流行但仍然有用的包含过滤器只能用于报告目标错误。 过滤器在XML文件中定义。 可以在命令行中使用exclude或include开关或通过在Ant构建文件中使用excludeFilter
和includeFilter
来指定它们。 在以下示例中,假定使用了排除开关。 另外请注意,在下面的讨论中,我会互换使用“错误代码”,“错误”和“检测器”。
可以通过多种方式定义过滤器:
- 符合您的课程之一的过滤条件。 这些过滤器可用于忽略在特定类中发现的所有问题。
- 与您的某一类中的特定错误代码匹配的过滤器。 这些过滤器可用于忽略在特定类中发现的一些错误。
- 与一组错误匹配的过滤器。 这些过滤器可用于忽略所有已分析类中的一组错误。
- 与所分析的类之一中的特定方法匹配的过滤器。 这些过滤器可用于忽略在类的一组方法中发现的所有错误。
- 筛选器匹配在所分析的类之一的方法中发现的一些错误。 您可以使用这些过滤器来忽略在一组特别有问题的方法中发现的一些错误。
这就是开始的全部。 有关可自定义FindBugs任务的其他方式,请参见FindBugs文档。 既然我们知道如何设置构建文件,那么让我们仔细看看如何将FindBugs集成到您的构建过程中。
将FindBugs集成到您的构建过程中
在将FindBugs集成到构建过程中时,您有几种选择。 您始终可以从命令行执行FindBugs,但是很有可能已经在使用Ant进行构建,因此使用FindBugs Ant任务是最自然的。 因为我们已经介绍了使用的FindBugs Ant任务的基础知识早期 ,我将介绍一些你应该添加的FindBugs到您的构建过程,并讨论一些你可能会遇到的问题的原因。
为什么要将FindBugs集成到构建过程中?
经常被问到的第一个问题是为什么我要在我的构建过程中添加FindBugs? 尽管有很多原因,但是最明显的答案是您希望确保在构建运行后立即发现问题。 随着您的团队的成长,您不可避免地向该项目添加了更多的初级开发人员,FindBugs可以充当安全网,检测已确定的错误模式。 我想重申一下FindBugs论文之一中表达的一些观点。 如果您将足够多的开发人员组合在一起,那么您的代码中将会有错误。 像FindBugs这样的工具当然不能找到所有的bug,但是它们会帮助您找到其中的一些。 现在找到一些比客户以后找到它们更好-特别是在将FindBugs集成到您的构建过程中的成本如此低的情况下。
一旦确定了要包括的过滤器和类,运行FindBugs的成本就可以忽略不计,它具有检测新错误的附加好处。 如果您编写了特定于应用程序的检测器,则好处可能更大。
产生有意义的结果
重要的是要认识到,这种成本/收益分析仅在您不会产生很多误报的情况下才有效。 换句话说,如果不再容易地确定是否引入了新的错误,那么该工具的价值就会降低。 您的分析越自动化,越好。 如果要修复错误意味着必须检查很多不相关的错误,那么您可能不会经常使用该工具,或者至少不会充分利用它。
确定您不关心的问题集,并将其排除在构建之外。 否则,选择一个小组检测的,你做的关心和运行只是那些。 另一种选择是从单个类别中排除检测器集,但从其他类别中排除。 FindBugs通过使用过滤功能提供了很多灵活性,这应该可以帮助您生成对您的团队有意义的结果,从而使我们进入下一部分。
确定如何处理FindBugs的结果
看起来似乎很明显,但是我与更多的团队合作,超出了您的想象,他们显然在其构建中添加了类似于FindBugs的工具,以获取纯粹的乐趣。 让我们更详细地探讨这个问题-您应该如何处理结果? 具体来说,这是一个很难回答的问题,因为它与团队的组织方式,代码所有权问题的处理方式等息息相关。 但是,以下是一些准则:
- 您可能需要考虑将FindBugs结果添加到源代码管理(SCM)系统。 一般的经验法则是不要将构建工件放入SCM系统中。 但是,在这种特殊情况下,违反规则可能是正确的做法,因为它使您可以随时间监视代码质量。
- 您可以选择将XML结果文件转换为HTML报告,然后将其发布到团队的网站上。 可以使用XSL样式表或脚本进行转换。 请查看FindBugs网站或邮件列表以获取示例(请参阅参考资料 )。
- 诸如FindBugs之类的工具通常可以转化为用于大举团队或个人的政治武器。 尽量不要鼓励或让它发生-请记住,这只是一个旨在帮助您提高代码质量的工具。 除了鼓舞人心的内容,在下个月的文章中,我将向您展示如何编写自定义错误检测器。
摘要
我鼓励您在代码上尝试某种形式的静态分析工具,无论是FindBugs,PMD还是其他工具。 它们是可以发现实际问题的有价值的工具,而FindBugs是消除误报的更好工具之一。 此外,其可插拔架构为编写宝贵的专用探测器提供了一个有趣的测试平台。 在本系列的第2部分中,我将向您展示如何编写自定义检测器以发现特定于应用程序的问题。
翻译自: https://www.ibm.com/developerworks/java/library/j-findbug1/index.html