Spring事務

一、事務簡介

具體的看資料庫中關於事務的知識點,這裡做一個大概

1. 什麼是事務?

講mysql的時候,提出了事務。 事務是指一組sql語句的集合, 集合中有多條sql語句可能是insert , update ,select ,delete, 我們希望這些多個sql語句都能成功,或者都失敗, 這些sql語句的執行是一致的,作為一個整體執行。

2. 在什麼時候想到使用事務

當我的操作,涉及得到多個表,或者是多個sql語句的insert,update,delete。需要保證這些語句都是成功才能完成我的功能,或者都失敗,保證操作是符合要求的

在java程式碼中寫程式,控制事務,此時事務應該放在那裡呢?

service類的業務方法上,因為業務方法會調用多個dao方法,執行多個sql語句

3. 通常使用JDBC訪問資料庫, 還是mybatis訪問資料庫怎麼處理事務
jdbc訪問資料庫,處理事務 Connection conn ; conn.commit(); conn.rollback();
mybatis訪問資料庫,處理事務, SqlSession.commit(); SqlSession.rollback();
hibernate訪問資料庫,處理事務, Session.commit(); Session.rollback();

4. 問題中事務的處理方式,有什麼不足

  1. 不同的資料庫訪問技術,處理事務的對象,方法不同,需要了解不同資料庫訪問技術使用事務的原理
  2. 掌握多種資料庫中事務的處理邏輯。什麼時候提交事務,什麼時候回顧事務
  3. 處理事務的多種方法。

總結: 就是多種資料庫的訪問技術,有不同的事務處理的機制,對象,方法

5. 怎麼解決不足
spring提供一種處理事務的統一模型, 能使用統一步驟,方式完成多種不同資料庫訪問技術的事務處理。

使用spring的事務處理機制,可以完成mybatis訪問資料庫的事務處理
使用spring的事務處理機制,可以完成hibernate訪問資料庫的事務處理。

6. 處理事務,需要怎麼做,做什麼
spring處理事務的模型,使用的步驟都是固定的。把事務使用的資訊提供給spring就可以了

  1. 事務內部提交,回滾事務,使用的事務管理器對象,代替你完成commit,rollback事務管理器是一個介面和他的眾多實現類。

    介面:PlatformTransactionManager ,定義了事務重要方法 commit ,rollback

    實現類:spring把每一種資料庫訪問技術對應的事務處理類都創建好了。
    mybatis訪問資料庫—spring創建好的是DataSourceTransactionManager
    hibernate訪問資料庫—-spring創建的是HibernateTransactionManager

    怎麼使用:你需要告訴spring 你用是那種資料庫的訪問技術,怎麼告訴spring呢?
    聲明資料庫訪問技術對於的事務管理器實現類, 在spring的配置文件中使用聲明就可以了
    例如,你要使用mybatis訪問資料庫,你應該在xml配置文件中
    <bean id=「xxx” class=”…DataSourceTransactionManager”>

  2. 你的業務方法需要什麼樣的事務,說明需要事務的類型。

    ​ 1. 說明方法需要的事務:
    事務的隔離級別:有4個值。
    DEFAULT:採用 DB 默認的事務隔離級別。MySql 的默認為 REPEATABLE_READ; Oracle默認為 READ_COMMITTED。
    ➢ READ_UNCOMMITTED:讀未提交。未解決任何並發問題。
    ➢ READ_COMMITTED:讀已提交。解決臟讀,存在不可重複讀與幻讀。
    ➢ REPEATABLE_READ:可重複讀。解決臟讀、不可重複讀,存在幻讀
    ➢ SERIALIZABLE:串列化。不存在並發問題。

    1. 事務的超時時間: 表示一個方法最長的執行時間,如果方法執行時超過了時間,事務就回滾
      • 單位是秒, 整數值, 默認是 -1.
    2. 事務的傳播行為 : 控制業務方法是不是有事務的, 是什麼樣的事務的, 7個傳播行為,表示你的業務方法調用時,事務在方法之間是如果使用的,記得前三個就行
      • PROPAGATION_REQUIRED
      • PROPAGATION_REQUIRES_NEW
      • PROPAGATION_SUPPORTS
      • PROPAGATION_MANDATORY
      • PROPAGATION_NESTED
      • PROPAGATION_NEVER
      • PROPAGATION_NOT_SUPPORTED
    3. 事務提交事務,回滾事務的時機
1. 當你的業務方法,執行成功,沒有異常拋出,當方法執行完畢,spring在方法執行後提交事務。事務管理器commit
2. 當你的業務方法拋出運行時異常或ERROR, spring執行回滾,調用事務管理器的rollback
	 運行時異常的定義: RuntimeException  和他的子類都是運行時異常, 例如NullPointException , NumberFormatException
3. 當你的業務方法拋出非運行時異常, 主要是受查異常時,提交事務
	受查異常:在你寫程式碼中,必須處理的異常。例如IOException, SQLException

7. 總結spring的事務

  1. 管理事務的是 事務管理和他的實現類
  2. spring的事務是一個統一模型
  • 指定要使用的事務管理器實現類,使用<bean>

  • 指定哪些類,哪些方法需要加入事務的功能

  • 指定方法需要的隔離級別,傳播行為,超時

    你需要告訴spring,你的項目中類資訊,方法的名稱,方法的事務傳播行為

二、程式舉例環境搭建

購買商品,用戶下單,向銷售表中添加銷售記錄,從商品表中減少數據

1. 創建數據表

創建數據表sale(銷售表)和goods(商品表)

sale:id自增,方便後面測試

goods:id不自增,

注意,平時開發price用decimal類型

向goods表示適當的添加兩天數據,用於測試

2. maven依賴pom.xml

<?xml version="1.0" encoding="UTF-8"?>

<project xmlns="//maven.apache.org/POM/4.0.0" xmlns:xsi="//www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="//maven.apache.org/POM/4.0.0 //maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>com.md</groupId>
  <artifactId>07-spring-trans</artifactId>
  <version>1.0-SNAPSHOT</version>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <maven.compiler.source>1.8</maven.compiler.source>
    <maven.compiler.target>1.8</maven.compiler.target>
  </properties>

  <dependencies>

    <!-- 單元測試-->
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.11</version>
      <scope>test</scope>
    </dependency>

    <!--spring核心-->
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-context</artifactId>
      <version>5.2.5.RELEASE</version>
    </dependency>

    <!--spring事務用到的-->
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-tx</artifactId>
      <version>5.2.5.RELEASE</version>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-jdbc</artifactId>
      <version>5.2.5.RELEASE</version>
    </dependency>


    <!--mybatis的-->
    <dependency>
      <groupId>org.mybatis</groupId>
      <artifactId>mybatis</artifactId>
      <version>3.5.1</version>
    </dependency>

    <!--mybatis和spring集成的-->
    <dependency>
      <groupId>org.mybatis</groupId>
      <artifactId>mybatis-spring</artifactId>
      <version>1.3.1</version>
    </dependency>

    <!--mysql驅動-->
    <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
      <version>5.1.9</version>
    </dependency>


    <!--德魯伊,資料庫連接池-->
    <dependency>
      <groupId>com.alibaba</groupId>
      <artifactId>druid</artifactId>
      <version>1.1.12</version>
    </dependency>




  </dependencies>

  <build>

    <!--目的是把src/main/java目錄中的xml文件包含到輸出結果中,也就是輸出到classes目錄中-->
    <resources>
      <resource>
        <directory>src/main/java</directory><!--所在的目錄-->
        <includes><!--包括目錄下的.properties,.xml 文件都會掃描到-->
          <include>**/*.properties</include>
          <include>**/*.xml</include>
        </includes>
        <filtering>false</filtering>
      </resource>
    </resources>



  </build>

</project>

3. 創建實體類

package com.md.domain;

/**
 * @author MD
 * @create 2020-08-11 9:20
 */
public class Sale {
    private Integer id;
    private Integer gid;
    private Integer nums;


    public Sale() {
    }

    public Sale(Integer id, Integer gid, Integer nums) {
        this.id = id;
        this.gid = gid;
        this.nums = nums;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public void setGid(Integer gid) {
        this.gid = gid;
    }

    public void setNums(Integer nums) {
        this.nums = nums;
    }


    public Integer getId() {
        return id;
    }

    public Integer getGid() {
        return gid;
    }

    public Integer getNums() {
        return nums;
    }

    @Override
    public String toString() {
        return "SaleDao{" +
                "id=" + id +
                ", gid=" + gid +
                ", nums=" + nums +
                '}';
    }
}

//-----------------
package com.md.domain;

/**
 * @author MD
 * @create 2020-08-11 9:21
 */
public class Goods {

    private Integer id;

    private String name;

    private Integer amount;

//實際開發中不用float
    private Float price;


    public Goods() {
    }

    public Goods(Integer id, String name, Integer amount, Float price) {
        this.id = id;
        this.name = name;
        this.amount = amount;
        this.price = price;
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getAmount() {
        return amount;
    }

    public void setAmount(Integer amount) {
        this.amount = amount;
    }

    public Float getPrice() {
        return price;
    }

    public void setPrice(Float price) {
        this.price = price;
    }

    @Override
    public String toString() {
        return "Goods{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", amount=" + amount +
                ", price=" + price +
                '}';
    }
}
//--------------------------------

4. 定義dao介面

package com.md.dao;

import com.md.domain.Sale;

/**
 * @author MD
 * @create 2020-08-11 9:24
 */
public interface SaleDao {

    // 增加銷售記錄
    int insertSale(Sale sale);

}
//----------------------
package com.md.dao;

import com.md.domain.Goods;

/**
 * @author MD
 * @create 2020-08-11 9:30
 */
public interface GoodsDao {
    // 更新庫存
    int updateGoods(Goods goods);

    // 查詢商品的資訊,根據id
    Goods selectGoods(Integer id);

}

5. 定義dao介面對應的sql映射文件

SaleDao.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "//mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.md.dao.SaleDao">
    <insert id="insertSale">
      insert into sale(gid,nums) values(#{gid},#{nums})
    </insert>
</mapper>

GoodsDao.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "//mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.md.dao.GoodsDao">
    <select id="selectGoods" resultType="com.md.domain.Goods">
      select id , name , amount , price from goods where id=#{id}
    </select>



    <!-- -->
    <update id="updateGoods">
        update goods set amount = amount - #{amount} where id=#{id}
    </update>

</mapper>

6. 定義異常類

定義service層可能拋出的異常類

package com.md.excep;

/**
 * @author MD
 * @create 2020-08-11 9:49
 */

// 自定義的運行時異常
public class NotEnoughException extends RuntimeException {


    public NotEnoughException() {
        super();
    }

    public NotEnoughException(String message) {
        super(message);
    }
}

7. 定義service介面及實現類

package com.md.service;

/**
 * @author MD
 * @create 2020-08-11 9:43
 */
public interface BuyGoodsService {

    // 購買商品,goodsId:購買商品的編號,nums:購買的數量
    void buy(Integer goodsId , Integer nums);

}
//-----------------------------------
package com.md.service.impl;

import com.md.dao.GoodsDao;
import com.md.dao.SaleDao;
import com.md.domain.Goods;
import com.md.domain.Sale;
import com.md.excep.NotEnoughException;
import com.md.service.BuyGoodsService;

/**
 * @author MD
 * @create 2020-08-11 9:45
 */
public class BuyGoodsServiceImpl implements BuyGoodsService {


    private SaleDao saleDao;
    private GoodsDao goodsDao;

    public void setSaleDao(SaleDao saleDao) {
        this.saleDao = saleDao;
    }

    public void setGoodsDao(GoodsDao goodsDao) {
        this.goodsDao = goodsDao;
    }

    @Override
    public void buy(Integer goodsId, Integer nums) {

        System.out.println("=========buy方法開始===========");

        // 記錄銷售記錄,向sale表中添加數據
        Sale sale = new Sale();
        sale.setGid(goodsId);
        sale.setNums(nums);
        saleDao.insertSale(sale);



        // 先查詢該商品
        Goods goods = goodsDao.selectGoods(goodsId);
        if (goods == null){
            // 商品不存在
            throw new NullPointerException("編號:"+goodsId+" 的商品不存在");
        }else if (goods.getAmount() < nums){
            // 商品庫存不足
            throw new NotEnoughException("編號:"+goodsId+" 的商品不足,只能購買: "+goods.getAmount()+" 個");
        }


        // 更新庫存
        Goods buyGoods = new Goods();
        buyGoods.setId(goodsId);
        buyGoods.setAmount(nums);
        goodsDao.updateGoods(buyGoods);

        System.out.println("=========buy方法結束===========");

    }
}

8. mybatis.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "//mybatis.org/dtd/mybatis-3-config.dtd">

<configuration>


    <!--settings:控制mybatis全局行為-->
    <settings>

        <!--設置mybatis輸出日誌-->
        <setting name="logImpl" value="STDOUT_LOGGING"/>
    </settings>



    <!--設置別名-->
    <typeAliases>
        <!--name:實體類所在的包名-->
        <package name="com.md.domain"/>

    </typeAliases>




    <!-- sql映射文件的位置 -->
    <mappers>

        <!--name是包名,這個包中所有mapper.xml一次載入-->
        <package name="com.md.dao"/>
    </mappers>

</configuration>

9. Spring配置文件

applicationContext.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="//www.springframework.org/schema/beans"
       xmlns:xsi="//www.w3.org/2001/XMLSchema-instance"
       xmlns:context="//www.springframework.org/schema/context"
       xsi:schemaLocation="//www.springframework.org/schema/beans
       //www.springframework.org/schema/beans/spring-beans.xsd //www.springframework.org/schema/context //www.springframework.org/schema/context/spring-context.xsd">

    <!--
        把資料庫的配置資訊寫在一個獨立的文件中,編譯修改資料庫的配置內容
        讓spring知道jdbc.properties文件的位置
    -->
    <context:property-placeholder location="classpath:jdbc.properties"/>


    <!--聲明數據源DataSource,作用是連接資料庫-->
    <bean id="myDataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">

        <!--set注入提供連接資料庫資訊-->
        <!--<property name="url" value="jdbc:mysql://localhost:3306/ssm" />-->
        <!--<property name="username" value="root" />-->
        <!--<property name="password" value="123456" />-->
        <!--<property name="maxActive" value="20" />-->

        <property name="url" value="${jdbc.url}" />
        <property name="username" value="${jdbc.username}" />
        <property name="password" value="${jdbc.password}" />
        <property name="maxActive" value="${jdbc.maxActive}" />

    </bean>



    <!--SqlSessionFactory-->
    <!--聲明的是mybatis中提供的SqlSessionFactoryBean類,這個類內部創建SqlSessionFactory-->
    <bean id="SqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">

        <!--set注入,把資料庫連接池付給dataSource屬性-->
        <property name="dataSource" ref="myDataSource"/>
        <!--mybatis主配置文件的位置
            configLocation屬性是Resource類型,讀取配置文件
            它的賦值使用的是value , 指定文件的路徑,使用的是classpath:表示文件的位置
        -->
        <property name="configLocation" value="classpath:mybatis.xml"/>
    </bean>



    <!--創建 dao對象
        使用SqlSession的getMapper(StudentDao.class)
        MapperScannerConfigurer在內部調用getMapper()生成每個dao介面的代理對象
    -->
    <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
        <!--指定的是SqlSessionFactory對象的id-->
        <property name="sqlSessionFactoryBeanName" value="SqlSessionFactory"/>

        <!--指定包名,包名是dao介面所在的包名
            MapperScannerConfigurer會掃描這個包中的所有介面,把每個介面都執行
            一次getMapper()方法,得到每個介面的dao對象
            創建好的dao對象放入到spring的容器中

            dao默認對象的名稱:是介面名字的首字母小寫
        -->


        <property name="basePackage" value="com.md.dao"/>

        <!--多個包-->
        <!--<property name="basePackage" value="com.md.dao,com.md.dao2"/>-->


    </bean>


    <!--上面的這個是一個模板,只有最後dao對象的這個包名的value的值是根據自己創建寫的-->




    <!--下面的就是自己定義的service-->



    <!--聲明service-->
    <bean id="buyGoodsService" class="com.md.service.impl.BuyGoodsServiceImpl">

        <!--就是上面通過創建的dao對象,在service介面的實現類中使用-->
        <property name="saleDao" ref="saleDao"/>
        <property name="goodsDao" ref="goodsDao"/>
    </bean>

</beans>

10. 測試

 @Test
    public void test01(){
        String config = "applicationContext.xml";

        ApplicationContext ac = new ClassPathXmlApplicationContext(config);

        // 從容器中獲取service,你聲明service時候的id
        BuyGoodsService buyGoodsService = (BuyGoodsService) ac.getBean("buyGoodsService");
        buyGoodsService.buy(1001,101);

    }

此時程式一切正常

三、使用 Spring 的事務註解管理事務

通過@Transactional 註解方式,可將事務織入到相應 public 方法中,實現事務管理

主要記住前三個就行了

@Transactional 的所有可選屬性如下所示:

  • propagation:用於設置事務傳播屬性。該屬性類型為 Propagation 枚舉,默認值為Propagation.REQUIRED
  • isolation:用於設置事務的隔離級別。該屬性類型為 Isolation 枚舉,默認值為Isolation.DEFAULT。
  • readOnly:用於設置該方法對資料庫的操作是否是只讀的。該屬性為 boolean,默認值為 false。
  • timeout:用於設置本操作與資料庫連接的超時時限。單位為秒,類型為 int,默認值為-1,即沒有時限。
  • rollbackFor:指定需要回滾的異常類。類型為 Class[],默認值為空數組。當然,若只有一個異常類時,可以不使用數組。
  • rollbackForClassName:指定需要回滾的異常類類名。類型為 String[],默認值為空數組。當然,若只有一個異常類時,可以不使用數組
  • noRollbackFor:指定不需要回滾的異常類。類型為 Class[],默認值為空數組。當然,若只有一個異常類時,可以不使用數組。
  • noRollbackForClassName:指定不需要回滾的異常類類名。類型為 String[],默認值為空數組。當然,若只有一個異常類時,可以不使用數組。

需要注意的是,

@Transactional 若用在方法上,只能用於 public 方法上

對於其他非 public方法,如果加上了註解@Transactional,雖然 Spring 不會報錯,但不會將指定事務織入到該方法中。因為 Spring 會忽略掉所有非 public 方法上的@Transaction 註解。
若@Transaction 註解在類上,則表示該類上所有的方法均將在執行時織入事務。

實現註解的事務步驟:

把上面寫好的程式重新複製一份,內容不變

1. 聲明事務管理器

還是在Spring的配置文件中加入

<!--1. 聲明事務管理器-->
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <!-- 連接資料庫,指定數據源-->
        <property name="dataSource" ref="myDataSource"/>
    </bean>

2. 開啟註解驅動

<!--2. 開啟事務註解驅動,告訴spring使用註解管理事務,創建代理對象 -->
    <!--注意:選擇結尾是tx的driven-->
    <!--transaction-manager:事務管理器對象的id-->
    <tx:annotation-driven transaction-manager="transactionManager"/>

3. 完整Spring配置文件

applicationContext.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="//www.springframework.org/schema/beans"
       xmlns:xsi="//www.w3.org/2001/XMLSchema-instance"
       xmlns:context="//www.springframework.org/schema/context" xmlns:tx="//www.springframework.org/schema/tx"
       xsi:schemaLocation="//www.springframework.org/schema/beans
       //www.springframework.org/schema/beans/spring-beans.xsd //www.springframework.org/schema/context //www.springframework.org/schema/context/spring-context.xsd //www.springframework.org/schema/tx //www.springframework.org/schema/tx/spring-tx.xsd">

    <context:property-placeholder location="classpath:jdbc.properties"/>


    <!--聲明數據源DataSource,作用是連接資料庫-->
    <bean id="myDataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
        <property name="url" value="${jdbc.url}" />
        <property name="username" value="${jdbc.username}" />
        <property name="password" value="${jdbc.password}" />
        <property name="maxActive" value="${jdbc.maxActive}" />
    </bean>



    <!--SqlSessionFactory-->
    <bean id="SqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
        <property name="dataSource" ref="myDataSource"/>
        <property name="configLocation" value="classpath:mybatis.xml"/>
    </bean>



    <!--創建 dao對象-->
    <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
        <property name="sqlSessionFactoryBeanName" value="SqlSessionFactory"/>
        <property name="basePackage" value="com.md.dao"/>
    </bean>

    <!--聲明service-->
    <bean id="buyGoodsService" class="com.md.service.impl.BuyGoodsServiceImpl">

        <!--就是上面通過創建的dao對象-->
        <property name="saleDao" ref="saleDao"/>
        <property name="goodsDao" ref="goodsDao"/>
    </bean>

    <!--
        使用spring的事務處理
    -->

    <!--1. 聲明事務管理器-->
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <!-- 連接資料庫,指定數據源-->
        <property name="dataSource" ref="myDataSource"/>
    </bean>

    <!--2. 開啟事務註解驅動,告訴spring使用註解管理事務,創建代理對象 -->
    <!--注意:選擇結尾是tx的driven-->
    <!--transaction-manager:事務管理器對象的id-->
    <tx:annotation-driven transaction-manager="transactionManager"/>


    <!-- 3. 在需要事務方法上加註解 -->

</beans>

4. 業務層 public 方法加入事務屬性

在方法上面加@Transactional

這裡也就是在service介面的實現類里的方法上,全部程式碼如下:

package com.md.service.impl;

import com.md.dao.GoodsDao;
import com.md.dao.SaleDao;
import com.md.domain.Goods;
import com.md.domain.Sale;
import com.md.excep.NotEnoughException;
import com.md.service.BuyGoodsService;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

/**
 * @author MD
 * @create 2020-08-11 9:45
 */
public class BuyGoodsServiceImpl implements BuyGoodsService {


    private SaleDao saleDao;
    private GoodsDao goodsDao;

    public void setSaleDao(SaleDao saleDao) {
        this.saleDao = saleDao;
    }

    public void setGoodsDao(GoodsDao goodsDao) {
        this.goodsDao = goodsDao;
    }


    /**
     *rollbackFor:表示發生指定的異常一定回滾
     *
     */

//    @Transactional(
//            propagation = Propagation.REQUIRED,
//            isolation = Isolation.DEFAULT,
//            readOnly = false,
//            rollbackFor = {
//                    NullPointerException.class,NotEnoughException.class
//            }
//    )

    // 都使用默認值也是可以的,默認的傳播行為是REQUIRED,默認的隔離級別是DEFAULT
    // 默認拋出運行時異常,回滾事務
    @Transactional
    @Override
    public void buy(Integer goodsId, Integer nums) {

        System.out.println("=========buy方法開始===========");

        // 記錄銷售記錄,向sale表中添加數據
        Sale sale = new Sale();
        sale.setGid(goodsId);
        sale.setNums(nums);
        saleDao.insertSale(sale);



        // 先查詢該商品
        Goods goods = goodsDao.selectGoods(goodsId);
        if (goods == null){
            // 商品不存在
            throw new NullPointerException("編號:"+goodsId+" 的商品不存在");
        }else if (goods.getAmount() < nums){
            // 商品庫存不足
            throw new NotEnoughException("編號:"+goodsId+" 的商品不足,只能購買: "+goods.getAmount()+" 個");
        }


        // 更新庫存
        Goods buyGoods = new Goods();
        buyGoods.setId(goodsId);
        buyGoods.setAmount(nums);
        goodsDao.updateGoods(buyGoods);

        System.out.println("=========buy方法結束===========");

    }
}

5. 測試

@Test
    public void test01(){
        String config = "applicationContext.xml";

        ApplicationContext ac = new ClassPathXmlApplicationContext(config);

        // 從容器中獲取service,你聲明service時候的id
        BuyGoodsService buyGoodsService = (BuyGoodsService) ac.getBean("buyGoodsService");

        // jdk動態代理對象com.sun.proxy.$Proxy16
        //System.out.println(buyGoodsService.getClass().getName());

        buyGoodsService.buy(1003,100);

    }

假設此時的1003號商品不存在,即使上面已經向sale表中添加數據了,但是由於還沒有執行到更新庫存方法的時候出現了異常,此時由於添加了事務,這個時候就會回滾,你查看sale的數據表沒有數據,但是你之後買一個存在的商品,發現id號已經不是連續的了,如圖所示:

由於sale的id設置的自增,就是因為回滾的原因,id不連續

四、使用 AspectJ 的 AOP 配置管理事務

使用 XML 配置事務代理的方式的不足是,每個目標類都需要配置事務代理。當目標類較多,配置文件會變得非常臃腫。
使用 XML 配置顧問方式可以自動為每個符合切入點表達式的類生成事務代理。其用法很簡單,只需將前面程式碼中關於事務代理的配置刪除,再替換為如下內容即可

還是使用上面舉例的程式,複製一份

1. maven依賴pom.xml

還是直接把完整的pom.xml放這,主要就是添加了一個aspectj的

<?xml version="1.0" encoding="UTF-8"?>

<project xmlns="//maven.apache.org/POM/4.0.0" xmlns:xsi="//www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="//maven.apache.org/POM/4.0.0 //maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>com.md</groupId>
  <artifactId>09-spring-trans-aspectj</artifactId>
  <version>1.0-SNAPSHOT</version>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <maven.compiler.source>1.8</maven.compiler.source>
    <maven.compiler.target>1.8</maven.compiler.target>
  </properties>

  <dependencies>

    <!-- 單元測試-->
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.11</version>
      <scope>test</scope>
    </dependency>



    <!--aspectj依賴-->
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-aspects</artifactId>
      <version>5.2.5.RELEASE</version>
    </dependency>


    <!--spring核心-->
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-context</artifactId>
      <version>5.2.5.RELEASE</version>
    </dependency>

    <!--spring事務用到的-->
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-tx</artifactId>
      <version>5.2.5.RELEASE</version>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-jdbc</artifactId>
      <version>5.2.5.RELEASE</version>
    </dependency>


    <!--mybatis的-->
    <dependency>
      <groupId>org.mybatis</groupId>
      <artifactId>mybatis</artifactId>
      <version>3.5.1</version>
    </dependency>

    <!--mybatis和spring集成的-->
    <dependency>
      <groupId>org.mybatis</groupId>
      <artifactId>mybatis-spring</artifactId>
      <version>1.3.1</version>
    </dependency>

    <!--mysql驅動-->
    <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
      <version>5.1.9</version>
    </dependency>


    <!--德魯伊,資料庫連接池-->
    <dependency>
      <groupId>com.alibaba</groupId>
      <artifactId>druid</artifactId>
      <version>1.1.12</version>
    </dependency>




  </dependencies>

  <build>

    <!--目的是把src/main/java目錄中的xml文件包含到輸出結果中,也就是輸出到classes目錄中-->
    <resources>
      <resource>
        <directory>src/main/java</directory><!--所在的目錄-->
        <includes><!--包括目錄下的.properties,.xml 文件都會掃描到-->
          <include>**/*.properties</include>
          <include>**/*.xml</include>
        </includes>
        <filtering>false</filtering>
      </resource>
    </resources>



  </build>

</project>

2. 在容器中添加事務管理器

還是在Spring的配置文件中加入

 <!--1. 聲明事務管理器對象-->
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="myDataSource"/>
    </bean>

3. 配置事務通知

為事務通知設置相關屬性。用於指定要將事務以什麼方式織入給哪些方法。
例如,應用到 buy 方法上的事務要求是必須的,且當 buy 方法發生異常後要回滾業務

 <!--2. 聲明業務方法它的事務屬性
        id:自定義名稱,表示<tx:advice>和</tx:advice>之間的配置內容
        transaction-manager:事務管理器對象的id
    -->
    <!--注意:advice選擇tx結尾的-->
    <tx:advice id="myAdvice" transaction-manager="transactionManager">
        <!-- tx:attributes :配置事務的屬性-->
        <tx:attributes>
            <!-- tx:method:給具體的方法配置事務屬性,可以有多個,分別給不同的方法設置事務
                name:方法名稱
                      1. 完整的方法名稱,不帶包和類
                      2. 方法可以使用通配符,*表示任意字元
                propagation:傳播行為
                isolation:隔離級別
                rollback-for:你指定的異常類名,全限定類名,發生異常一定回滾

            -->
            <tx:method name="buy" propagation="REQUIRED" isolation="DEFAULT"
                       rollback-for="java.lang.NullPointerException , com.md.excep.NotEnoughException"/>



            <!--當方法多的時候,使用通配符-->
            <!--表示add開頭的方法,其他都是默認-->
            <tx:method name="add*" />

            <tx:method name="modify*"/>


        </tx:attributes>

    </tx:advice>

4. 配置增強器

指定將配置好的事務通知,織入給誰

 <!--3. 配置aop-->

    <aop:config>
        <!--配置切入點表達式:指定那些包中的類,要使用事務
            id:切入點表達式名稱,唯一
            expression:切入點表達式,指定那些類要使用事務,aspectj會創建代理對象
                        這裡寫的表示所有service類中的所有方法
        -->
        <aop:pointcut id="servicePt" expression="execution(* *..service..*.*(..))"/>

        <!--配置增強器:關聯advice和pointcut
            advice-ref:通知,上面tx:advice裡面的配置
            pointcut-ref:切入點表達式的id
         -->
        <aop:advisor advice-ref="myAdvice" pointcut-ref="servicePt"/>
    </aop:config>

5. 完整Spring配置文件

applicationContext.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="//www.springframework.org/schema/beans"
       xmlns:xsi="//www.w3.org/2001/XMLSchema-instance"
       xmlns:context="//www.springframework.org/schema/context" xmlns:tx="//www.springframework.org/schema/tx"
       xmlns:aop="//www.springframework.org/schema/aop"
       xsi:schemaLocation="//www.springframework.org/schema/beans
       //www.springframework.org/schema/beans/spring-beans.xsd //www.springframework.org/schema/context //www.springframework.org/schema/context/spring-context.xsd //www.springframework.org/schema/tx //www.springframework.org/schema/tx/spring-tx.xsd //www.springframework.org/schema/aop //www.springframework.org/schema/aop/spring-aop.xsd">

    <!--
        把資料庫的配置資訊寫在一個獨立的文件中,編譯修改資料庫的配置內容
        讓spring知道jdbc.properties文件的位置
    -->
    <context:property-placeholder location="classpath:jdbc.properties"/>


    <!--聲明數據源DataSource,作用是連接資料庫-->
    <bean id="myDataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
        <property name="url" value="${jdbc.url}" />
        <property name="username" value="${jdbc.username}" />
        <property name="password" value="${jdbc.password}" />
        <property name="maxActive" value="${jdbc.maxActive}" />

    </bean>



    <!--SqlSessionFactory-->
    <bean id="SqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
        <property name="dataSource" ref="myDataSource"/>
        <property name="configLocation" value="classpath:mybatis.xml"/>
    </bean>



    <!--創建 dao對象-->
    <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
        <property name="sqlSessionFactoryBeanName" value="SqlSessionFactory"/>

        <property name="basePackage" value="com.md.dao"/>
    </bean>



    <!--下面的就是自己定義的service-->



    <!--聲明service-->
    <bean id="buyGoodsService" class="com.md.service.impl.BuyGoodsServiceImpl">

        <!--就是上面通過創建的dao對象-->
        <property name="saleDao" ref="saleDao"/>
        <property name="goodsDao" ref="goodsDao"/>
    </bean>




    <!--
        聲明式事務處理:和源程式碼完全分離
    -->

    <!--1. 聲明事務管理器對象-->
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="myDataSource"/>
    </bean>


    <!--2. 聲明業務方法它的事務屬性
        id:自定義名稱,表示<tx:advice>和</tx:advice>之間的配置內容
        transaction-manager:事務管理器對象的id
    -->
    <!--注意:advice選擇tx結尾的-->
    <tx:advice id="myAdvice" transaction-manager="transactionManager">
        <!-- tx:attributes :配置事務的屬性-->
        <tx:attributes>
            <!-- tx:method:給具體的方法配置事務屬性,可以有多個,分別給不同的方法設置事務
                name:方法名稱
                      1. 完整的方法名稱,不帶包和類
                      2. 方法可以使用通配符,*表示任意字元
                propagation:傳播行為
                isolation:隔離級別
                rollback-for:你指定的異常類名,全限定類名,發生異常一定回滾

            -->
            <tx:method name="buy" propagation="REQUIRED" isolation="DEFAULT"
                       rollback-for="java.lang.NullPointerException , com.md.excep.NotEnoughException"/>



            <!--當方法多的時候,使用通配符-->
            <!--表示add開頭的方法,其他都是默認-->
            <tx:method name="add*" />

            <tx:method name="modify*"/>


        </tx:attributes>

    </tx:advice>



    <!--3. 配置aop-->

    <aop:config>
        <!--配置切入點表達式:指定那些包中的類,要使用事務
            id:切入點表達式名稱,唯一
            expression:切入點表達式,指定那些類要使用事務,aspectj會創建代理對象
                        這裡寫的表示所有service類中的所有方法
        -->
        <aop:pointcut id="servicePt" expression="execution(* *..service..*.*(..))"/>

        <!--配置增強器:關聯advice和pointcut
            advice-ref:通知,上面tx:advice裡面的配置
            pointcut-ref:切入點表達式的id
         -->
        <aop:advisor advice-ref="myAdvice" pointcut-ref="servicePt"/>
    </aop:config>




</beans>

6. 測試類

@Test
    public void test01(){
        String config = "applicationContext.xml";

        ApplicationContext ac = new ClassPathXmlApplicationContext(config);

        // 從容器中獲取service,你聲明service時候的id
        BuyGoodsService buyGoodsService = (BuyGoodsService) ac.getBean("buyGoodsService");
        buyGoodsService.buy(1001,10);

    }

五、總結

1. Spring 的事務註解管理事務

適合中小項目使用的註解方案

spring框架自己用aop實現給業務方法增加事務的功能, 使用@Transactional註解增加事務。
@Transactional註解是spring框架自己註解,放在public方法的上面,表示當前方法具有事務。
可以給註解的屬性賦值,表示具體的隔離級別,傳播行為,異常資訊等等

使用@Transactional的步驟:

  1. 需要聲明事務管理器對象

    • <bean id=”xx” class=”DataSourceTransactionManager”>
  2. 開啟事務註解驅動, 告訴spring框架,我要使用註解的方式管理事務

    • spring使用aop機制,創建@Transactional所在的類代理對象,給方法加入事務的功能
    • spring給業務方法加入事務:在你的業務方法執行之前,先開啟事務,在業務方法之後提交或回滾事務,使用aop的環繞通知
    @Around("你要增加的事務功能的業務方法名稱")
    		 Object myAround(){
               開啟事務,spring給你開啟
    			  try{
    			     buy(1001,10);
    				  spring的事務管理器.commit();
    			  }catch(Exception e){
                 spring的事務管理器.rollback();
    			  }
    			 
    		 }
    
  3. 在你的方法的上面加入@Trancational

2. AspectJ 的 AOP 配置管理事務

適合大型項目,有很多的類,方法,需要大量的配置事務,使用aspectj框架功能,在spring配置文件中聲明類,方法需要的事務

這種方式業務方法和事務配置完全分離

實現步驟: 都是在xml配置文件中實現

  1. 要使用的是aspectj框架,需要加入依賴

    <dependency>
    		<groupId>org.springframework</groupId>
    		<artifactId>spring-aspects</artifactId>
    		<version>5.2.5.RELEASE</version>
    	</dependency>
    
  2. 聲明事務管理器對象

    <bean id=”xx” class=”DataSourceTransactionManager”>

  3. 聲明方法需要的事務類型(配置方法的事務屬性【隔離級別,傳播行為,超時】)

  4. 配置aop:指定哪些哪類要創建代理

Tags: