關于防御性編程,你應該知道的事
提起編程,對于程序員同學而言并不陌生,關于防御性編程相信大家也有所耳聞,但是它具體包括哪些內容呢?又有哪些行之有效的處理方案呢?我們又該如何正確應用呢?
本文作者結合實際工作中的一些應用經驗,來全面解析一下防御性編程。
本文主要內容:
- 什么是防御性編程
- 防御性編程的重要性
- 輸入檢查
- 斷言的應用
- 錯誤處理
- 隔離
- 防御策略及建議
1. 什么是防御性編程
關于防御性編程,這一概念開始來自于汽車的防御性駕駛技術,意為你永遠無法確定另一位司機將要做什么,才能確保他人做出危險動作時不會傷害到你。
防御性編程應用過程中,并不是指讓你從保護自身,對他人持有“批判或攻擊”的態度,而是將保護的意識落地到自身程序上,通過一些防御手段讓你的代碼程序不因傳入的錯誤數據而出錯崩潰。大家通常會說,“代碼有問題很正常的呀”,的確是這樣,那更應該在編寫程序的時候提高防御性的重要性,尤其核心程序能力,做好程序錯誤影響的包容性。
2. 防御性編程的重要性
隨著目前互聯網已滲透到各行各業,每個細微的風險問題都可能會被放大,足以影響整個行業。
- 1996年6月4日,歐洲航天局的 Ariane 5 Flight 501 在起飛后 40 秒被引爆。因為導航軟件里的一個 bug,這個價值 10 億美金的運載火箭不得不自毀。
- 2019年1月,拼多多被爆出現重大BUG,用戶可領100元無門檻券,造成大批用戶開始‘薅羊毛’,一晚上200多億都是話費充值。
- 2019年5月時候,部分用戶反映其支付寶出現網絡故障,賬號無法登錄或支付。支付寶官方表示,該故障是由于杭州市蕭山區某地光纖被挖斷導致,這一事件造成部分用戶無法使用支付寶。
系統服務的穩定性對于企業來說非常重要,不僅僅會對企業帶來直接的經濟損失,甚至會對行業、人們的生活造成非常嚴重的影響。
3. 輸入檢查
在學習編碼的時候,估計大家都聽過“不要相信用戶的輸入”,指的就是對用戶輸入做檢查的必要性。談到輸入,常見Web開發主要包括以下兩個方面:
3.1 檢查所有來自系統外部的數據
在系統建設過程中,我們經常會需要跟外部系統做數據交互處理,這里包括:文件、接口、消息隊列、表單用戶輸入等等,對于來自系統外部輸入的數據內容,我們需要明確做到:
- 數據格式是否準確
- 數據類型是否準確
- 數據長度是否準確
對數據做預期準確性檢查,保證輸入數據在我們程序的可接受范圍以內。其實,所有的安全問題的本質都是信任的問題。數據檢查,這個跟車站、機場的安全檢查相似。通過一個安全檢查(過濾,凈化)的過程,可以梳理未知的人或物,使其變得可信任。被劃分出來的具有不同信任級別的區域,我們稱為信任域,劃分兩個不同信任域之間的邊界,我們稱之為信任邊界。對于異常數據處理情況,做好防御檢查的,同時需要做好日志記錄,以防追后賬呢,哈哈~
3.2 檢查接口API的參數值
對于系統內部接口API請求,需要檢查程序的輸入參數的值。這個跟檢查來自外部系統的數據一樣。
/**
* 請求處理通用類
**/
public class CommonRequest {
@NotBlank(message="參數str不能為空")
private String str;
@NotNull(message = "參數i不能為空")
private Integer i;
@Min(value =0,message = "最小值不能小于0")
private int min;
@Max(value=100,message = "最大值不能大約100")
private int max;
}
通常情況下,需要驗證如下幾項:
- 字段必傳和非必傳
- 字段類型是否一致
- 參數值是否合法
- 長度是否符合要求
對于接口參數/字段異常情況,大家可以按照以下思路來驗證問題:
- Q1:如果參數缺失或者漏傳,會有默認值么?
- Q2:如果參數問題,業務邏輯會發生哪些不合理的情況?
- Q3:字段缺失、不合法情況,對于寫操作,是否會造成垃圾數據的產生?
注意:補充一個關鍵情況,需要結合業務場景來評估可能的影響范圍。
必要情況,設置白名單而不是黑名單。
舉個栗子,在你設置圖像擴展名的時候,不要設置無效的類型,而是檢查有效的類型并排除其他類型。在 PHP 有無數的開源校驗庫,讓你的工作更簡單。
要記?。哼M攻是最好的防守??偠灾灰獙⒋a外部的函數調用或方法調用想得太過美好。請確保你調用外部的API和庫之前理解并測試了錯誤。
4. 斷言的應用
4.1 何謂斷言?
所謂斷言,是指在開發期間使用的,讓程序在運行時進行自檢的代碼。通常是一個子程序或者宏。
斷言的目的為了表示與驗證軟件開發者預期的結果,當程序執行到斷言的位置時,對應的斷言應該為真;若斷言不為真時,程序會中止執行,并給出錯誤信息。
舉個例子:
如果系統假定一份數據信息文件所包含的記錄數不超過20000,那么程序中可以設置一個斷定記錄數<=20000 的斷言。只要 記錄數<= 20000,這一斷言都不會觸發,然而一旦記錄數超過20000,它就會斷言程序存在一個錯誤。
4.2 斷言的形式
斷言可以有兩種形式:
- assert Expression1
- assert Expression1:Expression2
其中 Expression1 應該總是一個布爾值,Expression2是斷言失敗時輸出的失敗消息的字符串。
如果Expression1為假,則拋出一個 AssertionError,這是一個錯誤,而不是一個異常,也就是說是一個不可控制異常(unchecked Exception),AssertionError由于是錯誤,所以可以不捕獲,但不推薦這樣做,因為那樣會使你的系統進入不穩定狀態。
public class TestAssert{undefined
public static void main(String[] args){undefined
String name = "abner chai";
//String name = null;
assert (name!=null):"變量name為空null";
System.out.println(name);
}
}
5. 錯誤處理
根據前面的介紹,斷言可以用于處理代碼中不應該發生的錯誤,那又該如何處理那些預料中可能要發生的錯誤呢?
異常和錯誤處理是防御性編程的一個組成部分。
想象一下,啟動了一個異步操作,運行并輸出結果,沒有異常,這是一個理想的情況。如果在執行過程中發生錯誤怎么辦?與任何未處理的異常一樣,應用程序通常會崩潰。假設任何異步操作都會成功運行而沒有任何錯誤,那么可能會失敗。
高級語言中一般會采用try catch方式捕獲異常處理,如下示例:
try {
//邏輯代碼
} catch (exception e){
//異常處理代碼
}
try{
//邏輯代碼
} finally {
//一定要執行的代碼
}
try {
//邏輯代碼
} catch (exception e){
//異常處理代碼
} finally{
//一定要執行的代碼
}
而Golang的錯誤處理規范也是Go語言的最大亮點之一。
- error接口
標準庫把error定義為接口類型, 以便于自己定義錯誤類型。
type error interface{
Error() string
}
- Painc
golang的內置方法,能夠改變程序的控制流。當函數調用了panic,函數會停止運行,但是defer函數會運行,程序會在當前panic的goroutine全部退棧以后crash。
- Recover
recover也是golang的內置方法,用于恢復發生panic的goroutine的控制,recover只在defer函數中生效。如果當前goroutine將要發生panic的話, recover會捕獲這個panic,并恢復正常執行。
- Defer
聊到panic和recover,需要聊聊defer這個關鍵字,后面會看到defer在異常處理機制中起到的作用。go的defer是用來延遲執行函數的,延遲的發生是在調用函數的returen之后。
6. 隔離
所謂隔離,是指程序可以包容由錯誤造成的損害,稱為一種容損策略。這個在軟件行業中最常見的方案,就是多機房建設。實現服務雙機房部署建設,承受單機房故障,保障用戶體驗。這個實現的難點主要有如下三點:
- 跨機房網絡延遲和帶寬限制導致的數據層面一致性問題
- 機房之間流量調度
- 業務改動不能太大
各個大廠對于核心服務多機房部署的實現,簡單列舉以下幾種實現方案:
- 高德
基于地理位置單元化,不同服務集群間雙向數據復制,內部調用路由。
- 微博
MySQL多機房同步(寫入時寫但機房,有專門的組件負責同步寫入到另一個機房)。
隔離的應用,同時體現了在架構設計上規定應該如何應用如何處理錯誤的價值。
7. 防御策略及建議
在防御性編程的路上,沒有銀彈。在產品中保留過多的防御性代碼,則會與精簡代碼實現產生相矛盾的地方。從產品本身出發,在不影響用戶體驗的使用的情況下,使程序能夠穩定的運行,梳理了如下幾項建議:
- 保留重要錯誤檢查的代碼,去掉檢查細微錯誤的代碼
- 保留讓程序穩妥地崩潰的代碼,去掉會導致程序硬性崩潰的代碼
- 確認代碼中的錯誤消息是友好的,為技術支持人員做好錯誤信息記錄
其實,對于防御性編程,我們其實是要在保障程序穩定和程序不過于臃腫之間找到一個合理的平衡。防御式編程技術可以讓錯誤更容易發現,更容易修改,并減少錯誤對代碼的破壞,斷言可以幫助人們更早的發現錯誤,關于如何處理錯誤輸入的決策是一項關鍵的錯誤處理決策,也是一項關鍵的高層設計決策。防范看似微小的錯誤,收益價值可能遠遠超出你的想象。