文章目录:
1.4 事务定义接口 TransactionDefinition
2.3 加入项目需要使用到的Maven依赖(pom.xml)
2.5.1 商品实体类Goods对应的dao接口和mapper文件
2.5.2 销售记录实体类Sale对应的dao接口和mapper文件
2.8.1 Spring使用注解(@Transactional)控制事务
2.9.1 引入外部属性配置文件(jdbc.properties)
2.9.2 声明数据源、SqlSessionFactory对象、读取mapper文件
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&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框架控制事务
使用这种方法控制事务的步骤如下:
- 在pom.xml中加入 spring-aspects 依赖
- 在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注解
- Spring框架自己提供的事务控制。
- 适合中小型项目。
- 使用方便,效率高。
二、使用AspectJ框架
- 缺点:理解难,配置复杂。
- 优点:代码和事务配置是分开的。控制事务,其源代码不用修改。能快速的了解和掌控项目的全部事务,适合大型项目。