前言: 事務處理的本質 在學習事務處理前,需要明確一點: 資料庫操作最終都要使用到JDBC,那麼無論上層如何封裝,底層都是調用Connection的commit,rollback來完成 煩人的事務處理: 在日常開發中,數據訪問層(DAO)必然需要進行事務的處理,但是我們會發現,事務處理的代碼通常是簡 ...
前言:
事務處理的本質
在學習事務處理前,需要明確一點:
資料庫操作最終都要使用到JDBC,那麼無論上層如何封裝,底層都是調用Connection的commit,rollback來完成
煩人的事務處理:
在日常開發中,數據訪問層(DAO)必然需要進行事務的處理,但是我們會發現,事務處理的代碼通常是簡單的重覆的,編寫這樣的重覆代碼會浪費大量的時間,所以我們需要找到一種方案可以將這些重覆的代碼進行抽取,以便與管理維護和復用,
我們的需求:在一系列資料庫操作上的方法上增加額外的事務處理代碼,讓原來的方法中只關註具體的數據處理,即在原本以及存在的資料庫操作方法上添加額外的事務處理邏輯
到這裡你應該想到AOP了,沒錯! 這樣的場景下AOP是最好的解決方案;
解決方案:AOP
回顧一下Spring的AOP:在結合目前的需求
1.將目標對象(DAO)放入Spring容器
2.告知Spring你的通知代碼是什麼(事務處理)
3.告知Spring 哪些方法(DAO的CRUD)要應用那些通知(不同的事務處理代碼)
4.從Spring中獲取代理對象來完成原本的CRUD,代理對象會自動完成事務處理
Spring 事務處理API
Spring作為框架,需要進行詳細的設計,全方位的考慮事務處理的各個方面,而不僅是簡單的幫你執行commit,rollback;
Spring對事務處理進行了抽象定義,形成了一套具體的API結構,如下:
TransactionDefinition:定義事務的具體屬性,如隔離級別,超時設置,傳播行為等
TransactionStatus: 用於獲取當前事務的狀態信息
PlatformTransactionMananger: 主要的事務管理介面,提供三個實現類對應不同場景
類型 | 場景 |
---|---|
DataSourceTransactionManager | 使用Spring JDBC或 iBatis 進行持久化數據時使用 |
HibernateTransactionManager | 使用Hibernate3.0版本 進行持久化數據時使用 |
JpaTransactionManager | 使用JPA進行持久化時 使用 |
JtaTransactionManager | 使用一個JTA實現來管理事務,跨數據源時使用 |
註意其分佈在不同的jar包中,使用時根據需要導入對應jar包
事務的傳播行為控制
這是一個新概念但是也非常簡單,即在一個執行sql的方法中調用了另一個方法時,該如何處理這兩個方法之間的事務
Spring定義了7種不同的處理方式:
常量名 | 含義 |
---|---|
PROPAGATION_REQUIRED | 支持當前事務。如果 A 方法已經在事 務中,則 B 事務將直接使用。否則將 創建新事務 |
PROPAGATION_SUPPORTS | 支持當前事務。如果 A 方法已經在事 務中,則 B 事務將直接使用。否則將 以非事務狀態執行 |
PROPAGATION_MANDATORY | 支持當前事務。如果 A 方法沒有事 務,則拋出異常 |
PROPAGATION_REQUIRES_NEW | 將創建新的事務,如果 A 方法已經在 事務中,則將 A 事務掛起 |
PROPAGATION_NOT_SUPPORTED | 不支持當前事務,總是以非事務狀態 執行。如果 A 方法已經在事務中,則 將其掛起 |
PROPAGATION_NEVER | 不支持當前事務,如果 A 方法在事務 中,則拋出異常 |
PROPAGATION.NESTED | 嵌套事務,當外層出現異常則連同內層一起回滾,若外層正常而內部異常,僅回滾內部操作 |
上述涉及的掛起,意思是開啟一個獨立的事務,已存在的事務暫停執行,等待新事務執行完畢後繼續執行,兩個事務不會互相影響
Spring 整合MyBatis
在開始前我們先完成一個基礎的CURD功能,後續開發中Spring + MyBatis項目是很常見的,那要將MyBatis整合到Spring中來,要明確一下兩者的關係和定位
Spring Java開發框架,其本質是一個對象容器,可以幫助我們完成IOC,DI,AOP
MyBatis是一個持久層框架,用於簡化對資料庫的操作
將兩者整合起來,就是將MyBatis中的對象交給Spring來管理,且將這些對象的依賴也交給Spring來管理;
添加依賴:
Spring 3.0 的開發在 MyBatis 3.0 官方發佈前就結束了,於是MyBatis社區自己召集開發者完成了這一部分工作,於是有了mybatis-spring項目,後續Spring也就沒有必要在開發一個新的模塊了,所以該jar是MyBatis提供的
<!-- Spring整合MyBatis依賴 -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>2.0.3</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.2</version>
</dependency>
<!--JDBC-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.44</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.2.2.RELEASE</version>
</dependency>
<!--Spring JDBC-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.2.2.RELEASE</version>
</dependency>
<!--事務管理-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<version>5.2.2.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>5.2.2.RELEASE</version>
</dependency>
<!--AspectJ-->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.8.0</version>
</dependency>
SM基礎使用
配置文件
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
<!-- 載入properties-->
<context:property-placeholder location="jdbc.properties"/>
<!-- 數據源 後續可更換為其他更方便的數據源-->
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="url" value="${url}"/>
<property name="username" value="${usr}"/>
<property name="password" value="${password}"/>
<property name="driverClassName" value="${driver}"/>
</bean>
<!-- MyBatis核心對象SqlSessionFactory-->
<bean id="sessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource"/>
</bean>
<!-- 掃描Mapper 將代理對象放入Spring-->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="com.yh.dao"/>
</bean>
</beans>
jdbc.properties:
driver = com.mysql.jdbc.Driver
url = jdbc:mysql:///SMDB?serverTimezone=Asia/Shanghai&characterEncoding=utf8&useSSL=false
usr = root
password = admin
location = /Users/jerry/.m2/repository/mysql/mysql-connector-java/8.0.17/mysql-connector-java-8.0.17.jar
測試代碼:
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:applicationContext.xml")
public class Test1 {
@Autowired
private StudentMapper studentMapper;
@Test
public void test(){
Student student = studentMapper.selectByPrimaryKey(1);
System.out.println(student);
}
}
編碼式事務
編碼式事務,即在源代碼中加入 事務處理的代碼, 即commit,rollback等,這是非常原始的做法僅作為瞭解
純手動管理事務
配置文件:
<!-- 在之前的配置中添加內容-->
<!--事務管理器-->
<bean class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<!-- 事務定義 -->
<bean class="org.springframework.transaction.support.DefaultTransactionDefinition">
<!-- 隔離級別 可預設-->
<property name="isolationLevelName" value="ISOLATION_REPEATABLE_READ"/>
<!-- 傳播行為 可預設-->
<property name="propagationBehaviorName" value="PROPAGATION_REQUIRED"/>
</bean>
測試代碼:
@Autowired
private StudentMapper studentMapper;
@Autowired
private DataSourceTransactionManager manager;
@Autowired
private DefaultTransactionDefinition definition;
@Test
public void test(){
TransactionStatus transactionStatus = manager.getTransaction(definition);
try{
Student student = studentMapper.selectByPrimaryKey(1);
System.out.println(student);
student.setAge(201);
studentMapper.updateByPrimaryKey(student);
int i = 1/0;
manager.commit(transactionStatus);
}catch (Exception e){
e.printStackTrace();
manager.rollback(transactionStatus);
}
}
上述代碼僅用於測試事務處理的有效性;
我們已經在Spring中配置了MyBatis,併進行了事務處理,但是沒有解決重覆代碼的問題
使用事務模板
事務模板原理是將要執行的具體代碼交給模板,模板會在執行這寫代碼的同時處理事務,當這寫代碼出現異常時則自動回滾事務,以此來簡化書寫
配置文件:
<!-- 在上述配置基礎上刪除事務定義 添加模板Bean-->
<bean id="transactionTemplate" class="org.springframework.transaction.support.TransactionTemplate">
<!-- 傳播行為隔離級別等參數 可預設-->
<property name="propagationBehaviorName" value="PROPAGATION_REQUIRED"/>
<property name="transactionManager" ref="transactionManager"/>
</bean>
測試代碼:
public class Test2 {
@Autowired
private StudentMapper studentMapper;
@Autowired
private TransactionTemplate transactionTemplate;
@Test
public void test(){
transactionTemplate.execute(new TransactionCallback() {
public Object doInTransaction(TransactionStatus transactionStatus) {
Student student = studentMapper.selectByPrimaryKey(1);
System.out.println(student);
student.setAge(1101);
studentMapper.updateByPrimaryKey(student);
// int i = 1/0;
return null;
}
});
}
}
可以看到事務模板要求提供一個實現類來提交原始的資料庫操作給模板,從而完成事務代碼的增強
無論是純手動管理還是利用模板,依然存在大量與業務無關的重覆代碼,這也是編碼式事務最大的問題;
聲明式事務
即不需要在原來的業務邏輯代碼中加入任何事務相關的代碼,而是通過xml,或者註解的方式,來告訴框架,哪些方法需要添加事務處理代碼,讓框架來完成在原始業務邏輯前後增加事務處理的代碼(通過AOP),這也是AOP使用較多的場景之一;
基於tx名稱空間的配置
配置文件:
需要引入aop和tx名稱空間
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
<import resource="mybatis-beans.xml"/>
<context:component-scan base-package="com.yh.service"/>
<!-- 添加事務管理器-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<!-- 事務通知-->
<tx:advice id="transactionAdvice" transaction-manager="transactionManager">
<tx:attributes>
<!-- name指定要應用的方法名稱 還有其他事務常用屬性如隔離級別傳播行為等..-->
<tx:method name="*" read-only="false"/>
</tx:attributes>
</tx:advice>
<!-- 切點信息-->
<aop:config >
<!-- 根據表達式中的信息可以自動查找到目標對象 從而進行增強 先查找目標再生產代理-->
<aop:pointcut id="pointcut" expression="execution(* com.yh.service.*.*(..))"/>
<aop:advisor advice-ref="transactionAdvice" pointcut-ref="pointcut"/>
</aop:config>
</beans>
tx:method屬性:
屬性名 | 含義 |
---|---|
name | 匹配的方法名稱 |
isolation | 事務隔離級別 |
read-only | 是否採用優化的只 讀事務 |
timeout | 超時 |
rollback-for | 需要回滾的異常類 |
propagation | 傳播行為 |
no-rollback-for | 不需要回滾的異常類 |
Service:
@Service
public class StudentService {
@Autowired
private StudentMapper studentMapper;
public Student getStudent(int id ){
return studentMapper.selectByPrimaryKey(id);
}
public void update(Student student){
studentMapper.updateByPrimaryKey(student);
int i = 1/0;
}
}
測試代碼:
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:applicationContext3.xml")
public class Test3 {
@Autowired
StudentService studentService;
@Test
public void test(){
Student student = studentService.getStudent(1);
System.out.println(student);
student.setAge(8818);
studentService.update(student);
}
}
強調:事務增強應該應用到Service層,即業務邏輯層,應為一個業務方法可能涉及多個資料庫操作,當某個操作遇到異常時需要將所有操作全部回滾
基於註解的配置
Spring當然也支持採用註解形式來處理事務
開啟註解事務支持:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
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/context http://www.springframework.org/schema/context/spring-context.xsd">
<!--為了分離關註點,故將MyBatis相關配置放到其他配置文件了-->
<import resource="mybatis-beans.xml"/>
<context:component-scan base-package="com.yh.service"/>
<!-- 添加事務管理器-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<!-- 開啟註解事務管理-->
<tx:annotation-driven transaction-manager="transactionManager"/>
</beans>
Service中增加方法:
@Transactional(propagation = Propagation.REQUIRED,readOnly = false)
public void transactionTest(){
Student student = getStudent(1);
student.setAge(1);
update(student);
int i = 1/0;
student.setName("jack");
update(student);
}
//當然註解上的參數都是可選的採用預設值即可
測試代碼
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:applicationContext4.xml")
public class Test4 {
@Autowired
StudentService studentService;
@Test
public void test(){
studentService.transactionTest();
}
}
你可能會覺得註解的方式比xml配置簡單的多,但是考慮一下,當你的項目特別大,涉及的表很多的時候呢,你可能需要些很多很多的註解,假設後期需要修改某些屬性,還得一個個改;
所以大項目建議採用XML,小項目使用註解也ok;
原理簡述
聲明式事務其底層用的還是AOP,你完全可以自己手動的配置每個環節,如目標,通知,切麵,代理等,這能讓你更清晰的理解每一行代碼背後到底做了什麼事情;
配置文件:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<import resource="mybatis-beans.xml"/>
<!-- 添加事務管理器-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<!-- 要進行事務增強的目標對象-->
<bean id="serviceTarget" class="com.yh.service.StudentService"/>
<!-- 事務通知-->
<bean id="transactionInterceptor" class="org.springframework.transaction.interceptor.TransactionInterceptor">
<property name="transactionManager" ref="transactionManager"/>
<property name="transactionAttributes">
<props>
<prop key="*">PROPAGATION_REQUIRED</prop>
</props>
</property>
</bean>
<!-- 代理對象-->
<bean id="orderService" class="org.springframework.aop.framework.ProxyFactoryBean">
<property name="target" ref="serviceTarget"/>
<property name="interceptorNames">
<list>
<idref bean="transactionInterceptor"/>
</list>
</property>
</bean>
</beans>
測試代碼:
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:applicationContext5.xml")
public class Test5 {
@Autowired
@Qualifier("orderService")
StudentService studentService;
@Test
public void test(){
Student student = studentService.getStudent(1);
student.setAge(1);
studentService.update(student);
}
}