16 June 2006

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,無法做回復。