天天看點

Spring事務不復原原因分析

Synchronized用于線程間的資料共享,而ThreadLocal則用于線程間的資料隔離。

在我完成一個項目的時候,遇到了一個Spring事務不復原的問題,通過aspectJ和@Transactional注解都無法完成對于事務的復原,經過檢視部落格和文檔

  1. 預設復原RuntimeException
  2. Service内部方法調用
  3. Spring父子容器覆寫

代碼已經上傳到 https://github.com/morethink/transactional

異常

下面是@Transactional的注釋文檔,下面有個If no rules are relevant to the exception,it will be treated like {@link org.springframework.transaction.interceptor.DefaultTransactionAttribute} (rolling back on runtime exceptions).

預設會使用

RuntimeException

,那為什麼Spring預設復原RuntimeException,因為Java把Exception分為兩種。

  1. checked Exception:Exception類本身,以及Exception的子類中除了"運作時異常"之外的其它子類都屬于被檢查異常。
  2. unchecked Exception: RuntimeException和Error都屬于未檢查異常。
Spring事務不復原原因分析
/**
 * Describes transaction attributes on a method or class.
 *
 * <p>This annotation type is generally directly comparable to Spring's
 * {@link org.springframework.transaction.interceptor.RuleBasedTransactionAttribute}
 * class, and in fact {@link AnnotationTransactionAttributeSource} will directly
 * convert the data to the latter class, so that Spring's transaction support code
 * does not have to know about annotations. If no rules are relevant to the exception,
 * it will be treated like
 * {@link org.springframework.transaction.interceptor.DefaultTransactionAttribute}
 * (rolling back on runtime exceptions).
 *
 * <p>For specific information about the semantics of this annotation's attributes,
 * consult the {@link org.springframework.transaction.TransactionDefinition} and
 * {@link org.springframework.transaction.interceptor.TransactionAttribute} javadocs.
 *
 * @author Colin Sampaleanu
 * @author Juergen Hoeller
 * @author Sam Brannen
 * @since 1.2
 * @see org.springframework.transaction.interceptor.TransactionAttribute
 * @see org.springframework.transaction.interceptor.DefaultTransactionAttribute
 * @see org.springframework.transaction.interceptor.RuleBasedTransactionAttribute
 */
           

是以,如果發生的不是RuntimeException,而你有沒有配置rollback for ,那麼,異常就不會復原。

service 内部方法調用

就是一個沒有開啟事務控制的方法調用一個開啟了事務控制方法,不會事務復原。

AccountService類

@Service
public class AccountService {

    @Autowired
    private AccountDao accountDao;

    /**
     * 完成轉錢業務,transfer方法開啟事務
     *
     * @param out
     * @param in
     * @param money
     */
    @Transactional(isolation = Isolation.DEFAULT, propagation = Propagation.REQUIRED)
    public void transfer(String out, String in, double money) {
        Account account = new Account();
        account.setName(out);
        account.setMoney(money);
        accountDao.out(account);
        int i = 1 / 0;
        account.setName(in);
        accountDao.in(account);
    }

    /**
     * 完成轉錢業務,transferProxy方法沒有開啟事務
     *
     * @param out
     * @param in
     * @param money
     */
    public void transferProxy(String out, String in, double money) {
        System.out.println("調用transfer方法 開始");
        transfer(out, in, money);
        System.out.println("調用transfer方法 結束");
    }
}
           

AccountAction類

@RestController
public class AccountAction {

    @Autowired
    private AccountService accountService;

    @RequestMapping("/transfer")
    public String transfer(String out, String in, double money) {
        accountService.transfer(out, in, money);
        return "transfer";
    }

    @RequestMapping("/transferProxy")
    public String transferProxy(String out, String in, double money) {
        accountService.transferProxy(out, in, money);
        return "transfer";
    }
}
           
  1. 通過transferProxy方法調用transfer方法時
    Logging initialized using 'class org.apache.ibatis.logging.stdout.StdOutImpl' adapter.
    調用transfer方法 開始
    Creating a new SqlSession
    SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@a0dbf4] was not registered for synchronization because synchronization is not active
    JDBC Connection [com.mchange.v2.c3p0.impl.NewProxyConnection@6386ed [wrapping: com.mysql.jdbc.JDBC4Connection@9f2009]] will not be managed by Spring
    ==>  Preparing: update account set money = money - ? where name = ?
    ==> Parameters: 100.0(Double), aaa(String)
    <==    Updates: 1
    Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@a0dbf4]
               
    發現沒有開啟事務
  2. 直接調用transfer方法時
    Logging initialized using 'class org.apache.ibatis.logging.stdout.StdOutImpl' adapter.
    Creating a new SqlSession
    Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@238be2]
    JDBC Connection [com.mchange.v2.c3p0.impl.NewProxyConnection@a502e0 [wrapping: com.mysql.jdbc.JDBC4Connection@3dbe42]] will be managed by Spring
    ==>  Preparing: update account set money = money - ? where name = ?
    ==> Parameters: 100.0(Double), aaa(String)
    <==    Updates: 1
    Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@238be2]
    Transaction synchronization deregistering SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@238be2]
    Transaction synchronization closing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@238be2]
               

我們都知道Spring事務管理是通過AOP代理實作的,可是那麼什麼條件會使得AOP代理開啟?通過檢視

Sprin官方文檔,發現隻有把整個Service設為事務控制時,才會進行AOP代理。如果我們通過一個沒有事務的transferProxy方法去調用有事務的transfer方法,是通過this引用進行調用,沒有開啟事務,即使發生了RuntimeException也不會復原。

Spring事務不復原原因分析

然後

Spring容器優先加載由ServletContextListener(對應applicationContext.xml)産生的父容器,而SpringMVC(對應mvc_dispatcher_servlet.xml)産生的是子容器。子容器Controller進行掃描裝配時裝配的@Service注解的執行個體是沒有經過事務加強處理,即沒有事務處理能力的Service,而父容器進行初始化的Service是保證事務的增強處理能力的。如果不在子容器中将Service exclude掉,此時得到的将是原樣的無事務處理能力的Service,因為在多上下文的情況下,如果同一個bean被定義兩次,後面一個優先。

當我們在applicationContext.xml,spring-mvc.xml都配置如下掃描包時,spring-mvc.xml中的service就會覆寫applicationContext.xml中的service。

context:component-scan base-package="net.morethink"/>
           

注意:當我們使用JUnit測試的時候,不會出現這種情況。

JUnit配置如下

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {"classpath:applicationContext.xml", "classpath:dispatcher-servlet.xml"})
@WebAppConfiguration
public class AccountActionTest {


    protected MockMvc mockMvc;
    @Autowired
    protected WebApplicationContext wac;

    @Before()  //這個方法在每個方法執行之前都會執行一遍
    public void setup() {
        mockMvc = MockMvcBuilders.webAppContextSetup(wac).build();  //初始化MockMvc對象
    }

    @Test
    public void testTransfer() throws Exception {
        String responseString = mockMvc.perform(
                get("/transfer")    //請求的url,請求的方法是get
                        .contentType(MediaType.APPLICATION_FORM_URLENCODED)  //資料的格式
                        .param("out", "aaa")
                        .param("in", "bbb")
                        .param("money", "100")
        ).andExpect(status().isOk())    //傳回的狀态是200
//                .andDo(print())         //列印出請求和相應的内容
                .andReturn().getResponse().getContentAsString();   //将相應的資料轉換為字元串
        System.out.println(responseString);
    }

    @Test
    public void testTransferProxy() throws Exception {
        String responseString = mockMvc.perform(
                get("/transferProxy")    //請求的url,請求的方法是get
                        .contentType(MediaType.APPLICATION_FORM_URLENCODED)  //資料的格式
                        .param("out", "aaa")
                        .param("in", "bbb")
                        .param("money", "100")
        ).andExpect(status().isOk())    //傳回的狀态是200
//                .andDo(print())         //列印出請求和相應的内容
                .andReturn().getResponse().getContentAsString();   //将相應的資料轉換為字元串
        System.out.println(responseString);
    }
}
           

可能因為JUnit不會産生父子容器。

還有可能是其它配置檔案出錯,例如,連接配接池配置為多例

參考文檔

  1. Spring聲明式事務為何不復原
  2. Spring中@Transactional事務復原(含執行個體詳細講解,附源碼)
  3. 深入研究java.lang.ThreadLocal類
  4. Spring單執行個體、多線程安全、事務解析