靜態(tài)分析工具承諾無(wú)需開(kāi)發(fā)人員費勁就能找出代碼中已有的缺陷。當然,如果有多年的編寫(xiě)經(jīng)驗,就會(huì )知道這些承諾并不是一定能兌現。盡管如此,好的靜態(tài)分析工具仍然是工具箱中的無(wú)價(jià)之寶。在這個(gè)由兩部分組成的系列文章的第一部分中,高級軟件工程師 Chris Grindstaff 分析了 FindBugs 如何幫助提高代碼質(zhì)量以及排除隱含的缺陷。
代碼質(zhì)量工具的一個(gè)問(wèn)題是它們容易為開(kāi)發(fā)人員提供大量但并非真正問(wèn)題的問(wèn)題——即 偽問(wèn)題(false positives)。出現偽問(wèn)題時(shí),開(kāi)發(fā)人員要學(xué)會(huì )忽略工具的輸出或者放棄它。FindBugs 的設計者 David Hovemeyer 和 William Pugh 注意到了這個(gè)問(wèn)題,并努力減少他們所報告的偽問(wèn)題數量。與其他靜態(tài)分析工具不同,FindBugs 不注重樣式或者格式,它試圖只尋找真正的缺陷或者潛在的性能問(wèn)題。
FindBugs 是一個(gè)靜態(tài)分析工具,它檢查類(lèi)或者 JAR 文件,將字節碼與一組缺陷模式進(jìn)行對比以發(fā)現可能的問(wèn)題。有了靜態(tài)分析工具,就可以在不實(shí)際運行程序的情況對軟件進(jìn)行分析。不是通過(guò)分析類(lèi)文件的形式或結構來(lái)確定程序的意圖,而是通常使用 Visitor 模式(請參閱 參考資料)。圖 1 顯示了分析一個(gè)匿名項目的結果(為防止可怕的犯罪,這里不給出它的名字):

讓我們看幾個(gè) FindBugs 可以發(fā)現的問(wèn)題。
![]() |
|
![]() ![]() |
![]()
|
下面的列表沒(méi)有包括 FindBug 可以找到的 所有問(wèn)題。相反,我側重于一些更有意思的問(wèn)題。
檢測器:找出 hash equals 不匹配
這個(gè)檢測器尋找與 equals() 和 hashCode() 的實(shí)現相關(guān)的幾個(gè)問(wèn)題。這兩個(gè)方法非常重要,因為幾乎所有基于集合的類(lèi)—— List、Map、Set 等都調用它們。一般來(lái)說(shuō),這個(gè)檢測器尋找兩種不同類(lèi)型的問(wèn)題——當一個(gè)類(lèi):
equals() 方法,但是沒(méi)有重寫(xiě)它的 hashCode 方法,或者相反的情況時(shí)。 equals() 或 compareTo() 方法。例如, Bob 類(lèi)定義其 equals() 方法為布爾 equals(Bob) ,它覆蓋了對象中定義的 equals() 方法。因為 Java 代碼在編譯時(shí)解析重載方法的方式,在運行時(shí)使用的幾乎總是在對象中定義的這個(gè)版本的方法,而不是在 Bob 中定義的那一個(gè)(除非顯式將 equals() 方法的參數強制轉換為 Bob 類(lèi)型)。因此,當這個(gè)類(lèi)的一個(gè)實(shí)例放入到類(lèi)集合中的任何一個(gè)中時(shí),使用的是 Object.equals() 版本的方法,而不是在 Bob 中定義的版本。在這種情況下, Bob 類(lèi)應當定義一個(gè)接受類(lèi)型為 Object 的參數的 equals() 方法。 檢測器:忽略方法返回值
這個(gè)檢測器查找代碼中忽略了不應該忽略的方法返回值的地方。這種情況的一個(gè)常見(jiàn)例子是在調用 String 方法時(shí),如在清單 1 中:
|
這個(gè)錯誤很常見(jiàn)。在第 2 行,程序員認為他已經(jīng)用 p 替換了字符串中的所有 b。確實(shí)是這樣,但是他忘記了字符串是不可變的。所有這類(lèi)方法都返回一個(gè)新字符串,而從來(lái)不會(huì )改變消息的接收者。
檢測器:Null 指針對 null 的解引用(dereference)和冗余比較
這個(gè)檢測器查找兩類(lèi)問(wèn)題。它查找代碼路徑將會(huì )或者可能造成 null 指針異常的情況,它還查找對 null 的冗余比較的情況。例如,如果兩個(gè)比較值都為 null,那么它們就是冗余的并可能表明代碼錯誤。FindBugs 在可以確定一個(gè)值為 null 而另一個(gè)值不為 null 時(shí),檢測類(lèi)似的錯誤,如清單 2 所示:
|
在這個(gè)例子中,如果第 1 行的 Map 不包括一個(gè)名為“bob”的人,那么在第 5 行詢(xún)問(wèn) person 的名字時(shí)就會(huì )出現 null 指針異常。因為 FindBugs 不知道 map 是否包含“bob”,所以它將第 5 行標記為可能 null 指針異常。
檢測器:初始化之前讀取字段
這個(gè)檢測器尋找在構造函數中初始化之前被讀取的字段。這個(gè)錯誤通常是——盡管不總是如此——由使用字段名而不是構造函數參數引起的,如清單 3 所示:
|
在這個(gè)例子中,第 6 行將產(chǎn)生一個(gè) null 指針異常,因為變量 actions 還沒(méi)有初始化。
這些例子只是 FindBugs 所發(fā)現的問(wèn)題種類(lèi)的一小部分(更多信息請參閱 參考資料)。在撰寫(xiě)本文時(shí),FindBugs 提供總共 35 個(gè)檢測器。
![]() ![]() |
![]()
|
要運行 FindBugs,需要一個(gè)版本 1.4 或者更高的 Java Development Kit (JDK),盡管它可以分析由老的 JDK 創(chuàng )建的類(lèi)文件。要做的第一件事是下載并安裝最新發(fā)布的 FindBugs——當前是 0.7.1 (請參閱 參考資料)。幸運的是,下載和安全是相當簡(jiǎn)單的。在下載了 zip 或者 tar 文件后,將它解壓縮到所選的目錄中。就是這樣了——安裝就完成了。
安裝完后,對一個(gè)示例類(lèi)運行它。就像一般文章中的情況,我將針對 Windows 用戶(hù)進(jìn)行講解,并假定那些 Unix 信仰者可以熟練地轉化這些內容并跟進(jìn)。打開(kāi)命令行提示符號并進(jìn)入 FindBugs 的安裝目錄。對我來(lái)說(shuō),這是 C:\apps\FindBugs-0.7.3。
在 FindBugs 主目錄中,有幾個(gè)值得注意的目錄。文檔在 doc 目錄中,但是對我們來(lái)說(shuō)更重要的是,bin 目錄包含了運行 FindBugs 的批處理文件,這使我們進(jìn)入下一部分。
![]() ![]() |
![]()
|
像如今的大多數數工具一樣,可以以多種方式運行 FindBugs——從 GUI、從命令行、使用 Ant、作為 Eclipse 插件程序和使用 Maven。我將簡(jiǎn)要提及從 GUI 運行 FindBugs,但是重點(diǎn)放在用 Ant 和命令行運行它。部分原因是由于 GUI 沒(méi)有提供命令行的所有選項。例如,當前不能指定要加入的過(guò)濾器或者在 UI 中排除特定的類(lèi)。但是更重要的原因是我認為 FindBugs 最好作為編譯的集成部分使用,而 UI 不屬于自動(dòng)編譯。
使用 FindBugs UI 很直觀(guān),但是有幾點(diǎn)值得說(shuō)明。如 圖 1所示,使用 FindBugs UI 的一個(gè)好處是對每一個(gè)檢測到的問(wèn)題提供了說(shuō)明。圖 1 顯示了缺陷 Naked notify in method的說(shuō)明。對每一種缺陷模式提供了類(lèi)似的說(shuō)明,在第一次熟悉這種工具時(shí)這是很有用的。窗口下面的 Source code 選項卡也同樣有用。如果告訴 FindBugs 在什么地方尋找代碼,它就會(huì )在轉換到相應的選項卡時(shí)突出顯示有問(wèn)題的那一行。
值得一提的還有在將 FinBugs 作為 Ant 任務(wù)或者在命令行中運行 FindBugs 時(shí),選擇 xml 作為 ouput 選項,可以將上一次運行的結果裝載到 UI 中。這樣做是同時(shí)利用基于命令行的工具和 UI 工具的優(yōu)點(diǎn)的一個(gè)很好的方法。
讓我們看一下如何在 Ant 編譯腳本中使用 FindBugs。首先將 FindBugs Ant 任務(wù)拷貝到 Ant 的 lib 目錄中,這樣 Ant 就知道新的任務(wù)。將 FIND_BUGS_HOME\lib\FindBugs-ant.jar 拷貝到 ANT_HOME\lib。
現在看看在編譯腳本中要加入什么才能使用 FindBugs 任務(wù)。因為 FindBugs 是一個(gè)自定義任務(wù),將需要使用 taskdef 任務(wù)以使 Ant 知道裝載哪一個(gè)類(lèi)。通過(guò)在編譯文件中加入以下一行做到這一點(diǎn):
|
在定義了 taskdef 后,可以用它的名字 FindBugs 引用它。下一步要在編譯中加入使用新任務(wù)的目標,如清單 4 所示:
|
讓我們更詳細地分析這段代碼中所發(fā)生的過(guò)程。
第 1 行: 注意 target 取決于編譯。一定要記住處理的是類(lèi)文件而 不 是源文件,這樣使 target 對應于編譯目標保證了 FindBugs 可在最新的類(lèi)文件運行。FindBugs 可以靈活地接受多種輸入,包括一組類(lèi)文件、JAR 文件、或者一組目錄。
第 2 行:必須指定包含 FindBugs 的目錄,我是用 Ant 的一個(gè)屬性完成的,像這樣:
|
可選屬性 output 指定 FindBugs 的結果使用的輸出格式??赡艿闹涤?xml 、 text 或者 emacs 。如果沒(méi)有指定 outputFile ,那么 FindBugs 會(huì )使用標準輸出。如前所述,XML 格式有可以在 UI 中觀(guān)看的額外好處。
第 3 行: class 元素用于指定要 FindBugs 分析哪些 JAR、類(lèi)文件或者目錄。分析多個(gè) JAR 或者類(lèi)文件時(shí),要為每一個(gè)文件指定一個(gè)單獨的 class 元素。除非加入了 projectFile 元素,否則需要 class 元素。更多細節請參閱 FindBugs 手冊。
第 4 行: 用嵌套元素 auxClasspath 列出應用程序的依賴(lài)性。這些是應用程序需要但是不希望 FindBugs 分析的類(lèi)。如果沒(méi)有列出應用程序的依賴(lài)關(guān)系,那么 FindBugs 仍然會(huì )盡可能地分析類(lèi),但是在找不到一個(gè)缺少的類(lèi)時(shí),它會(huì )抱怨。與 class 元素一樣,可以在 FindBugs 元素中指定多個(gè) auxClasspath 元素。 auxClasspath 元素是可選的。
第 5 行: 如果指定了 sourcePath 元素,那么 path 屬性應當表明一個(gè)包含應用程序源代碼的目錄。指定目錄使 FindBugs 可以在 GUI 中查看 XML 結果時(shí)突出顯示出錯的源代碼。這個(gè)元素是可選的。
您已經(jīng)將 FindBugs 引入到了團隊中,并運行它作為您的每小時(shí)/每晚編譯過(guò)程的一部分。當團隊越來(lái)越熟悉這個(gè)工具時(shí),出于某些原因,您決定所檢測到的一些缺陷對于團隊來(lái)說(shuō)不重要。也許您不關(guān)心一些類(lèi)是否返回可能被惡意修改的對象——也許,像 JEdit,有一個(gè)真正需要的(honest-to-goodness)、合法的理由調用 System.gc() 。
總是可以選擇“關(guān)閉”特定的檢測器。在更細化的水平上,可以在指定的一組類(lèi)甚至是方法中查找問(wèn)題時(shí),排除某些檢測器。FindBugs 提供了這種細化的控制,可以排除或者包含過(guò)濾器。當前只有用命令行或者 Ant 啟動(dòng)的 FindBugs 中支持排除和包含過(guò)濾器。正如其名字所表明的,使用排除過(guò)濾器來(lái)排除對某些缺陷的報告。較為少見(jiàn)但仍然有用的是,包含過(guò)濾器只能用于報告指定的缺陷。過(guò)濾器是在一個(gè) XML 文件中定義的??梢栽诿钚兄杏靡粋€(gè)排除或者包含開(kāi)關(guān)、或者在 Ant 編譯文件中用 excludeFilter 和 includeFilter 指定它們。在下面的例子中,假定使用排除開(kāi)關(guān)。還要注意在下面的討論中,我對 “bugcode”、“bug” 和“detector”的使用具有某種程度的互換性。
可以有不同的方式定義過(guò)濾器:
知道了這些就可以開(kāi)始使用了。有關(guān)其他定制 FindBugs 方法的更多信息,請參閱 FindBugs 文檔。知道如何設置編譯文件以后,就讓我們更詳細地分析如何將 FindBugs 集成到編譯過(guò)程中吧!
![]() ![]() |
![]()
|
在將 FindBugs 集成到編譯過(guò)程當中可以有幾種選擇??偸强梢栽诿钚袌绦?FindBugs,但是您很可能已經(jīng)使用 Ant 進(jìn)行編譯,所以最自然的方法是使用 FindBugs Ant 任務(wù)。因為我們在 如何運行 FindBugs一節中討論了使用 FindBugs Ant 任務(wù)的基本內容,所以現在討論應當將 FindBugs 加入到編譯過(guò)程中的幾個(gè)理由,并討論幾個(gè)可能遇到的問(wèn)題。
為什么應該將 FindBugs 集成到編譯過(guò)程中?
經(jīng)常問(wèn)到的第一個(gè)問(wèn)題是為什么要將 FindBugs 加入到編譯過(guò)程中?雖然有大量理由,最明顯的回答是要保證盡可能早地在進(jìn)行編譯時(shí)發(fā)現問(wèn)題。當團隊擴大,并且不可避免地在項目中加入更多新開(kāi)發(fā)人員時(shí),FindBugs 可以作為一個(gè)安全網(wǎng),檢測出已經(jīng)識別的缺陷模式。我想重申在一篇 FindBugs 論文中表述的一些觀(guān)點(diǎn)。如果讓一定數量的開(kāi)發(fā)人員共同工作,那么在代碼中就會(huì )出現缺陷。像 FindBugs 這樣的工具當然不會(huì )找出所有的缺陷,但是它們會(huì )幫助找出其中的部分?,F在找出部分比客戶(hù)在以后找到它們要好——特別是當將 FindBugs 結合到編譯過(guò)程中的成本是如此低時(shí)。
一旦確定了加入哪些過(guò)濾器和類(lèi),運行 FindBugs 就沒(méi)什么成本了,而帶來(lái)的好處就是它會(huì )檢測出新缺陷。如果編寫(xiě)特定于應用程序的檢測器,則這個(gè)好處可能更大。
重要的是要認識到這種成本/效益分析只有在不生成大量誤檢時(shí)才有效。換句話(huà)說(shuō),如果在每次編譯時(shí),不能簡(jiǎn)單地確定是否引入了新的缺陷,那么這個(gè)工具的價(jià)值就會(huì )被抵消。分析越自動(dòng)化越好。如果修復缺陷意味著(zhù)必須吃力地分析檢測出的大量不相干的缺陷,那么您就不會(huì )經(jīng)常使用它,或者至少不會(huì )很好地使用它。
確定不關(guān)心哪些問(wèn)題并從編譯中排除它們。也可以挑出 確實(shí)關(guān)注的一小部分檢測器并只運行它們。另一種選擇是從個(gè)別的類(lèi)中排除一組檢測器,但是其他的類(lèi)不排除。FindBugs 提供了使用過(guò)濾器的極大靈活性,這可幫助生成對團隊有意義的結果,由此我們進(jìn)入下一節。
可能看來(lái)很顯然,但是您想不到我參與的團隊中有多少加入了類(lèi)似 FindBugs 這樣的工具而沒(méi)有真正利用它。讓我們更深入地探討這個(gè)問(wèn)題——用結果做什么?明確回答這個(gè)問(wèn)題是困難的,因為這與團隊的組織方式、如何處理代碼所有權問(wèn)題等有很大關(guān)系。不過(guò),下面是一些指導:
![]() ![]() |
![]()
|
我鼓勵讀者對自己的代碼試用靜態(tài)分析工具,不管是 FindBugs、PMD 還是其他的。它們是有用的工具,可以找出真正的問(wèn)題,而 FindBugs 是在消除誤檢方面做得最好的工具。此外,它的可插入結構提供了編寫(xiě)有價(jià)值的、特定于應用程序的檢測器的、有意思的測試框架。在本系列的 第 2 部分中,我將展示如何編寫(xiě)自定義檢測器以找出特定于應用程序的問(wèn)題。
![]() ![]() |
![]()
|
![]() ![]() |
![]()
|
![]() | ||
![]() | Chris Grindstaff 是在北加利福尼亞 Research Triangle Park 工作的 IBM 高級軟件工程師。Chris 在 7 歲時(shí)編寫(xiě)了他的第一個(gè)程序,當時(shí)他讓小學(xué)老師認識到“鍵入”句子與手寫(xiě)它們一樣費力。Chris 目前參與了不同的開(kāi)放源代碼項目。他大量使用 Eclipse 并編寫(xiě)了幾個(gè)流行的 Eclipse 插件程序,可以在他的 網(wǎng)站找到這些插件程序??梢酝ㄟ^(guò) cgrinds@us.ibm.com或者 chris@gstaff.org與 Chrise 聯(lián)系。 | |
聯(lián)系客服