百萬分之一
作為一個勤奮的開發人員,您已經為幾個需要更好地訪問復雜的大量數據存儲的客戶安裝了一個應用程序,它編寫良好,而且經過了充分測試。
對每個客戶,現場測試階段都暢通無阻地通過了。您在去銀行的路上,心裡極少考慮這六個月來的軟件審查,這時您的傳呼機響了起來。您的一個客戶在使用您的軟件運行一個報表時,系統崩潰了。
您趕到出事地點,運行了一個隨機測試。工作良好。您運行另一個。沒出現問題。您又運行了數百個測試。還是沒有問題。您又檢查了持續六個月運行這個應用程序的其它客戶。沒有投訴。
您重復運行那個引起問題的報表。崩潰!怎麼回事?
破壞者數據錯誤模式
許多程序需要頻繁訪問和處理內部儲存的數據來執行各種復雜的任務。這種數據可以從內存中的大型結構、數據庫或網絡上檢索得到。
這類程序非常容易遭受損壞的內部數據引起的崩潰。我稱這種錯誤模式為破壞者數據模式,是因為這種數據可以無限期地存在於系統中(很象冷戰中的潛伏間諜一樣),不引發任何問題,直到訪問一段特定的數據時,損壞的數據才象炸彈一樣爆炸。
語法原因
假定我們有一個 JDBC 應用程序,它存儲了一個名為 Mapping 的數據庫表,該表將 String 的名稱映射到一系列元素的集合。(請參閱 參考資料,以獲取關於 JDBC API 的更多信息。)每個集合中的每個元素都引用另一個表中的一個關鍵字(該表名為 Properties,包含這些元素的不同已知屬性)。
這樣說吧,Mapping 和 Properties 表最初都是從一個文本文件中讀取的,這個文本文件由外部源( 外部意為不是內部產生的任意數據源)發展而來,而在外部源中,每行都以一個名稱開頭,後面跟著對應集合的表達,如下所示:
清單 1. 樣本,外部源文本文件
In the Mapping file:
apples {macintosh, gala, golden-delicious}
trees {elm, beech, maple, pine, birch}
rocks {quartz, limestone, marble, diamond}
...
In the Properties file:
macintosh {color: red, taste: sour}
gala {color: red, taste: sweet}
diamond {color: clear, rigidity: hard, value: high}
...
可以對 Mapping 和 Properties 表條目進行語法分析並將其傳遞到一個方法中,此方法會把這些條目插入到一個數據庫中。但這種方法存在潛在的缺陷。例如,假定我們已經編寫了一個處理 JDBC 兼容數據庫的類。遵照 JDBC API,我們可以定義一個 PreparedStatement 對象並使用它把信息傳遞到數據庫中,如下所示:
清單 2. 使用 StreamTokenizer 插入域和區域字符串
...
PreparedStatement insertionStmt =
con.prepareStatement("INSERT INTO MAPPING VALUES(?,?)");
...
public void insertEntry(String domain, String range)
throws SQLException {
insertionStatement.setString(1, domain);
insertionStatement.setString(2, range);
insertionStatement.executeUpdate();
}
以這種方式插入兩個 String 合適與否取決於從文本文件中獲取 String 的方式。例如,假定一個簡單的正則表達式匹配工具被用來將每一行拆分成兩個 String :
一個 String 包含第一個 String 之前的全部字符。
一個 String 包含第一個 String 之後的全部字符。
這種對文本文件進行的基本的語法分析不會捕獲數據的較小損壞。例如,如果其中的一行是如下的形式:
清單 3. 數據破壞者
trees {elm, beech, maple, pine birch}
“pine”和“birch”之間的逗號漏掉了。這樣的錯誤很容易由生成文件的工具的錯誤或手工編輯文件而造成。
無論如何,數據都會以損壞的形式進入數據庫,靜靜地等待被訪問。如果用於訪問數據的方法要求用逗號和空格來分隔條目,在讀取這個條目的時候就會導致崩潰。
如果程序只是簡單地使用逗號來區分集合中的元素,更加嚴重的錯誤都可能發生。系統可能將“pine birch”解釋為一個單獨的樹類型(數據的單個條目),並將這個錯誤進一步傳播到計算中去。
語義原因
我們的示例是一個違反了數據的一個簡單的語法特征演示。當然,這不是可能損壞數據的唯一途徑。
語義級別上的限定因素也可能被破壞。在我們的示例中,Mapping 表中數據的一種要求是,每個集合中的每個元素都是 Properties 表中的一個域條目。如果這種不變量被破壞,程序就會在試圖讀取一個在 Properties 表中並不存在的元素時失敗,導致異常被拋出。
在本文中,我使用數據庫條目作為示例,但是破壞者數據錯誤會以各種方式出現 ― 與數據輸入的方式一樣多。當程序讀取數據時,不管它是從文件、鍵盤、麥克風、網絡還是電子手套讀取,破壞者數據錯誤都有可能存在。
治療和預防措施
最好的防備破壞者數據錯誤的方法是編譯器和解釋器開發人員普遍采用的那種方法。由於輸入到這些程序的數據是如此復雜,開發人員別無它法,只有在第一次讀取輸入內容的時候就執行盡可能徹底的完整性檢查,而不是在以後訪問的時候再進行檢查。
語法分析作為消除錯誤的方法
實際上,對輸入內容進行 語法分析的方法恰好是消除大量這些錯誤的途徑。不幸的是,很多程序員(他們從來不會考慮編寫沒有語法分析器的編譯器)沒有能夠為較簡單的數據編寫足夠的語法分析方法。較簡單的數據的語法分析當然要容易一點,但是這並不能成為根本不對它進行語法分析的借口。
任何讀取數據的程序 ― 不管有多簡單 ― 都應該對數據進行語法分析。畢竟,這種程序在它的有效輸入的集合所定義的“語言”上可以被看作是一個編譯器(或者解釋器)。
從經歷過的人那裡吸取一點經驗吧。我年輕時比較魯莽,犯了個錯誤 ― 處理數據的時候沒有作適當的語法分析,然後我就遭受了那樣的後果 ― 猖獗的破壞者。我可不推薦這種經驗。
類型檢查作為消除錯誤的方法
編譯器為許多語言(當然包括 Java 語言)所作的檢查的另一種普遍形式是 類型檢查。類型檢查是程序完整性上的語義級別檢查的一個示例。
如果類型系統是健壯的(就象 Java 類型系統一樣),這種完整性檢查確實可以保證很多錯誤永遠不會在運行時產生。象語法分析一樣,編譯器編寫者的這個示例可以應用於其它經常在其輸入數據上規定語義級別不變量的程序。這些不變量通常不是明確的,但可以通過進行對應的檢查來把它們變成明確的。
反復操作作為消除錯誤的方法
當然,如果您懷疑這種錯誤模式的出現與已經讀入並存儲的數據有關,反復操作數據也是明智的:訪問在實際配置的應用程序中可能要操作的每個數據,保證一切都象期望的那樣工作。通過這種方法,您也能夠修正簡單的錯誤。
關於消除錯誤方法的一個告誡
我的意思當然不是暗示執行足夠的檢查來消除程序中的所有破壞者數據 總是可行的。如果真是那樣,就不會有引起錯誤模式的潛在問題了。
一個破壞者在開始帶來災難之前為什麼會無法覺察是有很多原因的:
執行所有檢查必需的數據直到破壞者數據已經存儲了以後才可用。
整套的限定元素甚至是無法計算的(就象編譯器和解釋器的情況一樣)。
限定元素可以計算,但是程序無法訪問檢查它們所需要的資源。
在這些情況下,我們最好是盡量消除可能的破壞者形式。
結論
下面是破壞者數據錯誤模式的總結:
模式:破壞者數據
症狀:一個存儲和處理復雜的輸入數據的程序在執行任務時意外地崩潰,而這個任務和其它沒有產生任何問題的任務很相似。
起因:一些內部數據被損壞,可能是語法上的損壞,也可能是語義上的。
治療和預防措施:對輸入數據盡量多地執行完整性檢查,而且要盡量早執行。對於已經損壞的持久數據,研究它並檢查其完整性。
消除數據破壞者的黃金法則: 任何讀取數據的程序都應該對數據進行語法分析。願您能順利消除這些錯誤!