1. 程式人生 > >Spring 事務處理

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結構,如下:

  • 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);
    }
}