用 PMD 铲除 bug
简介:?PMD 是一个开源的静态分析工具,是一个值得您添加到捉虫工具箱中的工具。Elliotte Rusty Harold 将解释如何使用 PMD 内置的规则以及您自己定制的规则集来提高 Java 代码质量。
Tom Copeland 的 PMD 是一个开源(BSD 许可)工具,它分析 Java 源代码,找出潜在的 bug。在一般意义上来说,它与 FindBugs 和 Lint4j 这类工具类似。但是,所有这些工具找出的 bug 各不相同,所以在给定代码基址上把这些工具都运行一遍很有好处。在本文中,我将解释如何使用 PMD,并展示可以从 PMD 中获得什么。本文将介绍 PMD 的命令行界面。您也可以把 PMD 与 Ant 集成在一起,以便进行自动源代码检查,而且还可以将 PMD 与一些可用于大多数主要 IDE 和程序员编辑器的插件集成在一起。
?
在清单 1 中可以看到,PMD 发现了两个问题:在 ImageGrabber.java 的第 32 行有一个短变量名称,在第 105 行的名称中有一个下划线。这些看起来可能是小问题,但是后果却可能是惊人的。在这个例子中,105 行的下划线只是一个有 10 年之久的老代码中一些容易修正的小毛病。但是仔细考察第一个问题,使我认识到可以完全排除 j 变量,因为它与另外一个单独递增变量的功能相同。这个程序仍然有用,但是它应该更简洁一些,以便应对以后的挑战。每当清除一行代码,也就减少了一个可能隐匿 bug 的地方。
您可以把 PMD 的输出重定向到文件中,或者通过管道,以常见的方式将它传递到编辑器中。我通常更喜欢生成 HTML 格式的输出,并将它加载到 Web 浏览器中,如图 1 中所示:
图 1. PMD 用 HTML 格式处理后的输出
在检查源代码树时,把结果输出到文件中会非常有帮助。如果把目录、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(),等等。 switch 语句应当有 default 块,应当避免深度嵌套的 if 块,不应当给参数重新赋值,不应该对 double 值进行相等比较。 java.lang 的类中。 suite() 方法是不是 static 和 public。 String 构造函数,对 String 变量调用 toString() 方法。 for、 if、 while 和 else 语句是否使用了括号。 finalize() 方法不是那么普遍(我上次编写这个代码也经是好多年前的事了),所以它们的使用规则虽然很详细,但是人们对它们相对不是很熟悉。这类检查查找 finalize() 方法的各种问题,例如空的终结函数,调用其他方法的 finalize() 方法,对 finalize() 的显式调用,等等。 clone() 方法的新规则。凡是重写 clone() 方法的类都必须实现 Cloneable, clone() 方法应该调用 super.clone(),而 clone() 方法应该声明抛出 CloneNotSupportedException 异常,即使实际上没有抛出异常,也要如此。 java.lang.Exception 异常,不应当将异常用于流控制,不应该捕获 Throwable,等等。 sun 包导入等。 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,那么您以及您的客户应该试试它。