FindBugs 是什麼?
FindBugs 是一個靜態分析工具,它檢查類或者 JAR 檔案,將位元組碼與一組缺陷模式進行對比以發現可能的問題。有了靜態分析工具,就可以在不實際運行程式的情況對軟體進行分析。不是通過分析類檔案的形式或結構來確定程式的意圖,而是通常使用 Visitor 模式(請參閱 參考資料)。圖 1 顯示了分析一個匿名項目的結果(為防止可怕的犯罪,這裡不給出它的名字):
在FindBugs的GUI中,需要先選擇待掃描的.class檔案(FindBugs其實就是對編譯後的class進行掃描,藉以發現一些隱藏的bug。)。如果你擁有這些.class檔對應的源檔案,可把這些.java檔案再選上,這樣便可以從稍後得出的報告中快捷的定位到出問題的代碼上面。此外,還可以選上工程所使用的library,這樣似乎可以幫助FindBugs做一些高階的檢查,藉以發現一些更深層的bug。
選定了以上各項後,便可以開始檢測了。檢測的過程可能會花好幾分鐘,具體視工程的規模而定。檢測完畢可生成一份詳細的報告,藉由這份報告,可以發現許多代碼中間潛在的bug。比較典型的,如引用了空指針(null pointer dereference), 特定的資源(db connection)未關閉,等等。如果用人工檢查的方式,這些bug可能很難才會被發現,或許永遠也無法發現,直到運行時發作…當除掉了這些典型的(classic) bug後,可以確信的是,我們的系統穩定度將會上一個新的台階。
以目前遇到的狀況來看,FindBugs可以有兩種使用時機。
開發階段
當Developer完成了某一部分功能模組開發的時候(這通常是指代碼撰寫完成,並已debug通過之後),可藉由FindBugs對該模組涉及的java檔案進行一次掃描,以發現一些不易察覺的bug或是效能問題。交付新版的時候,開發團隊可以跑一下FindBugs,除掉一些隱藏的Bug。FindBugs得出的報告可以作為該版本的一個參考文檔一併交付給測試團隊留檔待查。
在開發階段使用FindBugs,一方面開發人員可以對新版的品質更有信心,另一方面,測試人員藉此可以把更多的精力放在業務邏輯的確認上面,而不是花大量精力去進一些要在特殊狀況下才可能出現的BUG(典型的如Null Pointer Dereference)。從而可以提高測試的效率。
維護階段
這裡指的是系統已經上線,卻發現因為代碼中的某一個bug導致系統崩潰。在除掉這個已暴露的bug之後,為了快速的找出類似的但還未暴露的 bug,可以使用FindBugs對該版的代碼進行掃描。當然,在維護階段使用FindBugs往往是無奈之舉,且時間緊迫。此外,如果本來在新版交付的時候就使用過FindBugs的話,往往意味著這種bug是FindBugs還無法檢測出的。這也是FindBugs局限的地方。
FindBugs出到目前的版本,功能已經相當強大,不過也有待完善的地方。從實際使用來看,有一些隱藏的bug並不能靠FindBugs直接發現。那么,可不可以撰寫一個新的 Detector,來發現這種將一個未初始化的reference傳來傳去而形成的潛在的bug呢?理論上來講,應該是可以的。這個 Detector目前還未實現。哪位如果有興趣的話,可以參考FindBugs, Part 2: Writing custom detectors(擴展閱讀)這篇文章,幫忙實現這個Detector。實現一個新的Detector,便可以檢測出一種新型的bug,這樣不知又可以幫開發人員省去多少人工檢查的時間,功德無量啊。
FindBugs也不能發現非java的Bug。對於非java撰寫的代碼,如javascript,SQL等等,要找出其中可能的bug,FindBugs是無能為力的。當然,javascript中的bug似乎還不至於使系統崩潰,而SQL中的bug往往又跟業務邏輯相關,只要測試仔細一些應該是可以發現的。
FindBugs不過是一個工具。作為開發人員,當然首先要在編程的時候努力避免引入bug,而不要依賴於某個工具來為自己把關。不過由於代碼的複雜性,一些隱藏的bug確實很難靠咱們的肉眼發現。這時,套用一些好的工具或許就可以幫你發現這樣的bug。這便是FingBug存在的價值。
為什麼應該將 FindBugs 集成到編譯過程中?
經常問到的第一個問題是為什麼要將 FindBugs 加入到編譯過程中?雖然有大量理由,最明顯的回答是要保證儘可能早地在進行編譯時發現問題。當團隊擴大,並且不可避免地在項目中加入更多新開發人員時,FindBugs 可以作為一個安全網,檢測出已經識別的缺陷模式。我想重申在一篇 FindBugs 論文中表述的一些觀點。如果讓一定數量的開發人員共同工作,那么在代碼中就會出現缺陷。像 FindBugs 這樣的工具當然不會找出所有的缺陷,但是它們會幫助找出其中的部分。現在找出部分比客戶在以後找到它們要好——特別是當將 FindBugs 結合到編譯過程中的成本是如此低時。
一旦確定了加入哪些過濾器和類,運行 FindBugs 就沒什麼成本了,而帶來的好處就是它會檢測出新缺陷。如果編寫特定於應用程式的檢測器,則這個好處可能更大。
生成有意義的結果
重要的是要認識到這種成本/效益分析只有在不生成大量誤檢時才有效。換句話說,如果在每次編譯時,不能簡單地確定是否引入了新的缺陷,那么這個工具的價值就會被抵消。分析越自動化越好。如果修復缺陷意味著必須吃力地分析檢測出的大量不相干的缺陷,那么您就不會經常使用它,或者至少不會很好地使用它。
確定不關心哪些問題並從編譯中排除它們。也可以挑出 確實關注的一小部分檢測器並只運行它們。另一種選擇是從個別的類中排除一組檢測器,但是其他的類不排除。FindBugs 提供了使用過濾器的極大靈活性,這可幫助生成對團隊有意義的結果,由此我們進入下一節。
確定用 FindBugs 的結果做什麼
可能看來很顯然,但是您想不到我參與的團隊中有多少加入了類似 FindBugs 這樣的工具而沒有真正利用它。讓我們更深入地探討這個問題——用結果做什麼?明確回答這個問題是困難的,因為這與團隊的組織方式、如何處理代碼所有權問題等有很大關係。不過,下面是一些指導:
可以考慮將 FindBugs 結果加入到原始碼管理(SCM)系統中。一般的經驗做法是不將編譯工件(artifact)放到 SCM 系統中。不過,在這種特定情況下,打破這個規則可能是正確的,因為它使您可以監視代碼質量隨時間的變化。
可以選擇將 XML 結果轉換為可以傳送到團隊的網站上的 HTML 報告。轉換可以用 XSL 樣式表或者腳本實現。有關例子請查看 FindBugs 網站或者郵件列表(請參閱 參考資料)。
像 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")) |
檢測器:Null 指針對 null 的解引用(dereference)和冗餘比較
這個檢測器查找兩類問題。它查找代碼路徑將會或者可能造成 null 指針異常的情況,它還查找對 null 的冗餘比較的情況。例如,如果兩個比較值都為 null,那么它們就是冗餘的並可能表明代碼錯誤。FindBugs 在可以確定一個值為 null 而另一個值不為 null 時,檢測類似的錯誤,如清單 2 所示:
清單 2. Null 指針示例
1 Person person = aMap.get("bob"); 2 if (person != null) { 3 person.updateAccessTime(); 4 } 5 String name = person.getName(); |
檢測器:初始化之前讀取欄位
這個檢測器尋找在構造函式中初始化之前被讀取的欄位。這個錯誤通常是——儘管不總是如此——由使用欄位名而不是構造函式參數引起的,如清單 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 } |
這些例子只是 FindBugs 所發現的問題種類的一小部分(更多信息請參閱 參考資料)。在撰寫本文時,FindBugs 提供總共 35 個檢測器。
開始使用 FindBugs
要運行 FindBugs,需要一個版本 1.4 或者更高的 Java Development Kit (JDK),不過它可以分析由舊版本的 JDK 創建的類檔案。要做的第一件事是下載並安裝最新發布的 FindBugs——當前是 0.7.1 (請參閱 參考資料)。幸運的是,下載和安裝是相當簡單的。在下載了 zip 或者 tar 檔案後,將它解壓縮到所選的目錄中。就是這樣了——安裝就完成了。
安裝完後,通過一個示例類運行它。在此處我將針對 Windows 用戶進行講解。打開命令行提示符號並進入 FindBugs 的安裝目錄。例如: C:\apps\FindBugs-0.7.3。
在 FindBugs 主目錄中,有幾個值得注意的目錄。文檔在 doc 目錄中,bin 目錄包含了運行 FindBugs 的批處理檔案,即運行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 name="FindBugs" classname="edu.umd.cs.FindBugs.anttask.FindBugsTask"/> |
清單 4. 創建 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> |
第 1 行:注意 target 取決於編譯。一定要記住處理的是類檔案而不是源檔案,這樣使 target 對應於編譯目標保證了 FindBugs 可在最新的類檔案運行。FindBugs 可以靈活地接受多種輸入,包括一組類檔案、JAR 檔案、或者一組目錄。
第 2 行:必須指定包含 FindBugs 的目錄,我是用 Ant 的一個屬性完成的,如下所示:
<property name="FindBugs.home" value="C:\apps\FindBugs-0.7.3" /> |
第 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 集成到編譯過程中吧!