標籤:des style blog http color java 使用 os
防禦式編程並不是說讓你在編程時持“防備批評或攻擊”的態度——“它就是這麼工作!”這一概念來自防禦式駕駛。在防禦式駕駛中要建立這樣一種思維,那就是你永遠也不能確定另一位司機將要做什麼。這樣才能確保其他人在做出危險動作時你也不會受到傷害。你要擔負起保護自己的責任,哪怕是其他司機犯的錯誤。防禦式編程的主要思想是:子程式應該不因為傳入錯誤資料而被破壞,哪怕是由其他子程式產生的錯誤資料。更一般地說,其核心是要承認程式都會有問題,都需要被修改,聰明的程式員應該根據這一點來編程式。
8.1 Protecting Your Program from invalid Inputs
保護程式免遭非法輸入資料的破壞
在學校裡你可能聽說過“垃圾進,垃圾出(garbage in, garbage out)”這句話,這句話說的是軟體開發領域“出門概不退換”原則:讓使用者自己操心自己的事。
對已形成產品的軟體而言,僅僅“垃圾進,垃圾出”還不夠。不管進來什麼,好的程式都不會產生垃圾,而是做到“垃圾進,什麼都不出”、“進來垃圾、出去錯誤提示”或“不許垃圾進來”。按今天的標準來看,“垃圾進,垃圾出”已然成為缺乏安全性的差勁程式的標誌。
通常有三種方法來處理進來垃圾的情況。
檢查所有來源於外部的資料的值 當從檔案、使用者、網路或其他外部介面中擷取資料時,應檢查所獲得的資料值,以確保它在允許的範圍內。對於數值,要確保它在可接受的取值範圍內:對於字串、要確保其不超長。如果字串代表的是某個特定範圍內的資料(如金融交易ID或其他類似資料),那麼要確認其取值合乎用途,否則就應該拒絕接受。如果你在開發需要確保安全的應用程式,還要格外注意哪些狡猾的可能是攻擊你的系統的資料,包括企圖令緩衝區溢位的資料、注入SQL命令、注入的HTML或XML代碼、整數溢出以及傳遞給系統調用的資料,等等。
檢查子程式所有輸入參數的值 檢查子程式輸入參數的值,事實上和檢查來源於外部的數值一樣,只不過是資料來自於其他子程式而非外部介面。
決定如何處理錯誤的輸入資料 一旦檢測到非法的參數,你該如何處理它呢?根據情況的不同,你可以從十幾種不同的方案中選擇其一。
防禦式編碼的最佳方式就是在一開始就不要在代碼中引入錯誤。使用迭代設定、編碼前先寫虛擬碼、寫代碼前先寫測試案例、底層設計檢查等活動,都有助於防止引入錯誤。因此,要在防禦式編程之前優先運用這些技術。所幸的是,你可以把防禦式編程和其他技術結合起來使用。
8.2 Assertions
斷言
斷言(assertion)是指在開發期間使用的、讓程式在運行時進行自檢的代碼(通常是一個子程式或宏)。斷言為真,則表明程式運行正常,而斷言為假,則意味著它已經在代碼中發現了意料之外的錯誤。舉例來說,如果是系統假定一份客戶資訊檔所含的記錄數不能超過50 000,那麼程式中可以包含一個斷定記錄數小於等於50 000的斷言。只要記錄數小於等於50 000,這一斷言都會默默無語。然而一旦記錄數超過50 000,它就會大聲地“斷言”說程式中存在一個錯誤。
斷言對於大型的複雜程式或可靠性要求極高的程式來說尤其有用。通過使用斷言,程式員能更快第排查出因修改代碼或者別的原因,而弄進程式的不匹配的介面假定和錯誤等。
一個斷言通常有兩個參數:一個描述假設為真時的情況的布林運算式,和一個斷言為假時需要顯示的資訊。下面是假定變數denominator(分母)的值應為非零值時Java斷言的寫法:
package com.rollercoaster.codecomplete;/** * 斷言的使用 * @author zhujinrong * */public class AboutAssertion { /** * 入口函數 * @param args */ public static void main(String args[]) { System.out.println("1 / 2 = " + divide(1, 2)); System.out.println("1 / 0 = " + divide(1, 0)); } /** * 計算除法 * @param a 除數 * @param b 被除數 * @return a / b */ public static double divide(double a, double b) { assert b != 0 : "b is unexpectedly equal to 0!"; return a / b; }}
開啟斷言之後,java斷言部分, 參考另一篇文章:01 java斷言assert初步使用:斷言開啟、斷言使用
運行結果如下:
1 / 2 = 0.5
Exception in thread "main" java.lang.AssertionError: b is unexpectedly equal to 0!
at com.rollercoaster.codecomplete.AboutAssertion.divide(AboutAssertion.java:30)
at com.rollercoaster.codecomplete.AboutAssertion.main(AboutAssertion.java:17)
斷言可以用於在代碼中說明各種假設,澄清各種不希望的情形。可以用斷言檢查如下這類假定:
- 輸入參數或者輸出參數的取值處於預期的範圍;
- 子程式開始(或者結束)執行檔案或者流是處於開啟(或關閉)的狀態。
- 子程式開始(或者結束)執行時,檔案或流的讀寫位置處於開頭(或結尾)處;
- 檔案或流已用唯讀、唯寫或者可讀可寫方式開啟;
- 僅用於輸入的變數的值沒有被子程式所修改;
- 指標非空;
- 傳入子程式的數組或其他容器至少能容納X個資料元素;
- 表已初始化,儲存真實的數值;
- 子程式開始(或結束)執行時,某個容器是空的(或滿的);
- 一個經過高度最佳化的複雜子程式的運算結果和相對緩慢但代碼清晰的子程式的運算結果相一致。
正常情況下,你並不希望使用者看到產品代碼中的斷言資訊;斷言主要是用於開發和維護階段。通常斷言只是在開發階段被編譯到目標代碼中,而在產生產品代碼時並不編譯進去。在開發階段,斷言可以協助查清相互矛盾的假定、預料之外的情況以及給子程式的錯誤資料等。在產生產品代碼時,可以不把斷言編譯進目標代碼裡去,以免降低系統的效能。
Building Your Own Assertion Mechanism
建立自己的斷言機制
Guidelines for Using Assertion
使用斷言的指導建議
用錯誤碼來處理預期會發生的情況,用斷言來處理絕不應該發生的狀況。斷言是用來檢查永遠不該發生的情況,而錯誤處理代碼(error-handling code)是用來檢查不太可能經常發生的非正常情況,這些情況是在寫代碼時就能預料到的,且在產品代碼中也要處理這些情況。錯誤處理通常用來檢查有害的輸入資料。而斷言是用於檢查代碼中的bug。
避免把要執行的代碼放到斷言中。這樣的話,一旦關閉斷言,正常的邏輯也沒有執行了。
用斷言來註解並驗證前條件和後條件。
對於高健壯性的代碼,應該先使用斷言再處理錯誤。對於每種可能出錯的條件,通常子程式要麼使用斷言,要麼使用錯誤處理代碼來進行處理,但是不會同時使用二者。一些專家主張只須使用一種處理方法即可。
8.3 Error-handling Techniques
錯誤處理技術
斷言可以處理代碼中不應該發生的錯誤。那麼又如何處理那些預料中可能要發生的錯誤呢?主要有以下幾種處理方式:
返回中立值 有時,處理錯誤資料的最佳做法就是繼續執行操作並簡單地返回一個沒有危害的數值。比如說,數值計算可以返回0,字元操作可以返回Null 字元串,指標操作可以返回一個null 指標。
換用下一個正確的資料
返回與前次相同的資料
換用最接近的合法值
把警告資訊記錄到記錄檔中 在檢測到錯誤資料的時候,你可以選擇在記錄檔(log file)中記錄一條警告資訊,然後繼續執行。
返回一個錯誤碼 你可以決定只讓系統的某些部分處理錯誤。其他部分則不在本地(局部)處理錯誤,只是簡單地報告說有錯誤發生,並信任調用鏈上遊的某個子程式會處理該錯誤。通知系統其餘部分已經發生錯誤可以採用下列方法之一:
- 設定一個狀態變數的值
- 用狀態值作為函數的傳回值
- 用語言內建的異常機制拋出一個異常
在這種情況下,與確定特定的錯誤報表機制相比,更為重要的是要決定系統裡哪些部分應該直接處理錯誤,哪些部分只是報告所發生的錯誤。如果安全性很重要,請確認調用方的子程式總會檢查返回的錯誤碼。
調用錯誤處理子程式或對象 另一種方法則是把錯誤處理都集中在一個全域的錯誤處理子程式或對象中。這種方法的優點在於能把錯誤處理的職責集中到一起,從而讓調試工作更為簡單。而代價則是整個程式都要知道這個集中點並與之緊密耦合。如果你想在其他系統中重用其中的某些代碼,那就得把錯誤處理代碼一併帶過去。
這種方法對代碼的安全性有一定重要的影響。如果代碼發生了緩衝區溢位,那麼攻擊者很可能已經篡改了這一處理常式或對象的地址。這樣一來,一旦在應用程式運行期間發生緩衝區溢位,再使用這種方法就不再安全了。
當錯誤發生時顯示出錯資訊 這種方法可以把錯誤處理的開銷減到最少,然而它也可能會讓使用者介面出現資訊的資訊散布到整個應用程式中。當你需要建立一套統一協調的使用者介面時,或當你想讓使用者介面部分與系統的其他部分清晰的分開時,或者當你想把軟體本地化到另一種不同的語言時,都會面臨挑戰。還有,當心不要告訴系統的潛在攻擊者太多東西。攻擊者有時能利用錯誤資訊來發現如何攻擊這個系統。
用最妥當的方式在局部處理錯誤 一些設計方案要求在局部解決所遇到的錯誤——而具體使用何種錯誤處理方法,則留給設計和實現會遇到錯誤的這部分系統的程式員來決定。
關閉程式 有些系統一旦檢測到錯誤發生就會關閉。這一方法適用於人生安全攸關(safety-critical)的應用程式。
High-Level Design Implications of Error Processing
高層次設計對錯誤處理方式的影響
既然有這麼多的選擇,你就必須注意,應該在整個應用程式裡採用一致的方式處理非法的參數。對錯誤進行處理的方式會直接關係到軟體能否滿足在正確性、健壯性和其他非功能性指標方面的要求。一旦確定了某種方法,就要確保始終如一的貫徹這一方法。