为了正常的体验网站,请在浏览器设置里面开启Javascript功能!

FindBugs测试程序

2017-09-20 22页 doc 227KB 11阅读

用户头像

is_601191

暂无简介

举报
FindBugs测试程序FindBugs第1部分: 提高代码质量 静态分析工具承诺无需开发人员费劲就能找出代码中已有的缺陷。当然,如果有多年的编写经验,就会知道这些承诺并不是一定能兑现。尽管如此,好的静态分析工具仍然是工具箱中的无价之宝。在这个由两部分组成的系列文章的第一部分中,高级软件工程师 Chris Grindstaff 分析了 FindBugs 如何帮助提高代码质量以及排除隐含的缺陷。 代码质量工具的一个问题是它们容易为开发人员提供大量但并非真正问题的问题——即 伪问题(false positives)。出现伪问题时,开发人员要学会忽略工具的...
FindBugs测试程序
FindBugs第1部分: 提高代码质量 静态分析工具承诺无需开发人员费劲就能找出代码中已有的缺陷。当然,如果有多年的编写经验,就会知道这些承诺并不是一定能兑现。尽管如此,好的静态分析工具仍然是工具箱中的无价之宝。在这个由两部分组成的系列文章的第一部分中,高级软件工程师 Chris Grindstaff 分析了 FindBugs 如何帮助提高代码质量以及排除隐含的缺陷。 代码质量工具的一个问题是它们容易为开发人员提供大量但并非真正问题的问题——即 伪问题(false positives)。出现伪问题时,开发人员要学会忽略工具的输出或者放弃它。FindBugs 的设计者 David Hovemeyer 和 William Pugh 注意到了这个问题,并努力减少他们所报告的伪问题数量。与其他静态分析工具不同,FindBugs 不注重样式或者格式,它试图只寻找真正的缺陷或者潜在的性能问题。 FindBugs 是什么? FindBugs 是一个静态分析工具,它检查类或者 JAR 文件,将字节码与一组缺陷模式进行对比以发现可能的问题。有了静态分析工具,就可以在不实际运行程序的情况对软件进行分析。不是通过分析类文件的形式或结构来确定程序的意图,而是通常使用 Visitor 模式(请参阅 参考资料)。图 1 显示了分析一个匿名项目的结果(为防止可怕的犯罪,这里不给出它的名字): 图 1. FindBugs UI 让我们看几个 FindBugs 可以发现的问题。 本系列的第二篇文章“ 编写自定义检测器”解释了如何编写自定义检测器, 以便发现特定于应用程序的问题。 回页首 问题发现的例子 下面的列表没有包括 FindBug 可以找到的 所有问题。相反,我侧重于一些更有意思的问题。 检测器:找出 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() 方法。 检测器:忽略方法返回值 这个检测器查找代码中忽略了不应该忽略的方法返回值的地方。这种情况的一个常见例子是在调用 String 方法时,如在清单 1 中: 清单 1. 忽略返回值的例子 1  String aString = "bob"; 2  b.replace('b', 'p'); 3  if(b.equals("pop")) 这个错误很常见。在第 2 行,程序员认为他已经用 p 替换了字符串中的所有 b。确实是这样,但是他忘记了字符串是不可变的。所有这类方法都返回一个新字符串,而从来不会改变消息的接收者。 检测器:Null 指针对 null 的解引用(dereference)和冗余比较 这个检测器查找两类问题。它查找代码路径将会或者可能造成 null 指针异常的情况,它还查找对 null 的冗余比较的情况。例如,如果两个比较值都为 null,那么它们就是冗余的并可能表明代码错误。FindBugs 在可以确定一个值为 null 而另一个值不为 null 时,检测类似的错误,如清单 2 所示: 清单 2. Null 指针示例 1  Person person = aMap.get("bob"); 2  if (person != null) { 3      person.updateAclearcase/" target="_blank" >ccessTime(); 4  } 5  String name = person.getName();   在这个例子中,如果第 1 行的 Map 不包括一个名为“bob”的人,那么在第 5 行询问 person 的名字时就会出现 null 指针异常。因为 FindBugs 不知道 map 是否包含“bob”,所以它将第 5 行标记为可能 null 指针异常。 检测器:初始化之前读取字段 这个检测器寻找在构造函数中初始化之前被读取的字段。这个错误通常是——尽管不总是如此——由使用字段名而不是构造函数参数引起的,如清单 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 行将产生一个 null 指针异常,因为变量 actions 还没有初始化。 这些例子只是 FindBugs 所发现的问题种类的一小部分(更多信息请参阅 参考资料)。在撰写本文时,FindBugs 提供总共 35 个检测器。 回页首 开始使用 FindBugs 要运行 FindBugs,需要一个版本 1.4 或者更高的 Java Development Kit (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 method的说明。对每一种缺陷模式提供了类似的说明,在第一次熟悉这种工具时这是很有用的。窗口下面的 Source code 选项卡也同样有用。如果告诉 FindBugs 在什么地方寻找代码,它就会在转换到相应的选项卡时突出显示有问题的那一行。 值得一提的还有在将 FinBugs 作为 Ant 任务或者在命令行中运行 FindBugs 时,选择 xml 作为 ouput 选项,可以将上一次运行的结果装载到 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 后,可以用它的名字 FindBugs 引用它。下一步要在编译中加入使用新任务的目标,如清单 4 所示: 清单 4. 创建 FindBugs 目录 1  2      3          4          5          6      让我们更详细地分析这段代码中所发生的过程。 第 1 行: 注意 target 取决于编译。一定要记住处理的是类文件而 不 是源文件,这样使 target 对应于编译目标保证了 FindBugs 可在最新的类文件运行。FindBugs 可以灵活地接受多种输入,包括一组类文件、JAR 文件、或者一组目录。 第 2 行:必须指定包含 FindBugs 的目录,我是用 Ant 的一个属性完成的,像这样:   可选属性 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,有一个真正需要的(honest-to-goodness)、合法的理由调用 System.gc() 。 总是可以选择“关闭”特定的检测器。在更细化的水平上,可以在指定的一组类甚至是方法中查找问题时,排除某些检测器。FindBugs 提供了这种细化的控制,可以排除或者包含过滤器。当前只有用命令行或者 Ant 启动的 FindBugs 中支持排除和包含过滤器。正如其名字所表明的,使用排除过滤器来排除对某些缺陷的报告。较为少见但仍然有用的是,包含过滤器只能用于报告指定的缺陷。过滤器是在一个 XML 文件中定义的。可以在命令行中用一个排除或者包含开关、或者在 Ant 编译文件中用 excludeFilter 和 includeFilter 指定它们。在下面的例子中,假定使用排除开关。还要注意在下面的讨论中,我对 “bugcode”、“bug” 和“detector”的使用具有某种程度的互换性。 可以有不同的方式定义过滤器: ∙ 匹配一个类的过滤器。可以用这些过滤器 忽略在特定类中发现的所有问题。 ∙ 匹配一个类中特定缺陷代码(bugcode)的 过滤器。可以用这些过滤器忽略在特定类中发现的一些缺陷。 ∙ 匹配一组缺陷的过滤器。可以用这些过滤器 忽略所分析的所有类中的一组缺陷。 ∙ 匹配所分析的一个类中的某些方法的过滤器。可以用这些过滤器忽略在一个类中的一组方法中发现的所有缺陷。 ∙ 匹配在所分析的一个类中的方法中发现的某些缺陷的过滤器。可以用这些过滤器忽略在一组方法中发现的特定缺陷。 知道了这些就可以开始使用了。有关其他定制 FindBugs 方法的更多信息,请参阅 FindBugs 文档。知道如何设置编译文件以后,就让我们更详细地分析如何将 FindBugs 集成到编译过程中吧! 回页首 将 FindBugs 集成到编译过程中 在将 FindBugs 集成到编译过程当中可以有几种选择。总是可以在命令行执行 FindBugs,但是您很可能已经使用 Ant 进行编译,所以最自然的方法是使用 FindBugs Ant 任务。因为我们在 如何运行 FindBugs一节中讨论了使用 FindBugs Ant 任务的基本内容,所以现在讨论应当将 FindBugs 加入到编译过程中的几个理由,并讨论几个可能遇到的问题。 为什么应该将 FindBugs 集成到编译过程中? 经常问到的第一个问题是为什么要将 FindBugs 加入到编译过程中?虽然有大量理由,最明显的回答是要保证尽可能早地在进行编译时发现问题。当团队扩大,并且不可避免地在项目中加入更多新开发人员时,FindBugs 可以作为一个安全网,检测出已经识别的缺陷模式。我想重申在一篇 FindBugs 中表述的一些观点。如果让一定数量的开发人员共同工作,那么在代码中就会出现缺陷。像 FindBugs 这样的工具当然不会找出所有的缺陷,但是它们会帮助找出其中的部分。现在找出部分比客户在以后找到它们要好——特别是当将 FindBugs 结合到编译过程中的成本是如此低时。 一旦确定了加入哪些过滤器和类,运行 FindBugs 就没什么成本了,而带来的好处就是它会检测出新缺陷。如果编写特定于应用程序的检测器,则这个好处可能更大。 生成有意义的结果 重要的是要认识到这种成本/效益分析只有在不生成大量误检时才有效。换句话说,如果在每次编译时,不能简单地确定是否引入了新的缺陷,那么这个工具的价值就会被抵消。分析越自动化越好。如果修复缺陷意味着必须吃力地分析检测出的大量不相干的缺陷,那么您就不会经常使用它,或者至少不会很好地使用它。 确定不关心哪些问题并从编译中排除它们。也可以挑出 确实关注的一小部分检测器并只运行它们。另一种选择是从个别的类中排除一组检测器,但是其他的类不排除。FindBugs 提供了使用过滤器的极大灵活性,这可帮助生成对团队有意义的结果,由此我们进入下一节。 确定用 FindBugs 的结果做什么 可能看来很显然,但是您想不到我参与的团队中有多少加入了类似 FindBugs 这样的工具而没有真正利用它。让我们更深入地探讨这个问题——用结果做什么?明确回答这个问题是困难的,因为这与团队的组织方式、如何处理代码所有权问题等有很大关系。不过,下面是一些指导: ∙ 可以考虑将 FindBugs 结果加入到源代码管理(SCM)系统中。一般的经验做法是不将编译工件(artifact)放到 SCM 系统中。不过,在这种特定情况下,打破这个规则可能是正确的,因为它使您可以监视代码质量随时间的变化。 ∙ 可以选择将 XML 结果转换为可以发送到团队的网站上的 HTML 报告。转换可以用 XSL 样式表或者脚本实现。有关例子请查看 FindBugs 网站或者邮件列表(请参阅 参考资料)。 ∙ 像 FindBugs 这样的工具通常会成为用于敲打团队或者个人的政治武器。尽量抵制这种做法或者不让它发生——记住,它只是一个工具,它可以帮助改进代码的质量。有了这种思想,在下一部分中,我将展示如何编写自定义缺陷检测器。 回页首 结束语 我鼓励读者对自己的代码试用静态分析工具,不管是 FindBugs、PMD 还是其他的。它们是有用的工具,可以找出真正的问题,而 FindBugs 是在消除误检方面做得最好的工具。此外,它的可插入结构提供了编写有价值的、特定于应用程序的检测器的、有意思的测试框架。在本系列的 第 2 部分中,我将展示如何编写自定义检测器以找出特定于应用程序的问题。 FindBugs,第 2 部分: 编写自定义检测器 FindBugs,第 2 部分: 编写自定义检测器 FindBugs,第 2 部分: 编写自定义检测器 如何编写自定义检测器以查找特定于应用程序的问题 FindBugs 是一种可以扩展和定制以满足自己团队独特要求的静态分析工具。在本系列的第 2 部分中,高级软件工程师 Chris Grindstaff 向您展示如何创建特定于应用程序的缺陷检测器。 1 评论: Chris Grindstaff (chris@gstaff.org), 软件工程师 关闭 [x] Chris Grindstaff 是在北加利福尼亚 Research Triangle Park 工作的 IBM 高级软件工程师。Chris 在 7 岁时编写了他的第一个程序,当时他让小学老师认识到“键入”句子与手写它们一样费力。Chris 目前参与了不同的开放源代码项目。他大量使用 Eclipse 并编写了几个流行的 Eclipse 插件程序,可以在他的 网站找到这些插件程序。可以通过 cgrinds@us.ibm.com或者 chris@gstaff.org与 Chrise 联系。 2004 年 6 月 01 日 ∙ 内容 o 编写自定义缺陷检测器 o 结束语 o 参考资料 o 评论 在本系统的 第一篇文章中,我展示了如何设置和执行 FindBugs。现在我们将分析 FindBugs 最强大的功能——自定义检测器。首先,我将说明为什么自定义缺陷检测器很有用,然后我将引导读者完成一个详细的例子。 编写自定义缺陷检测器 为什么要编写自定义缺陷检测器?我在被要求对一个小组的性能问题进行检查时遇到了这个问题。很显然小组自已开发的日志框架(像所有日志框架一样)随着时间而增大。原来是随意地大量调用 Logger。不幸的是,随着小组的扩大,他们的应用程序的性能也在变差,因为他们总是产生昂贵的日志消息——而当日志框架发现禁用日志后,这些消息只能是被它抛弃。解决这个问题的标准方式是在构造昂贵的日志消息之前,首先检查是否启用了日志。换句话说,使用一个像清单 1 这样的监护子句: 清单 1. 监护日志示例 if(Logger.isLogging()) {     Logger.log("perf", anObjectWithExpensiveToString + anotherExpensiveToString); } 本系列的第一篇文章“ 改进代码质量”介绍了 FindBugs,这是一个检查类或者 JAR 文件以发现可能存在的问题的静态分析工具,并展示了如何有效地使用它。 这个小组认定这是一种恰当的日志做法,并对现有的代码加以改变以体现新的做法。在这个非常大的项目的截止时间快到时听到还有很多地方未改完是不会让人感到意外的。小组需要有更好的方法找出尚未修改的地方。因为本文讨论的是 FindBugs,所以我们将用 FindBugs 解决这个问题。 目标是编写一个 FindBugs 检测器,它将找出代码中调用日志框架而未被包装在监护子句中的所有地方。 当初编写这个检测器时,我将问题分解为几个单独的步骤: 1. 首先用一条未监护的日志语句编写一个测试案例。 2. 其次,查看 FindBugs 源代码以查找类似于我要编写的检测器类似的检测器。 3. 然后创建正确打包的 JAR 文件(使用编译脚本),使 FindBugs 知道如何装载未监护检测器。 4. 运行这个测试案例并实现代码使测试通过。 5. 最后,加入更多测试案例,继续这个过程直到最后完成。 在浏览代码时,我特别检查了 BytecodeScanningDetector和 ByteCodePatternDetector的子类型。实现扫描检测器要做更多工作,但是它们能检测更一般类型的问题。如果所要检测的问题可以表述为一组字节码模式,则模式检测器是一种好的选择。它的一个好例子是 BCPMethodReturnCheck检测器——它查找那些不同方法的返回类型有可能被忽略的地方。 BCPMethodReturnCheck可以很容易地描述为一组模式,它查找在 POP或者 POP2指令后面调用某些方法的地方。绝大多数检测器目前被编写为扫描检测器,尽管我认为这仅仅是因为开发人员还没有足够的时间转移到 ByteCodePatternDetector。 我决定使用 FindRunInvocations作为例子,主要是因为它是最小的一种检测器。对我来说如何实现使用一组模式的检测器还不是很明确。 FindBugs 利用了 Byte Code Engineering Library,或称为 BCEL (请参阅 参考资料),以实现其检测器。所有字节码扫描检测器都基于 visitor 模式,FindBugs 实现了这个模式。它提供了这些方法的默认实现,在实现自定义检测器时要覆盖这些方法。请分析 BetterVisitor及其子类以获得更多细节。出于我们的目的,我们将侧重于两个方法—— visit(Code)和 sawOpcode(int)。在 FindBugs 分析类时,它会在分析方法内容时调用 visit(Code)方法。与此类似,FindBugs 在分析方法正文中的每一个操作码时调用 sawOpcode(int)方法。 有了这些背景知识,让我们分析这些用于构建未监护日志检测器的方法的实现,如清单 2 所示: 清单 2. 未监护日志检测器:visit() 方法 18      public void visit(Code code) { 19          seenGuardClauseAt = Integer.MIN_VALUE; 20          logBlockStart = 0; 21          logBlockEnd = 0; 22          super.visit(code); 23      } 在读取现有检测器代码时,需要做的一件事是关注检测器是否需要在分析时建立状态。换句话说,检测器是否需要记住它在方法、类、层次结构或者整个程序级别上看到了什么?例如, Inconsistent Synchronization检测器构建整个程序的状态,这样它就可确定在同步方面,什么时候以非一致性的方式对字段进行了访问。我们的检测器只需要在字节码扫描阶段维护状态,因为我们查找的是方法级的问题。 可以在方法 visit(Code)中刷新或者重新设置检测器存储的、特定于方法的状态(如清单 2 所示)。在这里,检测器维护了一个使用三位(bit)的状态: ∙ seenGuardClauseAt:在所分析的代码中发现日志监护子句时,程序计数器的值 ∙ logBlockStart:监护子句开始处的索引 ∙ logBlockEnd:监护子句后面的指令的索引 关于 visit(Code)方法的实现有两点很重要。要注意的第一件事是对 super.visit()的调用,它是关键,因为这个方法的父类实现负责访问我们要分析的方法的内容。如果我们没有调用父类的实现,那么就不会检查所分析的方法。 第二点是在调用父类的实现之前重新设置存储的状态,这很重要,因为我们将分析的下一个方法—— sawOpcode()方法——将要使用这些变量。我们希望保证在这之前对它们作了重新设置。清单 3 显示了 sawOpcode()方法的实现: 清单 3. 未监护日志检测器:sawOpcode() 方法 25  public void sawOpcode(int seen) { 26      if ("cbg/app/Logger".equals(classConstant) && 27          seen == INVOKESTATIC && 28          "isLogging".equals(nameConstant) && "()Z".equals(sigConstant)) { 29              seenGuardClauseAt = PC; 30              return; 31      } 32      if (seen == IFEQ && (PC >= seenGuardClauseAt + 3 &&             PC < seenGuardClauseAt + 7)) { 33          logBlockStart = branchFallThrough; 34          logBlockEnd = branchTarget; 35      } 36      if (seen == INVOKEVIRTUAL && "log".equals(nameConstant)) { 37          if (PC < logBlockStart || PC >= logBlockEnd) { 38              bugReporter.reportBug( 39                  new BugInstance("CBG_UNPROTECTED_LOGGING", HIGH_PRIORITY) 40                  .addClassAndMethod(this).addSourceLine(this)); 41          } 42      } 43  } 如前所述,当 FindBugs 分析一个方法时,它会对方法中包含的每一个字节码指令调用 sawOpcode()。这个方法做三件事。事实上,原来的代码被重构为三个方法,但是在本文的讨论中,我把它排在一行以减少所占用的空间。这个方法做三件事: 1. 确定是否调用了 static 方法 Logger.isLogging(),如果调用了,程序计数器 (PC) 的值是什么 2. 确定在调用 Logger.isLogging() 后是否有一个 if指令 3. 寻找在监护子句外部调用 log()方法的情况 清单 4 更详细地分别显示了每一部分: 清单 4. 未监护日志检测器:调用了 sawOpcode()、isLogging() 25      public void sawOpcode(int seen) { 26          if ("cbg/app/Logger".equals(classConstant) && 27                  seen == INVOKESTATIC && 28                  "isLogging".equals(nameConstant) && "()Z".equals(sigConstant)) { 29              seenGuardClauseAt = PC; 30              return; 31          } classConstant、 nameConstant和 sigConstant字段是检测器从其父类继承的 protected 字段。它们包含有关当前操作码的细节。在编写自己的检测器时,打印出它们的值通常是有用的。浏览 BytecodeScanningDetector层次结构以寻找 DismantleBytecode类中更有用的字段和方法。另一个编写检测器可以使用的非常有用的工具是永久的 javap。对于理解编写检测器的逻辑和方法名,Java 反汇编程序是非常有用的工具。一般的方式是编写要查找的模式(在这里是编写 Java 文件中的监护子句)、保存它、再编译它。然后使用 javap -c以查看反汇编的字节码,并学习如何构造自己的 sawOpcode(int)方法。例如,清单 5 显示了对我的测试实例中使用的类运行 javap后的输出(这是一个正确使用日志监护子句的方法): 清单 5. 反汇编的监护子句及源代码 public void methodWithLogging_guarded();   Code:   0:  invokestatic    #28; //Method cbg/app/Logger.isLogging:()Z   3:  ifeq    18   6:  new    #16; //class Logger   9:  dup   10:  invokespecial  #17; //Method cbg/app/Logger."":()V   13:  ldc    #19; //String bob   15:  invokevirtual  #23; //Method cbg/app/Logger.log:(Ljava/lang/Object;)V   18:  aload_0   19:  invokespecial  #31; //Method doWork:()V   22:  return   corresponds to the Java source code   public void methodWithLogging_guarded() {         if (Logger.isLogging()) {             new Logger().log("bob");         }         doWork();     } 分析 javap的输出有助于理解方法的控制流程序以及如何构建需要在 sawOpcode()方法中指定的类、签名和名称常量。例如,清单 6 显示清单 5 中 javap的第一行代码 清单 6. 反汇编的方法调用 0:  invokestatic    #28; //Method cbg/app/Logger.isLogging:()Z 如果仔细观察 清单 4中 sawOpcode()方法的第 26 到 28 行,将会看到它们描述了一种与我们在 清单 5 中用 javap见到的内容相匹配的方式。在确定如何匹配这些格式(form )时, javap是一个有用的工具。 确定已经调用了 Logger.isLogging()方法后,我们要保存程序计数器的值,如清单 7 所示。需要用程序计数器确定在调用 Logger.isLogging()后,是否有一个 if子句,这将我们带入下一节的代码。 清单 7. 保存程序计数器的值 32  if (seen == IFEQ && (PC >= seenGuardClauseAt + 3 &&         PC < seenGuardClauseAt + 7)) { 33      logBlockStart = branchFallThrough; 34      logBlockEnd = branchTarget; 35  } 这段摘自 清单 3的代码检查在上述对 Logger.isLogging()的调用后面 3 到 7 字节码之间是否有 if分支语句。这些值是通过查看 javap的输出和通过试验确定的。你是说试验?没错,有些时候必须借助于反复试验才能找到伪错误和有用的结果之间的平衡点。将这个过程想像为使用试探法的计算机工程而不是计算机科学。确定了这个语句是 if(Logger.isLogging())语句后,我们需要找出 if代码块的边界。这是通过保存 branchFallThrough和 branchTarget而做到的。 branchFallThrough是 if子句的开始,而 branchTarget代表 if子句外面的第一行。有了这些信息,我们现在就可以进入这个方法的最后一部分了,如清单 8 所示: 清单 8. 检查对 log() 的调用 36          if (seen == INVOKEVIRTUAL && "log".equals(nameConstant)) { 37              if (PC < logBlockStart || PC >= logBlockEnd) { 38                  bugReporter.reportBug( 39                          new BugInstance("CBG_UNPROTECTED_LOGGING", HIGH_PRIORITY) 40                          .addClassAndMethod(this).addSourceLine(this)); 41              } 42          } 这段代码也取自 清单 3,查找对 Logger的 log()方法的调用。找到对 log()方法的调用后,我们检查程序计数器是否在前面确定的 if块外面。如果是的话,我们就通过创建一个新的 bug 实例报告一个缺陷,指定 bug 的类型(我们将在后面详细讨论)和其优先级。在 bug 中加入类、方法和源代码会提供很大方便,这样用户就知道在什么地方修复这个问题。 编写了代码后,需要创建一个特别打包的 JAR 文件,FindBugs 将它识别为插件程序 JAR。清单 9 显示了我用来创建这个 JAR 文件并将它拷贝到正确位置的编译脚本: 清单 9. 打包 FindBugs 检测器的脚本                                     这段代码创建一个包含源文件、类文件、FindBugs.xml 和 message.xml 的 JAR 文件。清单 10 和 11 显示了这两个 XML 文件的内容: 清单 10. FindBugs.xml 的内容     对于每一个新的检测器,在 FindBugs.xml 文件中增加一个 Detector元素和一个 BugPattern元素。 Detector元素指定用于实现检测器的类以及它是快速还是慢速检测器。在 UI 中查看检测器时就会用到 speed 属性,如图 1 所示。speed 属性的可能值有 slow、moderate 和 fast。 图 1. 配置检测器 UI BugPattern元素指定三个属性。 abbrev属性定义检测器的缩写。缩写用于标识用命令行客户运行时检测到的缺陷。可以用同一个缩写将几个相关的检测器组织到一起。. type属性是惟一标识符,有两个用途。在使用 Ant 版本或者命令行版本的 FindBugs 且输出格式设置为 XML 时,用 type属性标识问题。 type属性也是在检测器的 Java 代码中指定的,用以创建缺陷的正确类型。注意这里列出的类型与在 清单 8中第 39 行使用的名字相匹配。. category属性是枚举类型。它是以下类型中的一种: ∙ CORRECTNESS:一般正确性问题 ∙ MT_CORRECTNESS:多线程正确性问题 ∙ MALICIOUS_CODE:如果公开给恶意代码,有可能成为攻击点 ∙ PERFORMANCE:性能问题 FindBugs.xml 文件就是这些了。清单 11 显示了 messages.xml 文件的内容: 清单 11. messages.xml 的内容      
This detector finds logs statements that aren't contained in an if-logging block. It is a fast detector. ]]>    
 
      Found unprotected logging     Found unprotected logging in {1}    
This method logs without first checking that logging is enabled; for example ... more text omitted... ]]>    
 
    Found unprotected logging
messages.xml 文件由三个元素组成: Detector、 BugPattern和 BugCode。 检测器的 class属性应当指定检测器的类名。 Details元素包含检测器的简单 HTML 描述,因而应当包含在 CDATA部分中。UI 使用这些描述,如图 2 所示: 图 2. FindBugs UI 突出显示未监护日志检测器 BugPattern元素类似于在 FindBugs.xml 中定义的 BugPattern元素。需要 type属性,并且它应当匹配在 FindBugs.xml 和在检测器的 Java 代码中使用的相同惟一标识符。 BugPattern包含三个影响有关检测器的信息在 UI 中显示方式的元素: ShortDescription、 LongDescription和 Details——它们的意义都是相当直观的。 在 UI 中关闭 View > Full Descriptions 时,使用 ShortDescription。同样,在启用 View > Full Descriptions 时,使用 LongDescription。可以使用注释(annotation)将信息从缺陷检测器的 Java 代码中传递给完全描述。在描述中,用 {0}表示第一个注释、 {1}表示第二个注释等来指定变量。在运行时,如果发现缺陷,附加在缺陷实例上的注释将替换到描述中。注意在 清单 8的第 40 行,类和方法注释添加到了 BugInstance上。类注释在位置 0,方法注释在位置 1。更多细节请查看 BugInstance上的不同 add*()方法。 与前面一样, Details元素应当在 CDATA部分中包含一个 HTML 描述。图 2 显示了我们的检测器细节的一个例子。View > Full Descriptions 已经打开。 在使用 By Bug Type 选项卡时,UI 使用 BugCode元素。这个元素的文字在树中作为红色节点出现,如 图 2所示。公共检测器共享同一个缩写,因此 BugCode元素必须用元素的属性指定这个缩写。 创建了这两个 XML 文件后,我们现在就可以打包完整的 JAR 了。在编译了 JAR 并将它放到 FIND_BUGS_HOME\plugin 目录中后,就可以测试新的检测器了。 特定于应用程序的缺陷检测器 FindBugs 会是您的装备库中一件有用的工具。但是,像所有工具一样,必须知道如何使用它。不过,静态分析工具应作为单元 / 系统测试和代码审查的补充。 除了其改进代码质量的作用,FindBugs 还有很多特定于应用程序的用法,我鼓励读者去探索它们。例如,可以编写一组检测器,它们可以查找新手容易出现的问题。也可以编写检查代码是否符合小组规则的检测器。也许您正在构建一个框架,并且需要保证包中的所有类都有零参数的构造函数、或者所有带下划线前缀的字段都有 getter 而没有 setter。也许可以编写一组检测器,它们验证 J2EE 代码遵守适当的限制,如不创建 Thread或者 Socket。 未监护日志例子中的小组还有捕获异常的问题。值得称赞的是,他们没有简单忽略这些异常,相反,他们让它们打印自己的堆栈跟踪,这对于编译和调试应用程序很好,但是对于部署来说这不是很理想——特别是当可能有数千个异常时。(当然,如果应用程序抛出数千个异常,您的问题就比大的日志文件要严重得多了,但是姑且容许我为了说明问题而这样说。)小组需要一个检测器来找出代码中捕获异常并要求打印其堆栈跟踪的地方。这样他们就可以改变代码,将异常改为传递给日志框架。 我创建了另外一个有趣的未监护日志检测器。这个检测器用于寻找代码中所有在监护子句以外生成要记录消息的地方——如果使用了行为有些特别的 toString,这会是一个非常常见的问题,并且代价有可能相当昂贵。 回页首 结束语 不管是刚接触 FindBugs 还是已经熟悉它了,我鼓励您用自己的特定于应用程序的检测器进行试验。同时,我希望本文提供如何实现自定义检测器的简洁例子,并鼓励您将这些思路应用到小组的特定情况中去。 FindBugs,第 2 部分: 编写自定义检测器FindBugs,第 2 部分: 编写自定义检测器
/
本文档为【FindBugs测试程序】,请使用软件OFFICE或WPS软件打开。作品中的文字与图均可以修改和编辑, 图片更改请在作品中右键图片并更换,文字修改请直接点击文字进行修改,也可以新增和删除文档中的内容。
[版权声明] 本站所有资料为用户分享产生,若发现您的权利被侵害,请联系客服邮件isharekefu@iask.cn,我们尽快处理。 本作品所展示的图片、画像、字体、音乐的版权可能需版权方额外授权,请谨慎使用。 网站提供的党政主题相关内容(国旗、国徽、党徽..)目的在于配合国家政策宣传,仅限个人学习分享使用,禁止用于任何广告和商用目的。

历史搜索

    清空历史搜索