以下這些 practice 是在 struts+hibernate+spring 環境下琢磨出來的:
- 將所有的 Business Checked Exception 皆繼承單一的 BusinessException。有一個 root exception 對 AOP (Spring) 來說很方便。比方說在 Spring 裡,Transaction rollback 預設的 rule 可以設為
<property name="transactionAttributes"> <props> <prop key="save*">PROPAGATION_REQUIRED, -com.myapp.BusinessException</prop> ....
如此只要有 method throw BusinessException 的 subclass 即可 rollback,而不用每個 service method 都特別去設定某某 check exception 要 rollback 了。 - 請勿拿 exception 當做流程控制... 例如 A 段的 code 執行到一半出現 exception 後,就改做 B 段的 code... 這是錯誤的使用法。另外,如果你曾仔細觀察過,就會發現 exception 的速度是很慢的,拿exception 當 if 來用,真的會很慢...慢......
- 當發生錯誤時,使用者如果無法處理,請將該錯誤設計成 RuntimeException。還有即然無法回復,就不該再去執行其他的程式碼,而是應該讓程式全部中斷 (頂多只執行 finally的 block) 然後丟到最外層。通常 RuntimeException 都是當作 constraint/assertion 來用 (即出現時表示程式有 bug),或是不可抗力因素,如外部環境出錯、連不上 database 等等。
- 如果發生了使用者可回復的錯誤,就使用 BusinessException (checked exception) 的 subclass。然後傳給使用者,讓使用者來判斷在哪出錯,下一步該做什麼。以程式來看,就是 BusinessException 要一路 throw 到 Struts Action 那一層,再轉換為 message 讓使用者看。
//下面例子的 AException 為一 BusinessException subclass //錯誤:錯把 exception 當流程控制 public void wrong() { try { logicA.execute() ; } catch (AException e) { //錯! logicB.execute() ; //... } } //正確 1:check exception 應 throw 出去給 Action 轉存為 message //讓使用者判斷 public void right1() throw AException { logicA.execute() ; } //正確 2:如果真的遇到使用者無法處理的 check exception,那應當事先核對 //logicA 是否合理,如果是則繼續執行。若核對過了還出現 AExcetion //則改丟出 RuntimeException (表示程式中有某個 bug,這真的算是 "例外" 了) public void right2() { if(isLogicA_valid()) { //事先判斷 logicA 可否執行 try { logicA.execute() ; } catch (AException e) { throw new UnexpectedRuntimeException(e) ; } } else { logicB.execute() ; } }
- 請勿在 service layer 和 domain layer 中直接 catch 或 throw 最大的 Exception 或是 BusinessException:
try { //..... } catch (Exception e) { //錯誤範例:catch 最大的 exception } try { //..... } catch (BusinessException e) { //錯誤範例:catch 最上層的 exception } public void createSomething throws Exception { //錯誤範例:throw 最大的 exception } public void createSomething throws BusinessException { //錯誤範例:throw 最上層的 exception }
catch/throws 最大的 exception 多半只會出現在 AOP/interceptor/proxy 之類的地方,business logic 裡 catch/throws exception 範圍越小越好,這樣才會清楚發生了什麼事情,而不會不心就 catch 到不該 catch 的 exception。 - Struts Action 的 execute method 的 signature:
public ActionForward execute(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws Exception { // 這個 Exception 害人不淺啊!! }
如果用 IDE 產生這個 method,一不小心就會掛上這個最大的 Exception。哇咧!Service Facade 所 throw 的 business exception 頓時消失於無形,IDE/java compiler 不會告訴你沒 catch... 等到上線後才發覺就糗大啦。請務必拿掉這個 Exception,改成較小的 exception (最好完全不寫)。 - Exception 不需多,各模組大概只需要一兩個即可,例如 AccountException, ATMException, ReceiptException... 如果有需要特別的 exception ,則繼承該模組最上層的 Exception。 例如 AccountException 是 account 模組的最大 exception,現有一特別的 exception 為 AccountNameAlreadyExistException 這個 AccountNameAlreadyExistException 就應該繼承 AccountException。
- 我們 team 裡面用了一個特別的 excepton -- ResourseKeyBusinessException (繼承 BusinessException ) 這個 exception 的 message 便是 resource bundle 的 key:
//AccountException 為 ResourceKeyBusinessException的 subclass public class AccountServiceImpl implement AccountService { public void createAccount(String name) throws AccountException { if(isNameAlreadyExist(name)) { // key 和 args[],定義在 MessageResource.properties throw new AccountException("account.name.duplicate", name) ; } //... do other logic... } } 在 Action 裡我們可以這樣寫: public ActionForward execute(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) { String name = request.getParameter("name") ; try { getAccountService().createAccount(name) ; // forward to success.... } catch (AccountException e) { StrutsUtils.saveError(request, e) ;//自動轉換為 ActionMessage //do some resource cleanup return mapping.getInputForward() ; } }
StrutsUtils 是另外自己寫的小工具,它將 ResourceKeyBusinessException 中的 key 和 args 直接轉換為 ActionMessage,然後再呼叫 Action 的 saveErrors(...)。採用 ResourceKeyBusinessException 之後,就不必替各種小狀況單獨做一個 Exception 的 class 了 (比如說上面提到的 AccountNameAlreadyExistException ),只要一個 root exception:AccountException 即可。 也許會有人覺得這種做法將 struts 的東西帶到 business 層了,其實並不會... 試想如果你想將 exception 的訊息做成 i18n 的話,方法大概也是這樣做。況且 ResourceKeyBusinessException 並沒有 import 任何 struts 的 class 啊!