事務(wù)簡(jiǎn)介
所謂事務(wù)灶芝,指的是程序中可運(yùn)行的不可分割的最小單位合呐。在生活中事務(wù)也是隨處可見的啥辨。比方說你在Steam上剁手買了一款游戲,那么付款就是一個(gè)事務(wù)拴事,要么付款成功,游戲到手圣蝎;要么付款失敗,錢退回你賬戶牲证。不可能也絕不應(yīng)該出現(xiàn)花了錢游戲卻沒到的情況坦袍。所以捂齐,事務(wù)也應(yīng)該具有兩個(gè)操作:成功時(shí)候提交奠宜,或者失敗時(shí)候回滾压真。
許多框架也提供事務(wù)管理功能榴都。JDBC中嘴高,你可以關(guān)閉自動(dòng)提交,然后使用Connection.commit()
和Connection.rollback()
執(zhí)行提交或回滾春瞬。在Hibernate中宽气,也有類似的功能萄涯。但是涝影,這些框架的事務(wù)管理有一個(gè)問題燃逻,就是它們雖然提供了事務(wù)功能伯襟,但是為了使用這些功能姆怪,你必須在每個(gè)需要事務(wù)的地方添加額外代碼片效,當(dāng)執(zhí)行正常時(shí)提交淀衣,出現(xiàn)異常時(shí)回滾膨桥。這樣一來只嚣,程序中就會(huì)出現(xiàn)大量重復(fù)的事務(wù)管理代碼(有過這種經(jīng)歷的人應(yīng)該能夠感同身受吧)册舞。
另外事務(wù)還分為本地事務(wù)和全局事務(wù)调鲸。JDBC事務(wù)、Hibernate事務(wù)都是本地事務(wù)即供,只關(guān)注特定資源的事務(wù)管理逗嫡。全局事務(wù)則用來控制多個(gè)數(shù)據(jù)庫驱证、消息隊(duì)列等等雷滚。Spring提供了統(tǒng)一的事務(wù)管理來操作全局事務(wù)和本地事務(wù),讓我們的代碼更加簡(jiǎn)潔商源。
Spring事務(wù)管理
Spring事務(wù)的核心接口是org.springframework.transaction.PlatformTransactionManager
牡彻。
public interface PlatformTransactionManager {
TransactionStatus getTransaction(
TransactionDefinition definition) throws TransactionException;
void commit(TransactionStatus status) throws TransactionException;
void rollback(TransactionStatus status) throws TransactionException;
}
提交和回滾方法我們已經(jīng)了解了庄吼。getTransaction方法會(huì)根據(jù)給定的事務(wù)定義总寻,返回一個(gè)事務(wù)狀態(tài)對(duì)象渐行。事務(wù)定義包含了事務(wù)的一些特征:是否是只讀的祟印,超時(shí)設(shè)置蕴忆、事務(wù)的隔離和傳播等套鹅。Spring實(shí)現(xiàn)了幾個(gè)PlatformTransactionManager,用于不同環(huán)境(JDBC沉衣、Hibernate豌习、JPA等等)下的事務(wù)管理肥隆。我們?cè)谑褂玫臅r(shí)候只要設(shè)置好相應(yīng)的PlatformTransactionManager即可栋艳。事務(wù)管理包括在Spring核心包中吸占,所以只要項(xiàng)目中添加了spring-core.jar
矾屯,那么就可以使用Spring的事務(wù)管理功能了件蚕。如果需要和Hibernate等框架的集成排作,那么還需要spring-orm.jar
妄痪。
聲明式事務(wù)管理
Spring支持聲明式和編程式兩種方式來控制事務(wù)管理拌夏。最流行的方式就是使用聲明式障簿。利用聲明式事務(wù)管理站故,我們可以設(shè)置遇到什么異常時(shí)候回滾事務(wù)西篓、在哪些方法上執(zhí)行事務(wù)岂津,而不用修改任何代碼吮成。如果已經(jīng)了解了Spring AOP的話粱甫,應(yīng)該可以猜得到茶宵,Spring聲明式事務(wù)管理需要AOP代理的支持乌庶。下面我們來用一個(gè)例子說明一下瞒大。
配置PlatformTransactionManager
下面我們使用JDBC來演示一下Spring事務(wù)管理糠赦。因此首先需要配置相應(yīng)的PlatformTransactionManager拙泽,在這里是DataSourceTransactionManager顾瞻,它需要相應(yīng)的數(shù)據(jù)源來初始化荷荤。
<!--定義事務(wù)管理器-->
<bean id="dataSourceTransactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<!--定義數(shù)據(jù)源-->
<bean id="dataSource" class="org.apache.commons.dbcp2.BasicDataSource"
destroy-method="close">
<property name="url" value="jdbc:mysql://localhost:3306/test"/>
<property name="username" value="root"/>
<property name="password" value="12345678"/>
<property name="driverClassName" value="com.mysql.jdbc.Driver"/>
</bean>
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<constructor-arg ref="dataSource"/>
</bean>
然后我們需要一個(gè)類來進(jìn)行事務(wù)管理蕴纳。首先我們定義一個(gè)接口古毛。
public interface UserService {
void add(User user);
User get(String username);
}
然后來實(shí)現(xiàn)這個(gè)接口嫂冻。在這里我用了Spring JDBC來進(jìn)行數(shù)據(jù)操作桨仿。throwException來模擬拋出異常的情況案狠。
public class JdbcUserService implements UserService {
private JdbcTemplate template;
private boolean throwException;
@Autowired
public void setTemplate(JdbcTemplate template) {
this.template = template;
}
public void setThrowException(boolean throwException) {
this.throwException = throwException;
}
@Override
public void add(User user) {
template.update("INSERT INTO user(name) VALUES(?)", user.getName());
if (throwException) {
throw new RuntimeException("拋出異常");
}
}
@Override
public User get(String username) {
return template.queryForObject("SELECT id,name FROM user WHERE name=?", new UserRowMapper(), username);
}
}
配置事務(wù)管理
然后我們需要配置一個(gè)切入點(diǎn)和通知伴嗡,指定哪些方法應(yīng)用什么樣的事務(wù)管理从铲,這一部分屬于AOP的部分名段,這里不再細(xì)述伸辟。我們需要<tx:advice>
節(jié)點(diǎn)設(shè)置事務(wù)管理信夫,該節(jié)點(diǎn)需要設(shè)置標(biāo)識(shí)符和事務(wù)管理器静稻。<tx:attributes>
節(jié)點(diǎn)中的配置表示振湾,以get開頭的方法是只讀的,其他方法不是只讀的树酪。這有助于Spring正確設(shè)置事務(wù)续语。
<tx:advice id="txAdvice"
transaction-manager="dataSourceTransactionManager">
<tx:attributes>
<tx:method name="get*" read-only="true"/>
<tx:method name="*"/>
</tx:attributes>
</tx:advice>
<!--使用AOP設(shè)置事務(wù)管理-->
<aop:config>
<aop:pointcut id="userService"
expression="execution(* yitian..transaction.JdbcUserService.*(..))"/>
<aop:advisor advice-ref="txAdvice" pointcut-ref="userService"/>
</aop:config>
回滾操作
默認(rèn)情況下绵载,Spring會(huì)在方法中拋出運(yùn)行時(shí)錯(cuò)誤時(shí)執(zhí)行回滾焚虱,如果方法中拋出受檢異常則不回滾鹃栽。我們可以向<tx:method/>
節(jié)點(diǎn)添加rollback-for
屬性來指定當(dāng)方法拋出什么異常時(shí)執(zhí)行回滾躯畴,這里的異撑畛可以是某一個(gè)具體的異常饮亏,也可以是一些異常的父類路幸。
<tx:attributes>
<tx:method name="get*" read-only="true"/>
<tx:method name="*" rollback-for="Exception"/>
</tx:attributes>
相應(yīng)的付翁,還有一個(gè)no-rollback-for
屬性來配置在遇到某些異常下不執(zhí)行回滾操作百侧。
<tx:attributes>
<tx:method name="get*" read-only="true"/>
<tx:method name="*" no-rollback-for="IndexOutOfBoundsException"/>
</tx:attributes>
如果遇到了多個(gè)回滾規(guī)則佣渴,以最具體的那一個(gè)為準(zhǔn)观话。所以下面的配置频蛔,當(dāng)遇到InstrumentNotFoundException
時(shí)不會(huì)回滾晦溪,當(dāng)遇到其他異常時(shí)則執(zhí)行回滾挣跋。
<tx:advice id="txAdvice">
<tx:attributes>
<tx:method name="*" rollback-for="Throwable" no-rollback-for="InstrumentNotFoundException"/>
</tx:attributes>
</tx:advice>
事務(wù)傳播級(jí)別
上面簡(jiǎn)單使用了<tx:advice/>
節(jié)點(diǎn)舟肉。下面我們來分析一下這個(gè)節(jié)點(diǎn)的配置路媚。默認(rèn)情況下,<tx:advice/>
的配置如下:
- 傳播設(shè)置為:
REQUIRED
- 隔離級(jí)別是:
DEFAULT
- 事務(wù)是可讀可寫的
- 事務(wù)超時(shí)是底層事務(wù)系統(tǒng)的默認(rèn)超時(shí),如果底層不支持就沒有超時(shí)
- 任何運(yùn)行時(shí)異常會(huì)觸發(fā)回滾裤园,任何受檢異常不觸發(fā)回滾拧揽。
首先來說明一下事務(wù)的傳播强法,這指的是外層事務(wù)和內(nèi)層事務(wù)之間的關(guān)系闰歪。常用的值有三個(gè):PROPAGATION_REQUIRED
蓖墅、PROPAGATION_REQUIRES_NEW
和PROPAGATION_NESTED
教翩。
首先來說說PROPAGATION_REQUIRED
饱亿。當(dāng)外層事務(wù)需要一個(gè)內(nèi)層事務(wù)的時(shí)候彪笼,會(huì)直接使用當(dāng)前的外層事務(wù)配猫。這樣一來多個(gè)方法可能會(huì)共享同一個(gè)事務(wù)泵肄。如果內(nèi)層事務(wù)出現(xiàn)回滾腐巢,那么外層事務(wù)會(huì)也會(huì)回滾。這種情況下內(nèi)層事務(wù)會(huì)拋出一個(gè)UnexpectedRollbackException
異常诵盼,外層調(diào)用者需要捕獲該異常來判斷外層事務(wù)是否已回滾风宁。
PROPAGATION_REQUIRES_NEW
會(huì)為每個(gè)事務(wù)創(chuàng)建完全獨(dú)立的事務(wù)作用域戒财,如果外層事務(wù)需要一個(gè)內(nèi)層事務(wù),內(nèi)層事務(wù)會(huì)先掛起外層事務(wù)幽崩,當(dāng)內(nèi)層事務(wù)執(zhí)行完畢之后會(huì)恢復(fù)外層事務(wù)慌申。這樣一來內(nèi)外層事務(wù)的提交和回滾完全是獨(dú)立的理郑,不會(huì)互相干擾柒爵。
PROPAGATION_NESTED
使用帶有多個(gè)保存點(diǎn)的單個(gè)事務(wù)赚爵。這些保存點(diǎn)會(huì)映射到JDBC的保存點(diǎn)上棉胀。所以只能用于JDBC環(huán)境和DataSourceTransactionManager
中。
事務(wù)的隔離級(jí)別冀膝,除了DEFAULT
之外唁奢,剩下的幾種隔離級(jí)別和JDBC中的隔離級(jí)別一一對(duì)應(yīng)。詳細(xì)情況請(qǐng)查看JDBC的相關(guān)文檔畸写。
測(cè)試事務(wù)
前面都配置完成之后,我們就可以測(cè)試一下Spring的事務(wù)管理功能氓扛。下面使用了一個(gè)測(cè)試類來測(cè)試枯芬。設(shè)置userService.setThrowException(false)
之后论笔,事務(wù)不會(huì)拋出異常狂魔,我們可以看到成功插入了用戶數(shù)據(jù)待错。當(dāng)設(shè)置userService.setThrowException(true)
,事務(wù)會(huì)拋出異常,我們發(fā)現(xiàn)這次沒有插入數(shù)據(jù)。
@ContextConfiguration(locations = {"classpath:transaction.xml"})
@RunWith(SpringRunner.class)
public class TransactionTest {
@Autowired
private JdbcTemplate jdbcTemplate;
@Autowired
private JdbcUserService userService;
@Before
public void before() {
jdbcTemplate.execute("CREATE TABLE user(id INT AUTO_INCREMENT PRIMARY KEY ,name VARCHAR(255))");
}
@After
public void after() {
jdbcTemplate.execute("DROP TABLE IF EXISTS user");
}
@Test
public void testTransaction() {
//事務(wù)成功
userService.setThrowException(false);
User user = new User();
user.setName("yitian");
userService.add(user);
User u = userService.get(user.getName());
assertEquals(user.getName(), u.getName());
//事務(wù)失敗
userService.setThrowException(true);
user.setName("liu6");
u = null;
try {
userService.add(user);
} catch (Exception e) {
System.out.println(e.getMessage());
}
try {
//由于沒有記錄卸例,Spring JDBC會(huì)拋出異常
//所以必須捕獲該異常
u = userService.get(user.getName());
} catch (EmptyResultDataAccessException e) {
System.out.println(e.getMessage());
}
assertNull(u);
}
}
注解配置
前面用的都是XML配置方式呜舒,還可以使用注解配置聲明式事務(wù)管理。這需要在配置文件中添加一行,這一行仍然需要指明使用的事務(wù)管理器啤咽。
<tx:annotation-driven transaction-manager="txManager"/>
如果你全部使用注解配置鳞青,那么在標(biāo)記了@Configuration
的類上在添加@EnableTransactionManagement
即可埃儿。需要注意這兩種方法都會(huì)在當(dāng)前ApplicationContext中尋找@Transactional
Bean威鹿。
注解配置主要使用@Transactional
注解,該注解可以放置到類、接口或者公有方法上。該注解還有一些屬性和XML配置相對(duì)應(yīng)。但是根據(jù)配置的不同,注解可能不會(huì)起作用。下面是Spring官方的兩段話羹奉。
Spring建議你只在具體類上應(yīng)用注解@Transactional注解,而不是注解到接口上。你可以將注解應(yīng)用到接口(或者接口方法)上,但是這只在你知道你在用基于接口的代理時(shí)起作用。實(shí)際上撩匕,Java注解不會(huì)從接口繼承,這意味著如果你使用基于類的代理(proxy-target-class="true")或者基于編織的切面( mode="aspectj")令漂,那么事務(wù)設(shè)置不會(huì)被代理和編織體系識(shí)別收叶,事務(wù)對(duì)象也不會(huì)被包裝到事務(wù)代理中,這毫無疑問是件壞事俏竞。
在代理模式(這是默認(rèn)選項(xiàng))中,只有顯式經(jīng)過代理的方法調(diào)用會(huì)被攔截烦秩。這意味著自我調(diào)用,也就是目標(biāo)對(duì)象中的一個(gè)方法調(diào)用該目標(biāo)對(duì)象的另一個(gè)方法岭皂,不會(huì)在運(yùn)行時(shí)觸發(fā)事務(wù),即使該方法標(biāo)記了@Transactional。同樣的片部,代理必須完全初始化來提供期望的行為曾雕,所以你不應(yīng)該在初始化代碼中依賴這樣的功能(例如@PostConstruct)顾犹。
如果需要使用多個(gè)事務(wù)管理器噩咪,可以像下面這樣。
public class TransactionalService {
@Transactional("order")
public void setSomething(String name) { ... }
@Transactional("account")
public void doSomething() { ... }
}
配置文件中竖般,事務(wù)管理器需要使用<qualifier>
節(jié)點(diǎn)指定不同的名稱疗韵。
<tx:annotation-driven/>
<bean id="transactionManager1" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
...
<qualifier value="order"/>
</bean>
<bean id="transactionManager2" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
...
<qualifier value="account"/>
</bean>
如果要應(yīng)用的注解有很多地方重復(fù)革砸,可以將它們定義為一個(gè)自定義注解仗岸,然后使用自定義注解應(yīng)用到需要的地方。
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Transactional("account")
public @interface AccountTx {
}
Hibernate事務(wù)管理
前面簡(jiǎn)單使用了JDBC的事務(wù)管理。不過實(shí)際上大部分人應(yīng)該都需要使用Hibernate等高級(jí)框架的事務(wù)管理功能赴恨。下面也做一些介紹锹安。
首先需要一個(gè)接口叹哭,定義我們需要的操作入宦。
public interface UserDao {
void add(User user);
User get(String name);
}
然后我們實(shí)現(xiàn)這個(gè)接口,定義hibernate數(shù)據(jù)訪問病苗。這里使用了HibernateTemplate類阵幸,這個(gè)類是Spring提供的夺克,我們可以使用這個(gè)類簡(jiǎn)化Hibernate操作。我們可以看到使用這個(gè)類不需要操作Session爆安,Session會(huì)由Spring自動(dòng)管理叛复。當(dāng)然,這里為了使用Hibernate的自然主鍵扔仓,所以還是需要直接使用Session來查找自然主鍵致扯。
public class DefaultUserDao implements UserDao {
private HibernateTemplate template;
private SessionFactory sessionFactory;
@Autowired
public void setTemplate(HibernateTemplate template) {
this.template = template;
}
@Autowired
public void setSessionFactory(SessionFactory sessionFactory) {
this.sessionFactory = sessionFactory;
}
@Override
public void add(User user) {
template.save(user);
}
@Override
public User get(String name) {
Session session = sessionFactory.getCurrentSession();
return session.bySimpleNaturalId(User.class).load(name);
}
}
然后是Spring的配置,所有內(nèi)容都在前面解釋過了当辐。所以應(yīng)該很容易理解抖僵。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">
<!--設(shè)置數(shù)據(jù)源-->
<bean id="dataSource" class="org.apache.commons.dbcp2.BasicDataSource" destroy-method="close">
<property name="driverClassName" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/spring"/>
<property name="username" value="root"/>
<property name="password" value="12345678"/>
</bean>
<!--設(shè)置SessionFactory-->
<bean id="sessionFactory" class="org.springframework.orm.hibernate5.LocalSessionFactoryBean">
<property name="dataSource" ref="dataSource"/>
<property name="annotatedClasses">
<list>
<value>yitian.learn.hibernate.User</value>
</list>
</property>
<property name="hibernateProperties">
<props>
<prop key="hibernate.show_sql">true</prop>
<prop key="hibernate.format_sql">true</prop>
<prop key="hibernate.hbm2ddl.auto">create</prop>
</props>
</property>
</bean>
<!--設(shè)置hibernate模板-->
<bean id="hibernateTemplate" class="org.springframework.orm.hibernate5.HibernateTemplate">
<property name="sessionFactory" ref="sessionFactory"/>
</bean>
<!--設(shè)置hibernate事務(wù)管理器-->
<bean id="transactionManager" class="org.springframework.orm.hibernate5.HibernateTransactionManager">
<property name="sessionFactory" ref="sessionFactory"/>
</bean>
<!--用戶數(shù)據(jù)訪問對(duì)象-->
<bean id="userDao" class="yitian.learn.hibernate.DefaultUserDao"/>
<!--設(shè)置事務(wù)管理-->
<tx:advice id="txAdvice"
transaction-manager="transactionManager">
<tx:attributes>
<tx:method name="get*" read-only="true"/>
<tx:method name="*"/>
</tx:attributes>
</tx:advice>
<!--使用AOP設(shè)置事務(wù)管理-->
<aop:config>
<aop:pointcut id="userDaoPointcut"
expression="execution(* yitian.learn.hibernate.DefaultUserDao.*(..))"/>
<aop:advisor advice-ref="txAdvice" pointcut-ref="userDaoPointcut"/>
</aop:config>
</beans>
最后使用一個(gè)測(cè)試類來測(cè)試一下代碼≡稻荆可以看到耍群,在操作數(shù)據(jù)上我們完全沒有使用Hibernate的事務(wù)API义桂,完全交由Spring管理。當(dāng)然如果拋出異常蹈垢,Spring也會(huì)回滾慷吊。
@ContextConfiguration(locations = {"classpath:hibernate-bean.xml"})
@RunWith(SpringRunner.class)
public class HibernateTransactionTest {
@Autowired
private UserDao userDao;
@Test
public void testHibernateTransaction() {
User user = new User();
user.setUsername("yitian");
user.setPassword("1234");
user.setNickname("易天");
user.setBirthday(LocalDate.now());
userDao.add(user);
User u = userDao.get(user.getUsername());
System.out.println(u);
assertEquals(user.getNickname(), u.getNickname());
}
}
代碼在Csdn code,有興趣的同學(xué)可以看看曹抬。