Synchronized用於線程間的數據共享,而ThreadLocal則用於線程間的數據隔離。
在我完成一個項目的時候,遇到了一個Spring事務不回滾的問題,通過aspectJ和@Transactional注解都無法完成對於事務的回滾,經過查看博客和文檔
- 默認回滾RuntimeException
- Service內部方法調用
- 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分為兩種。
- checked Exception:Exception類本身,以及Exception的子類中除了"運行時異常"之外的其它子類都屬於被檢查異常。
- unchecked Exception: RuntimeException和Error都屬於未檢查異常。

/**
* 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";
}
}
- 通過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] - 直接調用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不會產生父子容器。
還有可能是其它配置文件出錯,例如,連接池配置為多例
參考文檔
