Spring——Spring中的事务使用注解(@Transactional)控制事务使用AspectJ框架控制事务

Posted 张起灵-小哥

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Spring——Spring中的事务使用注解(@Transactional)控制事务使用AspectJ框架控制事务相关的知识,希望对你有一定的参考价值。

文章目录:

1.事务的概念

1.1 Spring中的事务 

1.2 Spring的事务管理器 

1.3 关于事务的提交和回滚 

1.4 事务定义接口 TransactionDefinition

1.4.1 事务隔离级别 

1.4.2 事务传播行为 

1.4.3 事务超时时限 

2.框架搭建步骤

2.1 项目的大体框架结构 

2.2 使用Navicat创建两个表 

2.3 加入项目需要使用到的Maven依赖(pom.xml)

2.4 编写实体类(Goods类、Sale类)

2.4.1 Goods商品实体类 

2.4.2 销售记录实体类 

2.5 编写dao接口和对应的mapper映射文件

2.5.1 商品实体类Goods对应的dao接口和mapper文件

2.5.2 销售记录实体类Sale对应的dao接口和mapper文件 

2.6 编写MyBatis主配置文件

2.7 定义异常类(运行时异常)

2.8 定义Service接口和对应的实现类

2.8.1 Spring使用注解(@Transactional)控制事务

2.9 定义Spring配置文件

2.9.1 引入外部属性配置文件(jdbc.properties)

2.9.2 声明数据源、SqlSessionFactory对象、读取mapper文件

2.9.3 声明Service业务层对象

2.9.4 声明事务管理器、开启事务注解驱动 

2.10 定义测试类

2.10.1 测试方法1

2.10.2 测试方法2

2.10.3 测试方法3

4.使用AspectJ框架控制事务

5.两种方式各自的使用特点


1.事务的概念

什么是事务呢?我们都学过mysql,那么事务就是一些 sql 序列的集合,由多条 sql 语句构成,作为一个整体执行。

mysql 执行事务的代码:👇👇👇 

beginTransaction 开启事务
insert into student(...) values(...)
select * from teacher where id=1001
update school set name=XXX where address=...
endTransaction 结束事务

1.1 Spring中的事务 

事务原本是数据库中的概念,在 Dao 层。但一般情况下,需要将事务提升到业务层,即 Service 层。这样做是为了能够使用事务的特性来管理具体的业务。

在 Spring 中通常可以通过以下两种方式来实现对事务的管理:
(1)使用 Spring 的事务注解管理事务
(2)使用 AspectJ 的 AOP 配置管理事务

使用Spring的事务管理器,可以管理不同数据库访问技术的事务处理。开发人员只需要掌握Spring的事务处理方法,就可以实现使用不同的数据库访问技术的事务管理。管理事务面向的是Spring,由Spring来管理事务、做事务提交、事务回滚的操作。

1.2 Spring的事务管理器 

事务管理器是 PlatformTransactionManager 接口对象,定义了事物的操作,其主要用于完成事务的提交commit()、回滚rollback(),及获取事务的状态信息。

这个事务管理器接口有很多实现类,一种数据库访问技术对应有一个实现类,由该实现类完成具体的事务提交、回滚。

例如:1)JDBC、MyBatis的事务管理器的实现类是:DataSourceTransactionManager。

           2)Hibernate框架的事务管理器的实现类是:HibernateTransactionManager。

1.3 关于事务的提交和回滚 

对于这样一个问题:什么时候提交事务?什么时候回滚事务?

当你的业务方法没有异常、正常执行时,事务是提交的。如果你的业务方法抛出了运行时异常,事务是回滚的。

异常的分类:1)Error:严重错误,回滚事务。

                      2)Exception:异常类,可以打印出来的异常情况。

                           ①运行时异常:在程序执行过程中抛出的异常,主要是RuntimeException和它的子类。例如:NullPointerException、NumberFormatEception、ArithmeticException、IndexOutOfBoundsException。

                           ②受查(编译)异常:编写Java代码的时候,产生的异常,若不处理,则无法通过编译。例如:IOException、SQLException、FileNotFoundException。

总的来说:方法中抛出了运行时异常,则事务回滚;其他情况(正常执行或者是受查异常),则事务提交。

1.4 事务定义接口 TransactionDefinition

事务定义接口 TransactionDefinition 中定义了事务描述相关的三类常量:事务隔离级别、事务传播行为、事务默认超时时限,及对它们的操作。 给业务方法说明事务属性,这个与ACID不一样。

1.4.1 事务隔离级别 

这些常量均是以 ISOLATION_开头。即形如 ISOLATION_XXX。

1. DEFAULT : 采 用 DB 默 认 的 事 务 隔 离 级 别 。 MySql 的 默 认 为 REPEATABLE_READ; Oracle 默认为READ_COMMITTED。

2. READ_UNCOMMITTED:读未提交。未解决任何并发问题。

3. READ_COMMITTED:读已提交。解决脏读,存在不可重复读与幻读。

4. REPEATABLE_READ:可重复读。解决脏读、不可重复读,存在幻读。

5. SERIALIZABLE:串行化。不存在并发问题。

1.4.2 事务传播行为 

所谓事务传播行为是指,处于不同事务中的方法在相互调用时,执行期间事务的维护情况。如,A 事务中的方法 doSome()调用 B 事务中的方法 doOther(),在调用执行期间事务的维护情况,就称为事务传播行为。事务传播行为是加在方法上的。

事务传播行为常量都是以 PROPAGATION_ 开头,形如 PROPAGATION_XXX。

1. PROPAGATION_REQUIRED :指定的方法必须在事务内执行。若当前存在事务,就加入到当前事务中;若当前没有事务,则创建一个新事务。这种传播行为是最常见的选择,也是Spring 默认的事务传播行为。

2. PROPAGATION_REQUIRES_NEW :指定的方法支持当前事务,但若当前没有事务,也可以以非事务方式执行。

3. PROPAGATION_SUPPORTS :总是新建一个事务,若当前存在事务,就将当前事务挂起,直到新事务执行完毕。

4. PROPAGATION_MANDATORY

5. PROPAGATION_NESTED

6. PROPAGATION_NEVER

7. PROPAGATION_NOT_SUPPORTED

1.4.3 事务超时时限 

常量 TIMEOUT_DEFAULT 定义了事务底层默认的超时时限,sql 语句的执行时长。注意,事务的超时时限起作用的条件比较多,且超时的时间计算点较复杂。所以,该值一般就使用默认值即可。 

超时时限以秒为单位,是一个整数值,默认值为 -1。


2.框架搭建步骤

2.1 项目的大体框架结构 

2.2 使用Navicat创建两个表 

其中 sale 表存放的是销售记录,id表示销售记录的编号,主键,是自动增长的;gid是购买的商品编号;num是购买的商品数量。初始情况下,sale表中无数据。 

goods表存放是每种商品的具体信息。id是商品编号,主键;name是商品名称;amount是商品库存;price是商品单价。

初始情况下,向goods表中添加两条数据,如下:👇👇👇

2.3 加入项目需要使用到的Maven依赖(pom.xml)

<dependencies>

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

    <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>

    <dependency>
      <groupId>org.mybatis</groupId>
      <artifactId>mybatis</artifactId>
      <version>3.5.1</version>
    </dependency>

    <dependency>
      <groupId>org.mybatis</groupId>
      <artifactId>mybatis-spring</artifactId>
      <version>1.3.1</version>
    </dependency>

    <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>

    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.11</version>
      <scope>test</scope>
    </dependency>

  </dependencies>

  <build>
    <resources>
      <resource>
        <directory>src/main/java</directory><!--所在的目录-->
        <includes><!--包括目录下的.properties,.xml 文件都会扫描到-->
          <include>**/*.properties</include>
          <include>**/*.xml</include>
        </includes>
        <filtering>false</filtering>
      </resource>
    </resources>
  </build>

2.4 编写实体类(Goods类、Sale类)

2.4.1 Goods商品实体类 

package com.bjpowernode.entity;

/**
 *
 */
public class Goods 

    //商品编号,主键
    private Integer id;
    //商品名称
    private String name;
    //商品库存
    private  Integer amount;
    //商品单价
    private Float 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 +
                '';
    

2.4.2 销售记录实体类 

package com.bjpowernode.entity;

/**
 *
 */
public class Sale 

    //主键
    private Integer id;
    //购买商品的id
    private Integer gid;
    //购买商品的数量
    private Integer num;

    public Integer getId() 
        return id;
    

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

    public Integer getGid() 
        return gid;
    

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

    public Integer getNum() 
        return num;
    

    public void setNum(Integer num) 
        this.num = num;
    

    @Override
    public String toString() 
        return "Sale" +
                "id=" + id +
                ", gid=" + gid +
                ", num=" + num +
                '';
    

2.5 编写dao接口和对应的mapper映射文件

2.5.1 商品实体类Goods对应的dao接口和mapper文件

package com.bjpowernode.dao;

import com.bjpowernode.entity.Goods;

/**
 *
 */
public interface GoodsDao 

    //查询某个id的商品信息,返回的是Goods对象(商品类)
    Goods selectById(Integer id);

    //参数goods表示本次购买的商品id和amount购买数量
    int updateGoods(Goods goods);

<?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.bjpowernode.dao.GoodsDao">
    <!-- 使用insert、update、delete、select标签编写sql语句 -->
    <select id="selectById" resultType="com.bjpowernode.entity.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>

2.5.2 销售记录实体类Sale对应的dao接口和mapper文件 

package com.bjpowernode.dao;

import com.bjpowernode.entity.Sale;

/**
 *
 */
public interface SaleDao 

    int insertSale(Sale sale);

<?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.bjpowernode.dao.SaleDao">
    <!-- 使用insert、update、delete、select标签编写sql语句 -->
    <insert id="insertSale">
        insert into sale(gid,num) values(#gid,#num)
    </insert>

</mapper>

2.6 编写MyBatis主配置文件

<?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>
        <setting name="logImpl" value="STDOUT_LOGGING"/>
    </settings>

    <mappers>
<!--        <mapper resource=""/>-->
        <package name="com.bjpowernode.dao"/>

    </mappers>
</configuration>

2.7 定义异常类(运行时异常)

这个异常类主要用来处理购买商品时,库存、商品是否存在这些情况下,所发生的异常信息。 

package com.bjpowernode.excetion;

/**
 *  运行时异常
 */
public class NotEnoughException extends RuntimeException 

    public NotEnoughException() 
        super();
    

    public NotEnoughException(String message) 
        super(message);
    

2.8 定义Service接口和对应的实现类

package com.bjpowernode.service;

/**
 *
 */
public interface BuyGoodsService 

    /**
     * @param goodsId: 购买的商品id
     * @param num: 购买的商品数量
     */
    void buy(Integer goodsId,Integer num);

2.8.1 Spring使用注解(@Transactional)控制事务

@Transactional 的所有可选属性如下所示:

1. propagation:用于设置事务传播属性。该属性类型为 Propagation 枚举,默认值为 Propagation.REQUIRED。

2. isolation:用于设置事务的隔离级别。该属性类型为 Isolation 枚举,默认值为 Isolation.DEFAULT。

3. readOnly:用于设置该方法对数据库的操作是否是只读的。该属性为 boolean,默认值为 false。

4. timeout:用于设置本操作与数据库连接的超时时限。单位为秒,类型为 int,默认值为-1,即没有时限。

5. rollbackFor:指定需要回滚的异常类。类型为 Class[],默认值为空数组。当然,若只有一个异常类时,可以不使用数组。

6. rollbackForClassName:指定需要回滚的异常类类名。类型为 String[],默认值为空数组。当然,若只有一个异常类时,可以不使用数组。

7. noRollbackFor:指定不需要回滚的异常类。类型为 Class[],默认值为空数组。当然,若只有一个异常类时,可以不使用数组。

8.noRollbackForClassName:指定不需要回滚的异常类类名。类型为 String[],默认值为空数组。当然,若只有一个异常类时,可以不使用数组。 

需要注意的是,@Transactional 若用在方法上,只能用于 public 方法上。对于其他非 public 方法,如果加上了注解@Transactional,虽然 Spring 不会报错,但不会将指定事务织入到该方法中。因为 Spring 会忽略掉所有非public 方法上的@Transaction 注解。若@Transaction 注解在类上,则表示该类上所有的方法均将在执行时织入事务。

package com.bjpowernode.service.impl;

import com.bjpowernode.dao.GoodsDao;
import com.bjpowernode.dao.SaleDao;
import com.bjpowernode.entity.Goods;
import com.bjpowernode.entity.Sale;
import com.bjpowernode.excetion.NotEnoughException;
import com.bjpowernode.service.BuyGoodsService;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

/**
 *
 */
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;
    

    /**
     *  @Transactional 注解放在public业务方法的上面,表示方法有事务功能
     *  propagation = Propagation.REQUIRED: 设置事务的传播属性,默认值
     *  isolation = Isolation.DEFAULT: 设置事物的隔离级别,默认值
     *  readOnly = false: 对数据库的操作不是只读的
     *  timeout = 20: 超时时限设置为20s
     *  rollbackFor = NullPointerException.class,NotEnoughException.class): //两个异常发生任何一个,则执行回滚操作
     *  框架首先检查方法抛出的异常是不是在rollback的数组中,如果在,一定执行回滚。
     *  如果不在,框架会继续检查抛出的异常是不是RuntimeException,如果是,仍然执行回滚。
     */
    @Transactional(propagation = Propagation.REQUIRED,
                    isolation = Isolation.DEFAULT,
                    readOnly = false,
                    timeout = 20,
                    rollbackFor = NullPointerException.class,NotEnoughException.class)
    @Override
    public void buy(Integer goodsId, Integer num) 
        System.out.println("=====buy方法的开始======");

        //生成销售记录
        Sale sale=new Sale();
        sale.setGid(goodsId);
        sale.setNum(num);
        saleDao.insertSale(sale);

        //查询商品
        Goods goods=goodsDao.selectById(goodsId);
        if (goods == null) 
            throw new NullPointerException(goodsId + "商品不存在。");
        else if (goods.getAmount() < num) 
            throw new NotEnoughException(goodsId + "商品库存不足。");
        

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

        System.out.println("=====buy方法的完成======");
    

2.9 定义Spring配置文件

2.9.1 引入外部属性配置文件(jdbc.properties)

jdbc.url=jdbc:mysql://localhost:3306/ssm?useUnicode=true&amp;characterEncoding=utf-8
jdbc.username=root
jdbc.password=12345678
<context:property-placeholder location="classpath:jdbc.properties" />

2.9.2 声明数据源、SqlSessionFactory对象、读取mapper文件

<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" />
</bean>

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

<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
    <property name="sqlSessionFactoryBeanName" value="factory" />
    <property name="basePackage" value="com.bjpowernode.dao" />
</bean>

2.9.3 声明Service业务层对象

<bean id="buyGoodsService" class="com.bjpowernode.service.impl.BuyGoodsServiceImpl">
    <property name="goodsDao" ref="goodsDao" />
    <property name="saleDao" ref="saleDao" />
</bean>

2.9.4 声明事务管理器、开启事务注解驱动 

<!-- 声明事务的控制 -->
<!-- 声明事务管理器 -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <!-- 指定数据源DataSource -->
    <property name="dataSource" ref="myDataSource" />
</bean>

<!--
    开启事务注解驱动,告诉框架使用注解管理事务
    transaction-manager: 指定事务管理器的id
-->
<tx:annotation-driven transaction-manager="transactionManager" />

2.10 定义测试类

2.10.1 测试方法1

service.buy(1001,10);表示我们此时要购买1001号商品10台。 

@Test
public void test01() 
    ApplicationContext ctx=new ClassPathXmlApplicationContext("applicationContext.xml");

    BuyGoodsService service= (BuyGoodsService) ctx.getBean("buyGoodsService");
    service.buy(1001,10);

这里的 id 等于5,是因为在这之前,我做了几次代码测试,sale表中的主键(销售记录编号)是自增的,所以这里是5。

后面的 gid=1001,num=10,表示编号为1001的商品,卖出了10台。

2.10.2 测试方法2

service.buy(1002,200);表示我们此时要购买1002号商品200台。(而数据库中1002号商品总库存只有50台,所以这里会发生异常)

@Test
public void test02() 
    ApplicationContext ctx=new ClassPathXmlApplicationContext("applicationContext.xml");

    BuyGoodsService service= (BuyGoodsService) ctx.getBean("buyGoodsService");
    service.buy(1002,200);

这里产生的异常是我们自定义的 NotEnoughException 异常。所以事务一定执行回滚。

2.10.3 测试方法3

service.buy(1005,20);表示我们此时要购买1005号商品20台。(而数据库中并不存在1005号商品,所以这里会发生异常)

@Test
public void test03() 
    ApplicationContext ctx=new ClassPathXmlApplicationContext("applicationContext.xml");

    BuyGoodsService service= (BuyGoodsService) ctx.getBean("buyGoodsService");
    service.buy(1005,20);

这里产生的异常是Java中Exception类下的 NullPointerException 异常。所以事务一定执行回滚。


4.使用AspectJ框架控制事务

使用这种方法控制事务的步骤如下:

  1. 在pom.xml中加入 spring-aspects 依赖
  2. 在spring配置文件中声明事务的内容:①声明事务管理器;②声明业务方法需要的事务属性;③声明切入点表达式。

与上面使用 注解(@Transactional)相比,大部分代码都是一样的。只有Spring的配置文件中有所改动。

将第一种方法中spring配置文件中的 声明事务注解驱动 删掉,换成下面的代码就可以了。

    <!--
        声明业务方法的事务属性(隔离级别、传播行为、超时时限)
        id: 给业务方法配置事务段代码起个名称,唯一值
        transaction-manager: 事务管理器的id
    -->
    <tx:advice id="serviceAdvice" transaction-manager="transactionManager">
        <!-- 给具体的业务方法增加事务的说明 -->
        <tx:attributes>
            <!-- name的值: 1)业务方法的名称 2)带有通配符* -->
            <tx:method name="buy"
                       propagation="REQUIRED"
                       isolation="DEFAULT"
                       timeout="20"
                       read-only="false"
                       rollback-for="java.lang.NullPointerException,com.bjpowernode.excetion.NotEnoughException"/>
        </tx:attributes>
    </tx:advice>

    <!-- 声明切入点表达式,表示哪些包中的类、方法需要添加事务 -->
    <aop:config>
        <!--
            id: 切入点表达式的名称,唯一值
            expression: 切入点表达式
        -->
        <aop:pointcut id="servicePointcut" expression="execution(* *..service..*.*(..))" />
        <aop:advisor advice-ref="serviceAdvice" pointcut-ref="servicePointcut" />
    </aop:config>

5.两种方式各自的使用特点

一、使用@Transactional注解

  1. Spring框架自己提供的事务控制。
  2. 适合中小型项目。
  3. 使用方便,效率高。

二、使用AspectJ框架

  1. 缺点:理解难,配置复杂。
  2. 优点:代码和事务配置是分开的。控制事务,其源代码不用修改。能快速的了解和掌控项目的全部事务,适合大型项目。

以上是关于Spring——Spring中的事务使用注解(@Transactional)控制事务使用AspectJ框架控制事务的主要内容,如果未能解决你的问题,请参考以下文章

项目中Spring事务失效的场景问题排查

在spring3 中如果使用了事务注解,那么如何调用生成的AopProxy代理对象中被代理对象的业务方法?

Spring 使用注解方式进行事务管理

模拟Spring事务注解

@Transactional的参数意义及使用。spring中事务注解的配置情况

spring中事务配置