Domain Service Owner Transaction Design Pattern 這一個詞是由這本書: Java Transaction Design Strategies 所命名的。這個 pattern 我們 team 已經用了一段時間,我歸納了一些有關 exception 處理的須知和小技巧,大家可以參考參考。
目前我們 team 的 spring transaction 設計是採用 txProxyTemplate:
<bean id="txProxyTemplate" abstract="true" class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean"> <property name="transactionManager"> <ref local="transactionManager" /> </property> <property name="transactionAttributes"> <props> <prop key="save*">PROPAGATION_REQUIRED, -org.bioinfo.util.BusinessException</prop> <prop key="update*">PROPAGATION_REQUIRED, -org.bioinfo.util.BusinessException</prop> <prop key="delete*">PROPAGATION_REQUIRED, -org.bioinfo.util.BusinessException</prop> <prop key="remove*">PROPAGATION_REQUIRED, -org.bioinfo.util.BusinessException</prop> <prop key="tx*">PROPAGATION_REQUIRED, -org.bioinfo.util.BusinessException</prop> <prop key="*">PROPAGATION_REQUIRED,readOnly, -org.bioinfo.util.BusinessException</prop> </props> </property> </bean>
上面寫著只要是 save*,update*,delete*,remove*, 和 tx* 等開頭的 method 時,就會開啟 read-write transaction,其他的 method 則是開啟 read-only transacton。按這個 convention,我們的 XXXService 就不用一個一個 method 去調整 transaction 的設定。
Exception 的 rollback
上面的設定裡,都加了一行 -org.bioinfo.util.BusinessException。這個表示當你的 XXXService 的 method 丟出 BusinessException (或它的 subclass) 時,transaction 會作 rollback。以下面的例子為例:
public class ProjectException extends BusinessException { //.... skip } public class ProjectServiceImpl implements ProjectService { public void txCreateProject() throws ProjectException { //.... do something... } }
按我們的設定,當 projectService.txCreateProject() 丟出 ProjectExeception 時,則 spring 會將 transaction rollback。除此之外, 當 txCreateProject() 丟出 RuntimeException 也會 rollback。這是 spring 內定的 rule,所以我們不用特別去寫。
上面這一套 convention 是我們 team 現在採用的作法,簡單易寫而且不易出錯。不過這個作法目前有幾個限制要額外注意。
除了 BusinessException 之外,其他 CheckedException 不會作 rollback
//錯誤示範! public class ProjectServiceImpl implements ProjectService { public void txAttachFiles() throws IOException { //.... do something... } }
上面的例子裡,設計這個 txAttachFiles() method 的人,將 IOException 丟出來,讓外面來處理。這時問題就大了,IOException 不是 BusinessException 的 class,如果寫檔案失敗,那 transaction 並不會做 rollback!這個 method 有可能會將不全的資料寫入資料庫並且 commit。
要改進這個問題,我們該寫成這樣:
public class ProjectServiceImpl implements ProjectService { public void txAttachFiles() throws ProjectException { try { //.... do something... } catch (IOException e) { //轉為 BusinessException 或是 RuntimeException throw new ProjectException(e); } } }
上面我們將 IOException 轉為 ProjectException 出去,這樣就會 rollback 了。如果這個 exception 用戶沒辦法處理時,我們也可以轉為 RuntimeException。
那麼,是否有一個特殊的狀況是--"我希望丟出 exception,但不要 rollback" 呢 ? 我相信這個狀況是有的,但是應該很少。因為即然丟出了 exception,不就是代表 裡面出了問題而無法進行下去 嗎?出了 問題 卻還想 commit,這樣的思維是矛盾的。如果你在設計上出現了 "丟出 exception 不想 rollback",那很有可能你 錯把 exception 當做 flow control (即當做 if) 來用了。exception 就是例外狀況,是不可預期的,而不是程式流程中一個已知的分支。
當 service 互相呼叫時,其中一個丟出 RuntimeException 或是 BusinessException,則整個 request 會強制 rollback,無法回復。
public class ProjectServiceImpl implements ProjectService { private UserService userService ; public void txCreateMembers() throws ProjectException { try { //..... User newMember = userService.txCreate() ; //..... } catch (UserCreateException e) { //這個會讓整個 transaction 變成 rollback //catch 後這裡我們可以做..... ? } projectDAO.save(project); } }
上面這個例子 ProjectService 引用 userService 來建立 member,但建立 member 時有可能會丟 UserCreateException (是一個 BusinessException)。依我們的 txProxyTemplate 的設定,userService.txCreate() 丟出 BusinessException 後,整個 transaction 就 rollback 了。換句話說,即使 catch 了 UserCreateException,想做一些回復的動作是徒勞無功的,資料還是不會正常的 commit() (後面那一行 projectDAO.save(project) 無效了)。這是我們現在 txProxyTemplate 設計上的限制,請大家注意使用。
在這個限制下,我們無法回復裡面 service 丟出 BuinessException 所造成的 rollback。因此當我們 catch 到 exception 時,處理的方式只剩下再丟出 exception:
public void txCreateMembers() throws ProjectException { try { //..... User newMember = userService.txCreate() ; } catch (UserCreateException e) { //強制 rollback /* 這裡我們不用費神做回復了,因為回復不了... 能做的只剩下再丟出 exception */ //做法(1) 丟出原來的,也就是乾脆不 catch 了。 throw e ; //做法(2) 轉為這個 Service 用的 BusinessException throw new ProjectException(e) ; //做法(3) 轉為 RuntimeException throw new RuntimeException(e) ; } }
做法(1) 是直接丟出原來的 UserCreateException,這個做法最簡單,但是這會污染 ProjectService 的 API,因為 ProjectService 的 API 開始和 User的 API 相關聯了,失去封裝的效果。做法 (2), (3) 是比較建議的作法,選哪一種就看上一層該不該處理 UserCreateException 了。
雖然我們現在 txProxyTemplate 有這個限制,但這個限制還算有道理的 -- 因為當 method 選擇丟出 exception 時,表示內部的狀態已經損毀,本來就不期望你能回復什麼,而是希望你能全部重來。因此大部份的應用應該沒什麼問題。如果真的遇到少數的特例,那就不要用 txProxyTemplate 囉。
總結 txProxyTemplate 的注意事項
- 不要在 service method 上 throw 非 BusinessException 的 CheckedException。除非你有充份的理由。
- 當 service 內部呼叫其他 service method 時,如果內部的 service throw 了 BusinessException,你能做的只剩再丟出(或轉丟出) exception,無法做回復。