級(jí)別: 初級(jí)
Chris Grindstaff (chris@gstaff.org), 軟件工程師
2004 年 5 月 25 日
靜
態(tài)分析工具承諾無需開發(fā)人員費(fèi)勁就能找出代碼中已有的缺陷。當(dāng)然,如果有多年的編寫經(jīng)驗(yàn),就會(huì)知道這些承諾并不是一定能兌現(xiàn)。盡管如此,好的靜態(tài)分析工具
仍然是工具箱中的無價(jià)之寶。在這個(gè)由兩部分組成的系列文章的第一部分中,高級(jí)軟件工程師 Chris Grindstaff 分析了 FindBugs
如何幫助提高代碼質(zhì)量以及排除隱含的缺陷。
代碼質(zhì)量工具的一個(gè)問題是它們?nèi)菀诪殚_發(fā)人員提供大量但并非真正問題的問題——即
偽問題(false positives)。
出現(xiàn)偽問題時(shí),開發(fā)人員要學(xué)會(huì)忽略工具的輸出或者放棄它。FindBugs 的設(shè)計(jì)者 David Hovemeyer 和 William Pugh
注意到了這個(gè)問題,并努力減少他們所報(bào)告的偽問題數(shù)量。與其他靜態(tài)分析工具不同,F(xiàn)indBugs
不注重樣式或者格式,它試圖只尋找真正的缺陷或者潛在的性能問題。
FindBugs 是什么?
FindBugs
是一個(gè)靜態(tài)分析工具,它檢查類或者 JAR
文件,將字節(jié)碼與一組缺陷模式進(jìn)行對(duì)比以發(fā)現(xiàn)可能的問題。有了靜態(tài)分析工具,就可以在不實(shí)際運(yùn)行程序的情況對(duì)軟件進(jìn)行分析。不是通過分析類文件的形式或結(jié)
構(gòu)來確定程序的意圖,而是通常使用 Visitor 模式(請(qǐng)參閱 參考資料)。圖 1 顯示了分析一個(gè)匿名項(xiàng)目的結(jié)果(為防止可怕的犯罪,這里不給出它的名字):
圖 1. FindBugs UI
讓我們看幾個(gè) FindBugs 可以發(fā)現(xiàn)的問題。
 |
本系列的第二篇文章“
編寫自定義檢測(cè)器”解釋了如何編寫自定義檢測(cè)器, 以便發(fā)現(xiàn)特定于應(yīng)用程序的問題。
|
|
問題發(fā)現(xiàn)的例子
下面的列表沒有包括 FindBug 可以找到的
所有問題。相反,我側(cè)重于一些更有意思的問題。
檢測(cè)器:找出 hash equals 不匹配
這個(gè)檢測(cè)器尋找與
equals() 和
hashCode() 的實(shí)現(xiàn)相關(guān)的幾個(gè)問題。這兩個(gè)方法非常重要,因?yàn)閹缀跛谢诩系念悺?List、Map、Set 等都調(diào)用它們。一般來說,這個(gè)檢測(cè)器尋找兩種不同類型的問題——當(dāng)一個(gè)類:
- 重寫對(duì)象的
equals() 方法,但是沒有重寫它的
hashCode 方法,或者相反的情況時(shí)。
- 定義一個(gè) co-variant 版本的
equals() 或
compareTo() 方法。例如,
Bob 類定義其
equals() 方法為布爾
equals(Bob) ,它覆蓋了對(duì)象中定義的
equals() 方法。因?yàn)?Java 代碼在編譯時(shí)解析重載方法的方式,在運(yùn)行時(shí)使用的幾乎總是在對(duì)象中定義的這個(gè)版本的方法,而不是在
Bob 中定義的那一個(gè)(除非顯式將
equals() 方法的參數(shù)強(qiáng)制轉(zhuǎn)換為
Bob 類型)。因此,當(dāng)這個(gè)類的一個(gè)實(shí)例放入到類集合中的任何一個(gè)中時(shí),使用的是
Object.equals() 版本的方法,而不是在
Bob 中定義的版本。在這種情況下,
Bob 類應(yīng)當(dāng)定義一個(gè)接受類型為
Object 的參數(shù)的
equals() 方法。
檢測(cè)器:忽略方法返回值
這個(gè)檢測(cè)器查找代碼中忽略了不應(yīng)該忽略的方法返回值的地方。這種情況的一個(gè)常見例子是在調(diào)用
String 方法時(shí),如在清單 1 中:
清單 1. 忽略返回值的例子
1 String aString = "bob";
2 b.replace('b', 'p');
3 if(b.equals("pop"))
|
這個(gè)錯(cuò)誤很常見。在第 2 行,程序員認(rèn)為他已經(jīng)用 p 替換了字符串中的所有 b。確實(shí)是這樣,但是他忘記了字符串是不可變的。所有這類方法都返回一個(gè)新字符串,而從來不會(huì)改變消息的接收者。
檢測(cè)器:Null 指針對(duì) null 的解引用(dereference)和冗余比較
這個(gè)檢測(cè)器查找兩類問題。它查找代碼路徑將會(huì)或者可能造成 null 指針異常的情況,它還查找對(duì) null
的冗余比較的情況。例如,如果兩個(gè)比較值都為 null,那么它們就是冗余的并可能表明代碼錯(cuò)誤。FindBugs 在可以確定一個(gè)值為 null
而另一個(gè)值不為 null 時(shí),檢測(cè)類似的錯(cuò)誤,如清單 2 所示:
清單 2. Null 指針示例
1 Person person = aMap.get("bob");
2 if (person != null) {
3 person.updateAccessTime();
4 }
5 String name = person.getName();
|
在這個(gè)例子中,如果第 1 行的
Map 不包括一個(gè)名為“bob”的人,那么在第 5 行詢問
person 的名字時(shí)就會(huì)出現(xiàn) null 指針異常。因?yàn)?FindBugs 不知道 map 是否包含“bob”,所以它將第 5 行標(biāo)記為可能 null 指針異常。
檢測(cè)器:初始化之前讀取字段
這個(gè)檢測(cè)器尋找在構(gòu)造函數(shù)中初始化之前被讀取的字段。這個(gè)錯(cuò)誤通常是——盡管不總是如此——由使用字段名而不是構(gòu)造函數(shù)參數(shù)引起的,如清單 3 所示:
清單 3. 在構(gòu)造函數(shù)中讀取未初始化的字段
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 }
|
在這個(gè)例子中,第 6 行將產(chǎn)生一個(gè) null 指針異常,因?yàn)樽兞?
actions 還沒有初始化。
這些例子只是 FindBugs 所發(fā)現(xiàn)的問題種類的一小部分(更多信息請(qǐng)參閱
參考資料)。在撰寫本文時(shí),F(xiàn)indBugs 提供總共 35 個(gè)檢測(cè)器。
開始使用 FindBugs
要運(yùn)行 FindBugs,需要一個(gè)版本 1.4 或者更高的 Java Development Kit (JDK),盡管它可以分析由老的 JDK 創(chuàng)建的類文件。要做的第一件事是下載并安裝最新發(fā)布的 FindBugs——當(dāng)前是 0.7.1 (請(qǐng)參閱
參考資料)。幸運(yùn)的是,下載和安全是相當(dāng)簡單的。在下載了 zip 或者 tar 文件后,將它解壓縮到所選的目錄中。就是這樣了——安裝就完成了。
安
裝完后,對(duì)一個(gè)示例類運(yùn)行它。就像一般文章中的情況,我將針對(duì) Windows 用戶進(jìn)行講解,并假定那些 Unix
信仰者可以熟練地轉(zhuǎn)化這些內(nèi)容并跟進(jìn)。打開命令行提示符號(hào)并進(jìn)入 FindBugs 的安裝目錄。對(duì)我來說,這是
C:\apps\FindBugs-0.7.3。
在 FindBugs 主目錄中,有幾個(gè)值得注意的目錄。文檔在 doc 目錄中,但是對(duì)我們來說更重要的是,bin 目錄包含了運(yùn)行 FindBugs 的批處理文件,這使我們進(jìn)入下一部分。
運(yùn)行 FindBugs
像
如今的大多數(shù)數(shù)工具一樣,可以以多種方式運(yùn)行 FindBugs——從 GUI、從命令行、使用 Ant、作為 Eclipse 插件程序和使用
Maven。我將簡要提及從 GUI 運(yùn)行 FindBugs,但是重點(diǎn)放在用 Ant 和命令行運(yùn)行它。部分原因是由于 GUI
沒有提供命令行的所有選項(xiàng)。例如,當(dāng)前不能指定要加入的過濾器或者在 UI 中排除特定的類。但是更重要的原因是我認(rèn)為 FindBugs
最好作為編譯的集成部分使用,而 UI 不屬于自動(dòng)編譯。
使用 FindBugs UI
使用 FindBugs UI 很直觀,但是有幾點(diǎn)值得說明。如
圖 1所示,使用 FindBugs UI 的一個(gè)好處是對(duì)每一個(gè)檢測(cè)到的問題提供了說明。圖 1 顯示了缺陷
Naked notify in method的說明。對(duì)每一種缺陷模式提供了類似的說明,在第一次熟悉這種工具時(shí)這是很有用的。窗口下面的 Source code 選項(xiàng)卡也同樣有用。如果告訴 FindBugs 在什么地方尋找代碼,它就會(huì)在轉(zhuǎn)換到相應(yīng)的選項(xiàng)卡時(shí)突出顯示有問題的那一行。
值得一提的還有在將 FinBugs 作為 Ant 任務(wù)或者在命令行中運(yùn)行 FindBugs 時(shí),選擇
xml 作為
ouput 選項(xiàng),可以將上一次運(yùn)行的結(jié)果裝載到 UI 中。這樣做是同時(shí)利用基于命令行的工具和 UI 工具的優(yōu)點(diǎn)的一個(gè)很好的方法。
將 FindBugs 作為 Ant 任務(wù)運(yùn)行
讓
我們看一下如何在 Ant 編譯腳本中使用 FindBugs。首先將 FindBugs Ant 任務(wù)拷貝到 Ant 的 lib 目錄中,這樣
Ant 就知道新的任務(wù)。將 FIND_BUGS_HOME\lib\FindBugs-ant.jar 拷貝到 ANT_HOME\lib。
現(xiàn)在看看在編譯腳本中要加入什么才能使用 FindBugs 任務(wù)。因?yàn)?FindBugs 是一個(gè)自定義任務(wù),將需要使用
taskdef 任務(wù)以使 Ant 知道裝載哪一個(gè)類。通過在編譯文件中加入以下一行做到這一點(diǎn):
<taskdef name="FindBugs" classname="edu.umd.cs.FindBugs.anttask.FindBugsTask"/>
|
在定義了
taskdef 后,可以用它的名字
FindBugs 引用它。下一步要在編譯中加入使用新任務(wù)的目標(biāo),如清單 4 所示:
清單 4. 創(chuàng)建 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>
|
讓我們更詳細(xì)地分析這段代碼中所發(fā)生的過程。
第 1 行: 注意
target 取決于編譯。一定要記住處理的是類文件而
不 是源文件,這樣使
target 對(duì)應(yīng)于編譯目標(biāo)保證了 FindBugs 可在最新的類文件運(yùn)行。FindBugs 可以靈活地接受多種輸入,包括一組類文件、JAR 文件、或者一組目錄。
第 2 行:必須指定包含 FindBugs 的目錄,我是用 Ant 的一個(gè)屬性完成的,像這樣:
<property name="FindBugs.home" value="C:\apps\FindBugs-0.7.3" />
|
可選屬性
output 指定 FindBugs 的結(jié)果使用的輸出格式。可能的值有
xml 、
text 或者
emacs 。如果沒有指定
outputFile ,那么 FindBugs 會(huì)使用標(biāo)準(zhǔn)輸出。如前所述,XML 格式有可以在 UI 中觀看的額外好處。
第 3 行:
class 元素用于指定要 FindBugs 分析哪些 JAR、類文件或者目錄。分析多個(gè) JAR 或者類文件時(shí),要為每一個(gè)文件指定一個(gè)單獨(dú)的
class 元素。除非加入了
projectFile 元素,否則需要
class 元素。更多細(xì)節(jié)請(qǐng)參閱 FindBugs 手冊(cè)。
第 4 行: 用嵌套元素
auxClasspath 列出應(yīng)用程序的依賴性。這些是應(yīng)用程序需要但是不希望 FindBugs 分析的類。如果沒有列出應(yīng)用程序的依賴關(guān)系,那么 FindBugs 仍然會(huì)盡可能地分析類,但是在找不到一個(gè)缺少的類時(shí),它會(huì)抱怨。與
class 元素一樣,可以在 FindBugs 元素中指定多個(gè)
auxClasspath 元素。
auxClasspath 元素是可選的。
第 5 行: 如果指定了
sourcePath 元素,那么
path 屬性應(yīng)當(dāng)表明一個(gè)包含應(yīng)用程序源代碼的目錄。指定目錄使 FindBugs 可以在 GUI 中查看 XML 結(jié)果時(shí)突出顯示出錯(cuò)的源代碼。這個(gè)元素是可選的。
上面就是基本內(nèi)容了。讓我們提前幾個(gè)星期。
過濾器
您
已經(jīng)將 FindBugs
引入到了團(tuán)隊(duì)中,并運(yùn)行它作為您的每小時(shí)/每晚編譯過程的一部分。當(dāng)團(tuán)隊(duì)越來越熟悉這個(gè)工具時(shí),出于某些原因,您決定所檢測(cè)到的一些缺陷對(duì)于團(tuán)隊(duì)來說不重
要。也許您不關(guān)心一些類是否返回可能被惡意修改的對(duì)象——也許,像
JEdit,有一個(gè)真正需要的(honest-to-goodness)、合法的理由調(diào)用 System.gc() 。
總是可以選擇“關(guān)閉”特定的
檢測(cè)器。在更細(xì)化的水平上,可以在指定的一組類甚至是方法中查找問題時(shí),排除某些檢測(cè)器。FindBugs
提供了這種細(xì)化的控制,可以排除或者包含過濾器。當(dāng)前只有用命令行或者 Ant 啟動(dòng)的 FindBugs
中支持排除和包含過濾器。正如其名字所表明的,使用排除過濾器來排除對(duì)某些缺陷的報(bào)告。較為少見但仍然有用的是,包含過濾器只能用于報(bào)告指定的缺陷。過濾
器是在一個(gè) XML 文件中定義的。可以在命令行中用一個(gè)排除或者包含開關(guān)、或者在 Ant 編譯文件中用 excludeFilter 和
includeFilter 指定它們。在下面的例子中,假定使用排除開關(guān)。還要注意在下面的討論中,我對(duì) “bugcode”、“bug” 和“detector”的使用具有某種程度的互換性。
可以有不同的方式定義過濾器:
- 匹配一個(gè)類的過濾器。可以用這些過濾器 忽略在特定類中發(fā)現(xiàn)的所有問題。
- 匹配一個(gè)類中特定缺陷代碼(bugcode)的 過濾器。可以用這些過濾器忽略在特定類中發(fā)現(xiàn)的一些缺陷。
- 匹配一組缺陷的過濾器。可以用這些過濾器 忽略所分析的所有類中的一組缺陷。
- 匹配所分析的一個(gè)類中的某些方法的過濾器。可以用這些過濾器忽略在一個(gè)類中的一組方法中發(fā)現(xiàn)的所有缺陷。
- 匹配在所分析的一個(gè)類中的方法中發(fā)現(xiàn)的某些缺陷的過濾器。可以用這些過濾器忽略在一組方法中發(fā)現(xiàn)的特定缺陷。
知道了這些就可以開始使用了。有關(guān)其他定制 FindBugs 方法的更多信息,請(qǐng)參閱 FindBugs 文檔。知道如何設(shè)置編譯文件以后,就讓我們更詳細(xì)地分析如何將 FindBugs 集成到編譯過程中吧!
將 FindBugs 集成到編譯過程中
在將 FindBugs 集成到編譯過程當(dāng)中可以有幾種選擇。總是可以在命令行執(zhí)行 FindBugs,但是您很可能已經(jīng)使用 Ant 進(jìn)行編譯,所以最自然的方法是使用 FindBugs Ant 任務(wù)。因?yàn)槲覀冊(cè)?
如何運(yùn)行 FindBugs一節(jié)中討論了使用 FindBugs Ant 任務(wù)的基本內(nèi)容,所以現(xiàn)在討論應(yīng)當(dāng)將 FindBugs 加入到編譯過程中的幾個(gè)理由,并討論幾個(gè)可能遇到的問題。
為什么應(yīng)該將 FindBugs 集成到編譯過程中?
經(jīng)
常問到的第一個(gè)問題是為什么要將 FindBugs
加入到編譯過程中?雖然有大量理由,最明顯的回答是要保證盡可能早地在進(jìn)行編譯時(shí)發(fā)現(xiàn)問題。當(dāng)團(tuán)隊(duì)擴(kuò)大,并且不可避免地在項(xiàng)目中加入更多新開發(fā)人員
時(shí),F(xiàn)indBugs 可以作為一個(gè)安全網(wǎng),檢測(cè)出已經(jīng)識(shí)別的缺陷模式。我想重申在一篇 FindBugs
論文中表述的一些觀點(diǎn)。如果讓一定數(shù)量的開發(fā)人員共同工作,那么在代碼中就會(huì)出現(xiàn)缺陷。像 FindBugs
這樣的工具當(dāng)然不會(huì)找出所有的缺陷,但是它們會(huì)幫助找出其中的部分。現(xiàn)在找出部分比客戶在以后找到它們要好——特別是當(dāng)將 FindBugs
結(jié)合到編譯過程中的成本是如此低時(shí)。
一旦確定了加入哪些過濾器和類,運(yùn)行 FindBugs 就沒什么成本了,而帶來的好處就是它會(huì)檢測(cè)出新缺陷。如果編寫特定于應(yīng)用程序的檢測(cè)器,則這個(gè)好處可能更大。
生成有意義的結(jié)果
重
要的是要認(rèn)識(shí)到這種成本/效益分析只有在不生成大量誤檢時(shí)才有效。換句話說,如果在每次編譯時(shí),不能簡單地確定是否引入了新的缺陷,那么這個(gè)工具的價(jià)值就
會(huì)被抵消。分析越自動(dòng)化越好。如果修復(fù)缺陷意味著必須吃力地分析檢測(cè)出的大量不相干的缺陷,那么您就不會(huì)經(jīng)常使用它,或者至少不會(huì)很好地使用它。
確定不關(guān)心哪些問題并從編譯中排除它們。也可以挑出
確實(shí)關(guān)注的一小部分檢測(cè)器并只運(yùn)行它們。另一種選擇是從個(gè)別的類中排除一組檢測(cè)器,但是其他的類不排除。FindBugs 提供了使用過濾器的極大靈活性,這可幫助生成對(duì)團(tuán)隊(duì)有意義的結(jié)果,由此我們進(jìn)入下一節(jié)。
確定用 FindBugs 的結(jié)果做什么
可
能看來很顯然,但是您想不到我參與的團(tuán)隊(duì)中有多少加入了類似 FindBugs
這樣的工具而沒有真正利用它。讓我們更深入地探討這個(gè)問題——用結(jié)果做什么?明確回答這個(gè)問題是困難的,因?yàn)檫@與團(tuán)隊(duì)的組織方式、如何處理代碼所有權(quán)問題
等有很大關(guān)系。不過,下面是一些指導(dǎo):
- 可以考慮將 FindBugs 結(jié)果加入到源代碼管理(SCM)系統(tǒng)中。一般的經(jīng)驗(yàn)做法是不將編譯工件(artifact)放到 SCM 系統(tǒng)中。不過,在這種特定情況下,打破這個(gè)規(guī)則可能是正確的,因?yàn)樗鼓梢员O(jiān)視代碼質(zhì)量隨時(shí)間的變化。
- 可以選擇將 XML 結(jié)果轉(zhuǎn)換為可以發(fā)送到團(tuán)隊(duì)的網(wǎng)站上的 HTML 報(bào)告。轉(zhuǎn)換可以用 XSL 樣式表或者腳本實(shí)現(xiàn)。有關(guān)例子請(qǐng)查看 FindBugs 網(wǎng)站或者郵件列表(請(qǐng)參閱
參考資料)。
- 像 FindBugs 這樣的工具通常會(huì)成為用于敲打團(tuán)隊(duì)或者個(gè)人的政治武器。盡量抵制這種做法或者不讓它發(fā)生——記住,它只是一個(gè)工具,它可以幫助改進(jìn)代碼的質(zhì)量。有了這種思想,在下一部分中,我將展示如何編寫自定義缺陷檢測(cè)器。
結(jié)束語
我
鼓勵(lì)讀者對(duì)自己的代碼試用靜態(tài)分析工具,不管是 FindBugs、PMD 還是其他的。它們是有用的工具,可以找出真正的問題,而 FindBugs
是在消除誤檢方面做得最好的工具。此外,它的可插入結(jié)構(gòu)提供了編寫有價(jià)值的、特定于應(yīng)用程序的檢測(cè)器的、有意思的測(cè)試框架。在本系列的 第 2 部分中,我將展示如何編寫自定義檢測(cè)器以找出特定于應(yīng)用程序的問題。
參考資料
關(guān)于作者
 |
|
 |
Chris
Grindstaff 是在北加利福尼亞 Research Triangle Park 工作的 IBM 高級(jí)軟件工程師。Chris 在 7
歲時(shí)編寫了他的第一個(gè)程序,當(dāng)時(shí)他讓小學(xué)老師認(rèn)識(shí)到“鍵入”句子與手寫它們一樣費(fèi)力。Chris 目前參與了不同的開放源代碼項(xiàng)目。他大量使用
Eclipse 并編寫了幾個(gè)流行的 Eclipse 插件程序,可以在他的 網(wǎng)站找到這些插件程序。可以通過
cgrinds@us.ibm.com或者
chris@gstaff.org與 Chrise 聯(lián)系。
|
|