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="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://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"
        "http://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"
        "http://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"
        "http://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="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://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="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context" xmlns:tx="http://www.springframework.org/schema/tx"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/tx http://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="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://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="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context" 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/context https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd http://www.springframework.org/schema/aop https://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:指定哪些哪类要创建代理

posted @ 2020-08-13 22:17  山丘i  阅读(619)  评论(0编辑  收藏  举报