Spring 事務處理


前言:

事務處理的本質

在學習事務處理前,需要明確一點:

數據庫操作最終都要使用到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結構,如下:

image-20200114134619820

  • 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  system-properties-mode指定是否是否使用系統屬性 例如user值為當前系統用戶名
				該標簽只加載一次 若有多個屬性文件可以使用逗號隔開-->
        <context:property-placeholder location="classpath:jdbc.properties" system-properties-mode="NEVER"/>

<!--    數據源 后續可更換為其他更方便的數據源-->
    <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>

注意:建議在資源文件前添加classpath:可避免在web環境下資源路徑發生變化導致的異常

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名稱空間的XML配置

配置文件:

需要引入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);
    }
}

對了:在利用IDEA自動引入名稱空間時 經常會有多個名稱空間相同的 注意別選錯了......


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM