2009-08-26 94 views
2

過去7年來我一直在做面向對象編程,在此期間使用Java打開和關閉。我確信一些事情我有很好的把握,比如最有用的設計模式。事實上,下面的代碼使我能夠在一天之內完成一個小系統,這將處理我們現在準備實施的一個特定實例,同時具有足夠的靈活性來處理我所瞭解的未來需求:批判我的異常處理策略

public void importAndArchive(File detectedFile) throws FileNotFoundException, IOException { 
     File workingCopy = returnWorkingCopy(detectedFile); 
     List<String[]> csvData = csvData(workingCopy); 
     setHeaderFields(csvData.get(0)); 

     importData(csvData); //subclass will implement this abstract method 

     archiveWorkingCopy(workingCopy); 
    } 

我沒有表現上面吹噓我的模板方法的把握,而是作爲一個出發點,討論我的能力是在我的OO設計能力的光刺目的空隙我有。而這種差距是一種系統的異常處理方法。事實上,你可以在方法簽名中看到我暫時重新拋出一些異常,但實際上我已經在應用程序的另一個角落中根除了同樣的東西。

在我進一步探討之前,由於這是我第一次特別系統化的嘗試,所以我想要驗證一下迄今爲止我所做的一切。我們的應用程序在一堆文件上循環,「處理」它們,並「歸檔」它們。相當標準的票價。我爲了儘快推出原型而做出的一個決定如下。應用程序根據(ResourceBundled)屬性文件中的數據進行初始化。 ResourceBundle API中的各種方法拋出未經檢查的異常,但我暫時沒有真正處理它們,理由是它們將阻止應用程序啓動,並且堆棧跟蹤暫時足夠了。

但是我選擇了一個CSV處理庫,它引發了檢查異常。 NetBeans的那麼這很容易增殖的例外開始;)下面是我,因爲返工實際處理這些異常的方法:在一個循環內

private String detectImportHandler(File detectedFile) throws Exception { 
    String[] firstLine = null; 

    try { 
     /* 
     * CSVReader throws checked exceptions 
     */ 
     CSVReader csvReader = new CSVReader(new FileReader(detectedFile)); 
     firstLine = csvReader.readNext(); 
     csvReader.close(); 
    } 
    catch(Exception x) { 
     throw new Exception("CSVReader unable to process file: " + x.getMessage()); 
    } 

    try { 
     return firstLine[1]; 
    } 
    catch(Exception x) { 
     /* 
     * since we're re-throwing CSVReader's checked exceptions it seems easiest 
     * to also re-throw null-pointer and/or array-out-of-bounds errors 
     */ 
     throw new Exception(
       "First line null or did not have importHandlerType field: " + x.getMessage()); 
    } 
} 

上述方法被調用正是如此,處理該文件:

try { 
     importHandlerType = detectImportHandler(detectedFile); 
    } 
    catch(Exception x) { 
     unusableFileErrors.put(detectedFile.getName(), x.getMessage()); 
     continue; 
    } 

unusableFileErrors是一張地圖,我想,當我做遍歷文件,然後我可以用這個地圖,它包含在更高層次來處理事物的特定文件的消息,如日誌記錄,將文件移動到系統上的其他位置等。

無論如何,我已經走了很久。我已經訂購了這本書"Robust Java",我希望在它和SO社區之間,我可以改進這個被忽視的方面。我已經看到了其他類似的問題,但我認爲,在實際代碼的背景下請求具體建議可能會有好處。

+0

嗯,你應該在'finally'塊中關閉你的資源。 7年的Java,並沒有最終阻止嘖嘖!嘖嘖! :)恥辱Java沒有C#的'使用'塊 – pjp 2009-08-26 16:32:56

+0

只是對您的第一個方法的評論 - 它做了很多(文件加載,轉換爲CSV,保存CSV)看起來像一種實用方法 - 我認爲*單責任原則*可以應用於方法和類。 – 2009-08-26 16:33:57

+0

Nick,這不是一種實用方法,而是處理每個符合條件的文件的本質。在某種程度上,你必須有一種方法來封裝算法中看似不相關的步驟。 – 2009-08-26 16:54:13

回答

15

一個幾句話:

  1. 當包裝異常E,你只需要使用e.getMessage()。相反,我建議你使用use new Exception(message,e)。這將設置異常的原因,因此整個堆棧跟蹤將包含原始異常的堆棧跟蹤。這對調試異常的原因非常有用。

  2. 而不是拋出異常,我建議你拋出更明確的例外,例如FileNotFoundException異常。有用的時候使用由Java API定義的異常。

  3. 而不是捕捉異常,我建議你明確指出要捕捉哪些異常。

  4. (可選替換爲2)某些開發人員更喜歡 - 而不是爲每種異常定義異常類 - 拋出包含指示異常類型的錯誤鍵的常規異常。新的DetailedException(GeneralBusinessErrors.PARSING_ERROR)。這樣可以更輕鬆地爲您的業務錯誤分配語言相關資源文件。

  5. 將調試數據添加到您的異常可能很有用。例如,如果找不到文件,則可能是諸如您嘗試打開哪個文件等信息。這在維護中非常有用,您可能無法重現問題和/或使用調試器進行調試。

  6. 讓您的系統記錄任何未捕獲的未經檢查的未檢查異常可能很有用。一般來說,記錄任何引發(捕獲或不捕獲)異常可能是有用的。

1

一般來說,我處理異常中的兩個地方之一:

  1. 如果我能處理異常,而無需知道用戶,我處理它儘可能靠近錯誤越好。
  2. 如果錯誤需要某種輸出(記錄,彈出一個對話框,將消息寫入標準輸出),我將它傳遞給某個集中點,然後將其捕獲並分發到相應的輸出機制。

這似乎對我來說足夠好。

2

小記:我建議鏈接的實際原因,而不是僅僅追加原始消息;否則你會失去堆棧跟蹤,調試將比它需要更痛苦。

除此之外:我在detectImportHandler()裏面的try/catch塊中看不到很多值。由於該方法已經拋出了Exception,因此不需要重新包裝CSVReader所拋出的任何內容,並且NullPointerException將會像調用try/catch一樣被捕獲。

什麼會更有價值,潛在的,將是一個finally條款,關閉FileReader(和/或CSVReader),如有異常可以通過readNext()或由CSVReader構造函數拋出。

+0

+1,用於發現缺失的'finally' – pjp 2009-08-26 16:38:04

+0

是的,這絕對是我能夠在篩選所有這些優秀信息並制定我的策略時立即執行的一件事 – 2009-08-27 14:06:47

2

捕獲和投擲普通的舊Exception是壞的 - 你的所有運行例外,無論你想與否(可能不是)。

我嘗試拋出檢查的異常,這些異常在它們來自的方法的上下文中是有意義的。例如,如果我有一個loadCustomer(long id)我會拋出ObjectLoadException而不是SQLException

是的你沒有看錯,我的選擇是經過檢查的異常 - 我喜歡的是一個方法的消費者必須明確決定何時發生異常時做什麼,它增加了閱讀時的可讀性通過想象調用堆棧代碼imho。

6

通常我會聘請追趕重新拋出的方法,如果我想實現特定的例外翻譯成我想通過我的API暴露例外。

例如,假設您示例中的導入方法採用URL而不是File。如果這個URL恰巧指的是File,我不想在API上暴露FileNotFoundException(這是一個實現細節),而是捕獲並重新拋出該錯誤(例如作爲ImportException),但仍然鏈接到基礎FileNotFoundException原因(如建議由大衛)。

此外,在某些情況下(例如Spring的DataAccessException),我通常不會捕獲並重新拋出未經檢查的異常(如NullPointerException)。

3

我已經給了@Kolibri一張加票,所以請先閱讀他的第一張,但我有幾件事要補充。

異常處理是一種黑色藝術,預期的實踐從語言變爲語言。在Java中,將方法簽名視爲您的方法和調用方之間的契約。

如果調用者違約(例如,將「detectedFile」傳遞爲null),您希望引發未經檢查的異常。你的API給了他們合同,他們已經明確地破壞了它。他們的問題。 (忽略FileNotFoundException現在被檢查,這是我用Java的牛肉)

如果調用者傳遞正確的數據並且你不能完成他們的請求,但是,這是當你拋出一個檢查的異常。例如,如果由於磁盤錯誤(例如IOException)而無法讀取它們的文件,那就是違反了合同的結束,並且拋出檢查的異常是合理的。檢查異常會在調用者調用你的方法時警告所有可能發生錯誤的事情。

爲此,它取決於您想讓用戶處理您的異常的哪種粒度。拋出「例外」通常是不受歡迎的,因爲像其他人所說的那樣,它不會讓你處理髮生的事情。調用者不會立即知道他們是否違反了合同或您的方法,因爲Exception包含checked和unchecked異常。另外,也許你的調用者想要在他們的代碼中處理FileNotFoundException與IOException的不同。如果你只是拋出異常,他們會失去輕鬆做到這一點的能力。定義您自己的異常可以擴展這個概念,並允許您的方法的調用者以更細微的方式處理來自您的方法的異常。這裏的關鍵是,如果你拋出10個不同的異常,並且調用者不關心差異,他們可以做「catch(Exception e)」並且一次性捕獲它們。如果你只是「拋出異常」,用戶不再有選擇是否以不同的方式處理不同的異常或者只是捕獲它們。

我也非常贊同關於使用「throw new XXXException(message,e)」而不是「throw new Exception(message + e.getMessage())」的評論。如果使用第一種方法,則會將內部堆棧跟蹤一直保留回原始錯誤。如果使用第二種方法,那麼堆棧跟蹤現在只會追溯到「拋出新的異常」行,並且您將被迫進行更多調試以確定導致該異常發生的原因。這在過去讓我感到困擾。

最後,不要害怕在捕獲中使用Exception.printStackTrace(),如果它會幫助你的話。用戶討厭看到堆棧跟蹤,你應該儘可能地處理異常,但如果這是一個真正意想不到的異常,並且堆棧跟蹤將在未來幫助你極大地調試,那麼只需打印堆棧跟蹤。

哇,那就像一篇文章。我現在就停下來。祝你好運!:-D

=====================

附錄:我剛好趕上@ PJP的評論和值得重複......你一定要在「catch」語句後,在「finally」塊中調用「.close()」方法。否則,在異常情況下,您的方法將返回而不立即關閉您分配的&已打開的資源。

+0

一篇短文問題值得一篇短文答案;) – 2009-08-26 16:56:58

3

我認爲還有一個尚未被覆蓋的問題。那就是異常處理不只是關於編碼技術,而是關於錯誤情況以及你想要做什麼。 Java的檢查異常是一個混雜的包,因爲它迫使你在你不想要的時候考慮錯誤條件,導致一些開發者做的事情比在他們想要讓編譯器脫離他們的方式時無所事事。 (未經檢查的異常有相反的問題 - 它會導致開發人員忘記處理錯誤,即使是非常常見的情況,例如丟棄的網絡連接)。

有說是三類例外,對應於三種類型的Java:檢查異常,運行時異常和錯誤。

錯誤應該只在應用非常高的水平進行處理,一般應視爲不可恢復的。儘管OutOFMemoryError在實踐中有些可恢復性,但在你不認爲的代碼序列中出現了太多的錯誤(例如對象初始化),這些代碼可能會使事情處於不良狀態,只有當你真的可以隔離代碼(例如在應用程序服務器中)。

運行時例外,在手短,應該是事情是確定的,總是會出問題給出相同的輸入。例如,如果您將空引用傳遞給不允許空值的方法(NullPointerException),或者如果您執行Integer.parseInt,則給定的String的結果將每次都是相同的。

在很短手的定義檢查異常,再次,有些事情你無法預先知道他們是否會出問題與否。例如FileNotFoundException,當你創建File對象時,文件可能在那裏,你甚至可以檢查,但是幾毫秒後,有些東西可能會從你的下面刪除它。

如果您設計並對這些類別的異常做出反應(意識到並非所有的API設計人員都會遵循它們 - 例如,即使對於給定的XML輸入行爲是確定性的,也檢查XML異常,當然Spring決定爲了完全避免檢查異常),那麼你將有明確的方法來思考你需要什麼類型的錯誤處理。當一個網絡連接斷開時,你需要做的事情與當你的JVM耗盡內存時所做的工作非常不同(當然,這取決於你的項目),並且這兩者與如果你必須解析整數,但不要從用戶輸入中獲得一個。

一旦你在這些方面想,你做一下異常處理什麼會更自然地從你正在努力實現的是什麼類型的錯誤處理和魯棒性流動。

4

已經有一些非常好的答案,但我想提供一個額外的想法。

不僅適用於所有情況的單一錯誤處理策略。

在一般情況下,你應該有意識地使兩個根本相反的情況下的選擇。根據您正在構建的內容以及您當前正在處理的應用程序中的哪個層,選擇會有所不同。這兩種選擇是:

  1. 快速失敗。立即出現一個錯誤,你收集所有的信息,並確保它傳達給可以做些事情的人 - 無論是在你的調用代碼中,通過拋出一個Exception,或者打電話給服務檯,誰可以在任何時候修改代碼數據損壞發生。掩蓋錯誤,或者未能獲取足夠的信息,將會使得找出錯誤的原因變得更加困難。

  2. 防彈。不管如何,因爲停下來會是一場災難。當然,你仍然需要捕獲有關失敗的信息並以某種方式報告,但在這種情況下,你需要弄清楚你將如何恢復和繼續。

引入了很多錯誤,因爲程序員在採用第一種策略時採用了第二種策略。如果不清楚你應該如何從這種情況中恢復過來,那麼你應該拋出一個異常,並讓任何打電話給你的人做出明智的選擇 - 他們可能比你更清楚該做什麼,但他們需要有足夠的能力信息來決定這是什麼。

另一個有用的建議是確保拋出的異常在API級別上有意義。如果您的API旨在以html格式呈現數據,您不希望僅僅因爲有人決定啓用審計跟蹤而看到拋出的數據庫異常。像這樣的例子通常最好使用鏈式例外來處理 - 例如throw new ApiException(causeException);

最後,我想給我一些關於未經檢查和檢查的異常。我喜歡爲那些意味着程序員犯了錯誤的情況保留未經檢查的異常。例如,將null傳遞給需要對象引用的方法。如果程序員是完美的(即FileNotFound,如果程序員檢查它的存在之後該文件已被刪除),則可能會出現錯誤,但檢查的異常通常是合適的。從本質上講,創建API的程序員說「即使所有的輸入都是正確的,那麼這個問題仍然會出現,你可能需要處理它」。