防御性编程(Defensive Programming)
什么是防御性編程?(What is Defensive Programming?)
garbage in ,garbage out (GIGO),作為一條計算機界的“俗語”,一條相對“學院派”的設計理念,我們或多或少都有聽過。
但在實際的工程環境下,GIGO已然成為了一種“不作為”、“缺乏安全性”的標志。
所以我們要的是:不論進來什么,好的程序都不會生成“垃圾”。
如何踐行防御性編程?(How to make the program defensive)
檢查所有源于外部的數據
檢查所有來源千外部的數據的值當從文件、用戶、網絡或其他外部接口中獲取數據時,應檢查所獲得的數據值,以確保它在允許的范圍內。
對于數值,要確保它在可接受的取值范圍內;對于字符串,要確保其不超長。如果字符串代表 的是某個特定范圍內的數據(如金融交易ID或其他類似數據),那么要確認其取值合乎用途,否則就應該拒絕接受。
如果你在開發需要確保安全的應用程序,還要格外注意那些狡猾的可能是攻擊你的系統的數據,包括企圖令緩沖區溢出的數據、注入的SQL命令、注入的HTML或XML代碼、整數溢出以及傳遞給系統調用的數據。
檢查子程序所有輸入的參數的值
檢查子程序輸入參數的值,事實上和檢杳來源于外部的數值一樣,只不過數據是來自千其他子程序而非外部接口。
同時保證子程序的“隔離性”,以子程序為單位保證自己的“防御性”,那么整體的防御性就能得到較為有效的保證。
決定如何處理錯誤的輸入數據
見下文“錯誤處理手段”
通過“工具”幫助我們更好的構建程序
斷言(Assertion)
斷言(assertion)是指在開發期間使用的、讓程序在運行時進行自檢的代碼(通常是一個子程序或宏)。斷言為真,則表明程序運行正常,而斷言為假,則意味著 它已經在代碼中發現了意料之外的錯誤。
JAVA從1.4開始支持斷言,默認JVM是關閉斷言檢測的,若要開啟需要添加參數 - enableassertions
assert <布爾表達式> : <錯誤信息>
斷言對千大型的復雜程序或可靠性要求極高的程序來說尤其有用。 通過使用 斷言, 程序員能更快速地排查出因修改代碼或者別的原因, 而導致進程序里的不匹配的接口假定和錯誤等。
斷言適用范圍:
輸入參數或輸出參數的取值處于預期的范圍內;
子程序開始(或者結束)執行時文件或流是處于打開(或關閉)的狀態;
子程序開始(或者結束)執行時,文件或流的讀寫位置處于開頭(或結尾)處;
文件或流已用只讀、 只寫或可讀可寫方式打開;
輸入的變量的值沒有被子程序所修改;
指針非空;
傳入子程序的數組或其他容器至少能容納X個數據元素;
Collection是否已經被初始化
子程序開始(或結束)執行時, 某個容器是空的(或滿的)
一個經過高度優化的復雜子程序的運算結果和目標子程序的運算結果相一致
斷言的使用哲學:
1、斷言是用來檢查永遠不該發生的情況,而異常處理是用來檢查不太可能發生的非正常情況。異常處理的情況應該能在寫代碼的時候就預料到。
2、避免在斷言中直接調用方法,因為jvm默認不開啟斷言,斷言中調用的方法會被忽略。
如:
public static void main(String[] args) {assert test(); } public static boolean test(){System.out.println("123");return true; }3、先使用斷言,在處理異常
隨著項目迭代周期的變長,我們可能會由不同的人、不同的團隊處理項目中不同的模塊,從而導致代碼質量的不統一,以至于在產品交付之前發現所有bug是不現實的。為了應對這匯總情況,我們應同時用斷言和錯誤處理代碼來處理同一個錯誤,我們應在每個需要判斷的地方加入斷言,同時加入對此錯誤的異常處理,以保證整個功能的最大可用。
錯誤處理手段
1、返回中立值
與業務無關的非必需值出錯,我們可以采用返回中立值,如 0 進行處理異常。但要注意,對于關鍵業務數據(如 持倉金額等)務必不要使用中立值,避免對實際業務產生誤導性影響。
與業務相關的數據出錯,我們可以采用返回null等,通過不展示的方式明確此部分數據異常。
與此同時帶來了新的問題:我們如何面對API,因為我們知道Api也可能采用相同的辦法返回一個null
所以我們得到的結論是:我們不應相信任何一個api,最好進行null的判斷
2、換用下一個正確數據
在處理數據流的時候,有時只需返回下一個正確的數據 即可。如果在讀數據庫記錄并發現其中一條記錄已經損壞時,你可以繼續讀下去直到又找到一條正確記錄為止。
例如請求客戶數據接口,可能會出現接口在某一時刻不可用的情況,可以采用重復請求的方式,試圖獲取正常的數據。
3、返回與前次相同的數據
在一些非敏感的數據環境,你可以采用返回前一次請求的數據,因為大部分數據在較短時間內不會產生太大改變(若業務數據與客戶、用戶等有關聯,則可以考慮采用返回當前客戶的前一條數據)。
4、換用最接近的合法值
此方式在實際業務中有較多使用。例如某產品某一天凈值缺失,則取前一天或后一天的凈值以補全。(此方法得到大多數客戶的認可。)
5、把警告信息記錄到日志中
此方法可以其他方法結合使用,盡量保證所有可預期的異常都打印響應日志,入參、必要信息等,方便后續bug排查。
6、調用錯誤處理子程序(結合異常處理)
通過統一的異常處理子程序或對象,對全局(或局部)異常統一處理,簡化異常處理的整體流程。
7、當錯誤發生時顯示出錯消息
這種方法可以極大的減少錯誤處理的開銷,但他也可能會讓用戶界面中出現的錯誤信息成為攻擊者的“突破口”
8、用最妥當的方式在局部處理錯誤
一些設計方案要求能在局部解決所有遇到的錯誤,而具體使用何種錯誤處理方法,則留給設計和實現會遇到錯誤的這部分系統的程序員決定。
這種方法給予了開發者很大的靈活度,但系統的整體性將無法滿足其對正確性和可靠性的需求,不同的開發人員處理特定錯誤的辦法可能不盡相同,難以有統一標準。
9、關閉程序
簡單粗暴,但能有效的阻止“致命”且無法處理的異常在系統中蔓延
如果用作控制治療癌癥病人的放 療設備的軟件接收到了錯誤的放射劑量輸入數據, 那么怎樣處理這一錯誤最好?
異常處理
審慎明智的使用異常處理,可以有效降低項目的復雜度,而草率不負責任的使用,則只會讓代碼變得無法理解。
1、用異常通知程序的其他部分,發生了不可忽略的錯誤:異常的好處就是,這種錯誤是無法被忽略,必須要處理的,而其他方式,如替換下一個正確數據,則很容易導致異常的擴散。
2、只在真正例外的情況下才拋出異常僅在真正例外的情況下才使用異常一換句話說,就是僅在其他編碼實踐方法無法解決的情況下才使用異常。
優秀的的產品,系統內部不應發生異常,異常僅用來處理不罕見甚至永遠不該發生的情況,和應對外部依賴的變化。
且由于調用子程序的代碼需要了解被 調用代碼中可能會拋出的異常,因此異常弱化了封裝性,這與降低代碼復雜度的愿景是背道而馳的。
3、異常不是推卸責任的理由:如果能在局部處理的異常,請在局部處理。不要把原本可以處理的異常當做一個未捕獲的異常拋出。
4、避免在構造函數和析構函數中拋出異常,除非你在同一地方把它們捕獲當:從構造函數和析構函數里拋出異常時,處理異常的規則馬上就會變得非常復雜。 比如說在C++里,只有在對象已完全構造之后才可能調用析構函數,也就是說, 如果在構造函數的代碼中拋出異常,就不會調用析構函數,從而造成潛在的資源泄漏(Meyers1996, Stroustrup 1997)。在析構函數中拋出異常也有類似復雜的規則。
5、在異常消息中加入導致異常發生的全部信息:這有助于異常的排查,讓異?!案袃r值”
6、盡量避免空的catch,至少打印一下基本日志!!!
7、了解你所依賴的函數庫,可能出現的異常。
系統設計的影響
健壯性與正確性
健壯性:系統在不正常輸入或不正常外部環境下仍能夠表現正常的程度。我們在編程時往往會設定一定的規約,即輸入一些數據并且將這些數據經過處理后進行輸出,但是有時用戶會輸入一些非法數據,有可能會使程序做出一些未期望的行為并且使程序非法終止,所以為了使程序在這種情況下依然能夠準確無歧義的向用戶展示全面的錯誤信息以有助于DEBUG,程序的健壯性就顯得十分重要。
正確性:正確性是最重要的質量指標,是程序按照spec加以執行的能力。在程序出現bug時,正確性著重在于永不給用戶錯誤的結果,而健壯性則傾向于盡可能保持軟件運行而不是總是退出。
即正確性傾向于直接報錯,而健壯性傾向于容錯。
所以如何平衡健壯性和準確性,將貫穿整個代碼設計、產品設計始終。
隔離程序
在代碼設計時,以某一個功能為邊界,設計子程序,同時維護這個子程序整體的“防御性”。這樣的設計非常清晰的劃分我們防御的“邊界”,讓一部分模塊負責“防御”,從而解放的大多數模塊,也從某個側面提升了邏輯的“純粹”,提高了代碼的可讀性,讓復用變得更加容易。
當然有時候我們可能需要復數的過濾模塊,把控過濾模塊的數量、模塊內的復雜度,有助于我們維護好過濾模塊之間的關系,提升整個子程序的穩定性。
在開發過程中合理的消耗資源,引入輔助調試代碼,以便及時對錯誤進行診斷(可被及時移除)
復雜項目從開發早期開始,引入一些輔助調試代碼,可以有效的降低我們甄別定位錯誤的成本。當然這會花費額外的開發成本,但對于復雜程序或調試困難的部分來講,這是值得的。
當然我們也可以采用一些工具,例如skyWalking等,通過探針等技術實現與輔助代碼相同的功能。
注意:我們自行編寫的輔助代碼,需要在代碼交付時可以輕松的被移除或禁用。
進攻性編程、防御的偏執
當然,防御性編程是好的。但凡是過猶不及,當我們太過在意防御,而陷入了某種偏執,便會讓代碼產生“異味”。
public String badlyImplementedGetData(String urlAsString) {// Convert the string URL into a real URLURL url = null;try {url = new URL(urlAsString);} catch (MalformedURLException e) {logger.error("Malformed URL", e);}// Open the connection to the serverHttpURLConnection connection = null;try {connection = (HttpURLConnection) url.openConnection();} catch (IOException e) {logger.error("Could not connect to " + url, e);}// Read the data from the connectionStringBuilder builder = new StringBuilder();try (BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()))) {String line;while ((line = reader.readLine()) != null) {builder.append(line);}} catch (Exception e) {logger.error("Failed to read data from " + url, e);}return builder.toString(); }此代碼只是將URL的內容作為字符串讀取。 數量驚人的代碼可以完成非常簡單的任務,這很java。
java的check Exception可以讓你忽略這些問題,并繼續處理。甚至java在孤立你這么做。
“防御性編程”或“健壯性”的信奉者可能會認為,這很nice,這樣程序并不會崩潰。但我們在真的遇到問題的時候,我們已經失去了真實的上下文,且程序并沒報告任何錯誤。
此時,我們可以引入一個新的概念“攻擊性編程”,并暴力的重構這段代碼
public String getData(String url) throws IOException {HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();// Read the data from the connectionStringBuilder builder = new StringBuilder();try (BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()))) {String line;while ((line = reader.readLine()) != null) {builder.append(line);}}return builder.toString(); }OK,我們暴力的簡化了這段代碼,如果出現錯誤,則用戶和日志(大概)會收到正確的錯誤消息。
“攻擊性編程”的核心思想,就是在開發階段,盡可能“暴露”問題,及問題發生的環境,并加劇它產生的破壞,以此來警示開發者,修復這個問題。
如何采取“攻擊性編程”:
1、確保斷言語句使程序終止運行。不要讓程序員養成壞習慣,一碰到已知問 題就按回車鍵把它跳過。讓問題引起的麻煩越大越好,這樣它才能被修復。 完全填充分配到的所有內存,這樣可以讓你檢測到內存分配錯誤。
2、完全填充已分配到的所有文件或流,這樣可以讓你排查出文件格式錯誤。 確保每一個case語句中的default分支或else分支都能產生嚴重錯誤(比 如說讓程序終止運行),或者至少讓這些錯誤不會被忽視。
3、在刪除一個對象之前把它填滿垃圾數據。
4、讓程序把它的錯誤日志文件用電子郵件發給你,這樣你就能了解到在已發 布的軟件中還發生了哪些錯誤——如果這對于你所開發的軟件適用的話。
5、信任內部數據的有效性
6、信任引用的組件
離經叛道卻又有其合理性,最好的防守正是大膽進攻。在開發時慘痛地失敗,能讓你在發布產 品后不會敗得太慘。
當然,這種設計方式僅適用于開發階段,我們最后還是要完善各個地方的異常處理,保證產品在上線時能盡量少的暴露問題,讓程序穩定的處理發生的異常或停止。
我們需要多少防御式代碼?
1、保留哪些之檢查重要錯誤的代碼:我們在設計之初,就應該明確哪些部分是“不能忍受”錯誤的,哪些部分是可以接受錯誤的。
例如:我們可以接受頁面上某個oss資源或描述文案不顯示,但不接受涉及金額的用戶數據“出錯”。
2、關閉檢查細微錯誤的代碼:如果一個錯誤帶來的影響微乎其微,那么可以把檢查他的代碼“關閉”,注意:關閉 ,而非刪除。
3、去掉可以導致程序硬性崩潰的代碼。如上述“攻擊性編程”涉及的代碼,會導致項目停止或其他嚴重后果的代碼,不應出現在正式生產環境中。
4、為“開發人員”記錄正確的異常信息
5、確認異常信息是“交互友好的”。
我們需要做什么?
務實
理性且克制
一些其他的Tips
1、不要主動延長對象的使用周期
例如:使用迭代器時,盡量使用for而非while,因為使用while循環,迭代器將聲明在循環外,容易產生錯誤的重用。
2、補充讀物
《Defensive Coding Guide》https://developers.redhat.com/articles/defensive-coding-guide
3、NASA編碼標準 簡化為JS編碼標準 《NASA coding standards, defensive programming and reliability》
NASA => JavaScript
No function should be longer than a single sheet of paper 📝 => 1 function should do only 1 simple thing
Only use simple control flows, no goto statements and recursion => write predictable code, follow coding standards and use static analysis
Do not use dynamic memory allocation after initialization => Measure (benchmark) and compare (profile, memory snapshots) to detect possible memory leaks. Use object pooling, write clean code and use ESLint no-unused-vars.
All loops ? must have a fixed set of the upper bound. => recommend not to follow this rule, we need flexibility and recursion.
Assertion density should be at least 2 per function. => Minimal amount of tests is 2 per function, having a higher density is better of course. Watch for runtime anomalies.
Data objects must be declared at the smallest possible level of scope. => No shared state.
The return of functions must be checked by each calling function, and the validity of parameters must be checked inside each function. => we should skip it
Use of preprocessor must be limited. => JavaScript is transpiled by each browser, so we must monitor the performance of our code.
The use of pointers should be restricted. => Call chains and loose coupling should be used more often.
Code must be compiled with warnings enabled. ?? **** => Keep the project green from the first day of dev. If the tests are failing: prioritize, refactor and add new tests.
參考:
《代碼大全》第二版 第八章
《Defensive Programming Techniques》 https://www.linkedin.com/pulse/defensive-programming-techniques-omar-ismail
《Offensive programming》 https://www.javacodegeeks.com/2013/09/offensive-programming.html
《Defensive Programming Techniques Explained with Examples》https://www.golinuxcloud.com/defensive-programming/
《NASA coding standards, defensive programming and reliability》 https://coder.today/tech/2017-11-09_nasa-coding-standards-defensive-programming-and-reliability-a-postmortem-static-analysis./
總結
以上是生活随笔為你收集整理的防御性编程(Defensive Programming)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: WEB服务器、应用程序服务器、HTTP服
- 下一篇: linux cocoapods安装过程,