這篇文章記錄我的一些思考。在工作了一段時間之后。
問題的核心很簡單:到底如何返回錯誤信息。
學生時代,見到過當時的老師的代碼:
1 if (foo() == null) { 2 3 }
當然,這位老師是一位比較擅長c/c++的老程序員,所以他的代碼其實使用c寫的。但是意思和這段代碼類似。當時,我很好奇為什么要對一個方法的返回值是不是null進行判斷。現在當然很清楚了:在很多win32的API里面,是通過返回值為null來傳遞“函數調用失敗”這一種信息的。
那么,這么做好嗎?
我翻看了很多的博客,大致上說這種寫法不好的占多數。最重要的理由如下:
- 無法展示更詳細的錯誤信息
- 容易讓調用者忽略
這種說法基本還是有道理的。因為返回值為null,確實很明顯得存在這兩種問題。
對於第一點,其比較關鍵的影響就是,既然不知道更多信息,也就無法在日志中體現。從而不能針對可能出現的多種異常進行不同的日志打印和處理。曾經也發生過因為缺失了日志打印,導致花很多時間來定位一個問題的情況。尤其是線上環境不比本地隨意debug,一個簡單的問題經過幾次這種“信息丟失”之后,就有可能成為一個“疑案”。
而第二點,更是嚴重影響系統安全性、穩定性。人都有不小心的時候,如果忘了對某個接口的返回值進行校驗,就會導致代碼邏輯是按照“這件事已經發生並且正確發生”寫的,但是實際執行過程中發生了錯誤。一方面比較難排查,第二方面是可能導致一個問題在另外的地方爆發出來。
那么是不是絕對不能這么做呢?其實也不盡然。簡單來說,如果調用方在系統內部、代碼執行邏輯可控、在返回null之前有正確的日志打印、調用方對這個異常確實沒有辦法處理。滿足這幾個條件就可以使用返回null。但是還是具有危險性。
這個問題的改進方案是返回一個對象。比如寫一個Result對象。
1 public class Result { 2 3 private Integer code; 4 5 private String message; 6 7 private Object data; 8 9 // getter and setter 10 11 }
這樣就有一個優化:能夠返回錯誤的信息和一些數據。本次方法成功了嗎?如果失敗需要打印什么信息?如果成功,是不是需要一些內容?
但是這種寫法也有問題。
最主要的是其適用性問題。就是說如果很多方法使用同一個Result,能保證都能通過這種方式返回其需要的數據嗎?一些特別的方法返回的東西不能夠使用一個Object進行轉化。(或者說這樣做代價不小)
其次,很多代碼業務邏輯極其簡單,如果強行使用結果類進行封裝,反倒有畫蛇添足之嫌。
最后,如果要嚴格使用這個類,代碼開發的代價很大。
那么這種做法適合什么場景呢?一句話:對外接口。一方面我們要按照一個統一的規則去返回信息(正確或者錯誤)。第二方面,很難要求調用方去捕獲我們的異常。如果他們忘了捕獲,是不是就有可能會造成更嚴重的情況?所以,這種情況下,我們需要告知調用方失敗信息。但是又不能要求對方做出多少改變。
最后一種解決方案是拋異常。
Effective Java里面用一句話解釋什么時候需要拋異常,什么時候不需要。“當你對異常什么也做不了的時候,就不要拋異常”(大意,有可能記錯了)。
這句話是很有道理的。因為如果你顯式地拋出異常,那么調用方就需要去捕獲。如果要求對方捕獲,可是卻對這種異常什么也做不了。那么拋異常是不是就不那么有意義了?
這個地方的關注焦點在於,調用方如何處理這個異常?假如說,調用方只是打印日志,算不算這個異常有用?在最開始的時候,我曾經認為這種情況可以認定為這個異常對調用方沒有幫助。而現在我的想法產生了變化。簡單來說和具體業務有關。
再進一步來說:如果調用方在得到這個錯誤信息之后,代碼邏輯會改變,就適合拋異常。而如果調用方即使得知了錯誤信息,也不需要改變代碼運行邏輯,就可以更簡化得處理。比如這個接口是刪除數據。那么在失敗的情況下,就必須通過拋異常或者別的方式,告知調用方數據刪除失敗。否則調用方的代碼可能會忽視數據刪除失敗。這里就不適合返回null,因為返回null的檢查不是強制性的,調用方如果沒有進行判斷,就可能導致后面的邏輯完全錯誤。
第二種情況就在於對一些緩存的查詢。如果失敗,或者沒查到,拋異常就不再是一個必須的選項了。當然,如果拋異常,能讓調用方更清晰地了解為啥沒查到緩存,是網絡問題,還是參數不正確,或者確實沒有這個緩存。這里的重點在於,調用方即使知道了這些信息,恐怕也無法做更多的事,即使知道了是網絡問題,也很難通過代碼解決。而對於打印日志,這種“沒查到緩存”的情況是否有必須在調用端強調其原因,就看具體的業務了。不過我個人覺得,這里的日志打印交給服務提供方比較好,其一是提供方肯定比調用方知道的信息更全。其二是,提供方本來就有責任去維護系統的穩定性。所以對於日志,能做更多。
除了這些理由之外,還有一個點。就是在項目結構的關鍵層級,是否需要補貨所有異常。比如controller層,如果拋出異常,就會直接給用戶造成嚴重影響。當然如果每個函數都簡單地try catch住所有異常,則也是不負責任的,這樣不利於排查問題。
總結起來,是否拋異常,還是通過什么方式返回錯誤信息。主要取決於:
- 調用方拿到錯誤信息能做什么
- 業務上調用方是否需要獲取具體的錯誤信息
- 是否有必要捕獲所有異常