内容


用 PMD 铲除 bug

用这个方便的静态分析工具“在臭虫咬人之前发现它”

Comments

Tom Copeland 的 PMD 是一个开源(BSD 许可)工具,它分析 Java 源代码,找出潜在的 bug。在一般意义上来说,它与 FindBugs 和 Lint4j 这类工具类似(请参阅 参考资料)。但是,所有这些工具找出的 bug 各不相同,所以在给定代码基址上把这些工具都运行一遍很有好处。在本文中,我将解释如何使用 PMD,并展示可以从 PMD 中获得什么。本文将介绍 PMD 的命令行界面。您也可以把 PMD 与 Ant 集成在一起,以便进行自动源代码检查,而且还可以将 PMD 与一些可用于大多数主要 IDE 和程序员编辑器的插件集成在一起。

安装并运行 PMD

PMD 是用 Java 编程语言编写的,并且要求使用 JDK 1.3 或更高的版本。如果您习惯使用命令行,那么 PMD 的安装和运行会非常简单。先下载 zip 压缩文件(请参阅 参考资料),然后把它解压到合适的位置,比如 /usr 或您自己的主目录中。本文假设您把它解压到 /usr 中。

运行 PMD 最简单的方法是调用脚本 pmd.sh(在 Unix/Linux 上)或脚本 pmd.bat(在 Windows 上)。不太合常规的是,这些脚本在 pmd-2.1/etc 目录中,而不是在 bin 目录中。这个脚本采用了三个命令行参数:

  • 要检查的 .java 文件的路径。
  • 指定输出格式的关键字 htmlxml
  • 要运行的规则集的名称。

例如,以下命令使用命名规则集检查 ImageGrabber.java 文件并生成 XML 输出:

$ /usr/pmd-2.1/etc/pmd.sh ImageGrabber.java xml rulesets/naming.xml

分析结果

以上命令的输出类似于清单 1 中的报告,如下所示,默认情况下,这些输出被发送到 System.out

清单 1. PMD 的 XML 报告
<?xml version="1.0"?><pmd>
<file name="/Users/elharo/src/ImageGrabber.java">
<violation line="32" rule="ShortVariable" 
           ruleset="Naming Rules" priority="3">
Avoid variables with short names like j
</violation>
<violation line="105" rule="VariableNamingConventionsRule" 
           ruleset="Naming Rules" priority="1">
Variables that are not final should not contain underscores 
(except for underscores in standard prefix/suffix).
</violation>
</file>
<error filename="/Users/elharo/src/ImageGrabber.java" 
  msg="Error while processing /Users/elharo/ImageGrabber.java"/>
</pmd>

在清单 1 中可以看到,PMD 发现了两个问题:在 ImageGrabber.java 的第 32 行有一个短变量名称,在第 105 行的名称中有一个下划线。这些看起来可能是小问题,但是后果却可能是惊人的。在这个例子中,105 行的下划线只是一个有 10 年之久的老代码中一些容易修正的小毛病。但是仔细考察第一个问题,使我认识到可以完全排除 j 变量,因为它与另外一个单独递增变量的功能相同。这个程序仍然有用,但是它应该更简洁一些,以便应对以后的挑战。每当清除一行代码,也就减少了一个可能隐匿 bug 的地方。

您可以把 PMD 的输出重定向到文件中,或者通过管道,以常见的方式将它传递到编辑器中。我通常更喜欢生成 HTML 格式的输出,并将它加载到 Web 浏览器中,如图 1 中所示:

图 1. PMD 用 HTML 格式处理后的输出
PMD 示例输出
PMD 示例输出

在检查源代码树时,把结果输出到文件中会非常有帮助。如果把目录、zip 文件或 JAR 档案文件传递给第一个参数,那么 PMD 会递归地检查目录或档案中的每个 .java 文件。输出的总量可能有些吓人,特别是在 PMD 生成大量误报(false positive)的时候。例如,当针对 XOM(请参阅 参考资料)代码基址运行 PMD 时,它不断地报告应当“Avoid variables with short names like in.”(避免像 in 这样的短变量名)。而我却恰恰认为“in”是指向 InputStream 的变量的非常好的名称。尽管如此,如果用一个好的文档编辑器来查看输出,您通常会发现,可以很容易地认出并删除最常见的误报,因为它们通常非常相似;然后您就可以解决其余的问题了。

PMD 中惟一缺乏的特性就是不能向源代码中添加 “lint 注释”,以便对要执行的一些明显有危险的操作进行提醒。不过,也许这是一项特性,而不是 bug。只有一次,我改变了自己对真正误报的看法,觉得 PMD 始终是正确的。例如,对于一个长时间的 try- catch 块,类似 XOM 中的不同地方出现的那个 try-catch 块:

try {
  this.data = data.getBytes("UTF8");
}
catch (UnsupportedEncodingException ex) {
  // All VMs support UTF-8
}

PMD 把这标记为空 catch 块。这看起来不是什么问题,但是后来我发现某些虚拟机实际上不认识 UTF-8 编码,尽管这会使它们不符合标准。所以我把这个块修改如下,然后 PMD 开始停止报错:

try {
  this.data = data.getBytes("UTF8");
}
catch (UnsupportedEncodingException ex) {
   throw new RuntimeException("Broken VM: Does not support UTF-8");
}

可用的规则

PMD 包含 16 个规则集,涵盖了 Java 的各种常见问题,其中一些规则要比其他规则更有争议:

  • 基本(rulesets/basic.xml)—— 规则的一个基本合集,可能大多数开发人员都不认同它: catch 块不该为空,无论何时重写 equals(),都要重写 hashCode(),等等。
  • 命名(rulesets/naming.xml)—— 对标准 Java 命令规范的测试:变量名称不应太短;方法名称不应过长;类名称应当以小写字母开头;方法和字段名应当以小写字母开头,等等。
  • 未使用的代码(rulesets/unusedcode.xml)—— 查找从未使用的私有字段和本地变量、执行不到的语句、从未调用的私有方法,等等。
  • 设计(rulesets/design.xml)—— 检查各种设计良好的原则,例如: switch 语句应当有 default 块,应当避免深度嵌套的 if 块,不应当给参数重新赋值,不应该对 double 值进行相等比较。
  • 导入语句(rulesets/imports.xml)—— 检查 import 语句的问题,比如同一个类被导入两次或者被导入 java.lang 的类中。
  • JUnit 测试(rulesets/junit.xml)—— 查找测试用例和测试方法的特定问题,例如方法名称的正确拼写,以及 suite() 方法是不是 static 和 public。
  • 字符串(rulesets/string.xml)—— 找出处理字符串时遇到的常见问题,例如重复的字符串标量,调用 String 构造函数,对 String 变量调用 toString() 方法。
  • 括号(rulesets/braces.xml)—— 检查 forifwhileelse 语句是否使用了括号。
  • 代码尺寸(rulesets/codesize.xml)—— 测试过长的方法、有太多方法的类以及重构方面的类似问题。
  • Javabean(rulesets/javabeans.xml)—— 查看 JavaBean 组件是否违反 JavaBean 编码规范,比如没有序列化的 bean 类。
  • 终结函数(finalizer)—— 因为在 Java 语言中, finalize() 方法不是那么普遍(我上次编写这个代码也经是好多年前的事了),所以它们的使用规则虽然很详细,但是人们对它们相对不是很熟悉。这类检查查找 finalize() 方法的各种问题,例如空的终结函数,调用其他方法的 finalize() 方法,对 finalize() 的显式调用,等等。
  • 克隆(rulesets/clone.xml)—— 用于 clone() 方法的新规则。凡是重写 clone() 方法的类都必须实现 Cloneableclone() 方法应该调用 super.clone(),而 clone() 方法应该声明抛出 CloneNotSupportedException 异常,即使实际上没有抛出异常,也要如此。
  • 耦合(rulesets/coupling.xml)—— 查找类之间过度耦合的迹象,比如导入内容太多;在超类型或接口就已经够用的时候使用子类的类型;类中的字段、变量和返回类型过多等。
  • 严格的异常(rulesets/strictexception.xml)—— 针对异常的测试:不应该声明该方法而抛出 java.lang.Exception 异常,不应当将异常用于流控制,不应该捕获 Throwable,等等。
  • 有争议的(rulesets/controversial.xml)—— PMD 的有些规则是有能力的 Java 程序员可以接受的。但还是有一些争议。这个规则集包含一些更有问题的检验,其中包括把 null 赋值给变量、方法中有多个返回点,以及从 sun 包导入等。
  • 日志(rulesets/logging-java.xml)—— 查找 java.util.logging.Logger 的不当使用,包括非终状态(nonfinal)、非静态的记录器,以及在一个类中有多个记录器。

您可以一次用多个规则集进行检查,只需在命令行中用逗号分隔规则集名称即可:

$ /usr/pmd-2.1/etc/pmd.sh ~/Projects/XOM/src html
 rulesets/design.xml,rulesets/naming.xml,rulesets/basic.xml

构建自己的规则集

如果频繁地用某个规则集合进行检查,那么您可能想把它们组合在自己的规则集文件中,如清单 2 所示。这个规则集导入了一些基本规则、命名规则和设计规则:

清单 2. 导入基本规则、命名规则和设计规则的规则集
<?xml version="1.0"?>
<ruleset name="customruleset">
  <description>
  Sample ruleset for developerWorks article
  </description>
  <rule ref="rulesets/design.xml"/>
  <rule ref="rulesets/naming.xml"/>
  <rule ref="rulesets/basic.xml"/>
</ruleset>

如果您的需求还要细一些,那么您可以从每个规则集中选取每个想要包含的规则。例如,清单 3 显示了一个定制规则集,它从三个内置规则集中选取了 11 个特定的规则。因为检查大型的代码基址需要的时间相当长,即使是在快速硬件上也是如此,所以这种方法还可以帮助您更快地发现您要查找的特定问题。

清单 3. 导入 11 个特定规则的规则集
<?xml version="1.0"?>
<ruleset name="specific rules">
  <description>
  Sample ruleset for developerWorks article
  </description>
  <rule ref="rulesets/design.xml/AvoidReassigningParametersRule"/>
  <rule ref=
    "rulesets/design.xml/ConstructorCallsOverridableMethod"/>
  <rule ref="rulesets/design.xml/FinalFieldCouldBeStatic"/>
  <rule ref="rulesets/design.xml/DefaultLabelNotLastInSwitchStmt"/>
  <rule ref="rulesets/naming.xml/LongVariable"/>
  <rule ref="rulesets/naming.xml/ShortMethodName"/>
  <rule ref="rulesets/naming.xml/VariableNamingConventions"/>
  <rule ref="rulesets/naming.xml/MethodNamingConventions"/>
  <rule ref="rulesets/naming.xml/ClassNamingConventions"/>
  <rule ref="rulesets/basic.xml/EmptyCatchBlock"/>
  <rule ref="rulesets/basic.xml/EmptyFinallyBlock"/>
</ruleset>

您也可以把大多数规则包含在一个集合,但是不包括少数您不同意的或者会造成大量误报的特定规则。例如,XOM 在进行表查找的时候经常使用没有 default 块的 switch 语句。我可以保留大多数设计规则,但是可以在导入设计规则的规则元素中添加 <exclude name="SwitchStmtsShouldHaveDefault"/> 子元素,避开对遗漏 default 块的检查,如清单 4 所示:

清单 4. 排除了设计规则的规则集, switch 语句应当包含 default 块
<?xml version="1.0"?>
<ruleset name="dW rules">
  <description>
  Sample ruleset for developerWorks article
  </description>
  <rule ref="rulesets/design.xml">
    <exclude name="SwitchStmtsShouldHaveDefault"/>
  </rule>
</ruleset>

(但是,细想一下,也许 PMD 是对的,而我应该添加 default 块。)

您可用的规则并不仅限于内置规则。您可以添加新规则:可以通过编写 Java 代码并重新编译 PDM,或者更简单些,编写 XPath 表达式,它会针对每个 Java 类的抽象语法树进行处理。

结束语

即使只使用内置规则(内容相当全面),PMD 也可以找到您的代码中的一些真正问题。某些问题可能很小,但有些问题则可能很大。PMD 不可能找到每个 bug,您仍然需要做单元测试和接受测试,在查找已知 bug 时,即使是 PMD 也无法替代一个好的调试器。但是,PMD 确实可以帮助您发现未知的问题。我还没有看到 PMD 找不到任何问题的代码基址。这是一个便宜、简易、有意思的改进程序的方式。如果您以前从未用过 PMD,那么您以及您的客户应该试试它。


相关主题

  • 您可以参阅本文在 developerWorks 全球站点上的 英文原文
  • 请访问 PMD 主页 学习更多有关 PMD 的内容。您可以从 PMD 项目页面 下载 PMD。
  • 请参阅 Tom Copeland 撰写的文章“ Static Analysis with PMD”,这篇文章对 PMD 进行了介绍。
  • 从 Tom Copeland 撰写的文章“ Custom PMD Rules”中学习如何编写定制 PMD 规则。
  • FindBugs 是另一个开源静态代码分析工具。它与 PMD 的不同之处在于,它分析编译后的字节码,而不是分析源代码,它还提供一个可选的 GUI。
  • Lint4j 是一个免费的、非开源静态代码分析工具,它检查的一些问题与 PMD 和 FindBugs 的相同,它还可以检查一些独特的问题。
  • Checkstyle 是一个开源代码分析工具,侧重于遵循 Java 编码约定(例如行长度和缩进)而不是 bug 模式。
  • AppPerfect 是一个收费的静态代码分析工具,拥有检查 JSP 页面的独特能力,也可以检查常规的 Java 源代码。
  • FindBugs,第 1 部分: 提高代码质量”(developerWorks,2004 年 5 月)和“ FindBugs,第 2 部分: 编写自定义检测器”(developerWorks,2004 年 5 月)研究了 FindBugs 如何有助于改进代码质量、消除 bug。
  • 诊断 Java 代码:单元测试与自动化代码分析协同工作”(developerWorks,2004 年 5 月)分析了单元测试与静态分析之间的关系,以及两者如何才能相互利用。请参阅完整的 诊断 Java 代码 专栏系列。
  • XOM 在本文的示例中多次引用到,它是一个开源的、基于树的 API,用于处理 Java 中的 XML。
  • developerWorks Java 技术专区 可以找到数百篇有关 Java 各个方面的技术文章。
  • 请访问 Developer Bookstore,以获得技术书籍的完整清单,其中包括数百本 Java 相关主题 的书籍。

评论

添加或订阅评论,请先登录注册

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Java technology
ArticleID=58201
ArticleTitle=用 PMD 铲除 bug
publish-date=01072005