八、Spring事务(注解方案)
声明式事务:AOP
编程式事务:需要在代码中进行事务管理
1、Spring事务处理模型
*使用步骤是固定的,只要把事务的使用信息提供给Spring就完事了。
- 事务内部提交、回滚事务,使用事务管理器对象,代替你完成commit、rolback
- 事务管理器是一个接口和它众多的实现类
- 事务管理器接口:PlatformTransactionManager,定义了事务重要的方法commit、rolback
- 事务管理器接口实现类:Spring把每一种数据库访问技术对应的事务处理类都创建好了
- mybatis访问数据库:DataSourceTransactionManager
- hibernate访问数据库:HibernateTransactionManager
- … …
- 使用方法:告诉Spring要使用哪种数据库访问技术
- 在spring配置文件使用声明需要使用的数据库访问技术对应的事务管理器实现类
- eq:
<bean id="xxx" class="...DataSourceTransactionManager"/>
- eq:
- 在spring配置文件使用声明需要使用的数据库访问技术对应的事务管理器实现类
- 说明需要事务的类型(TransactionDefinition)
- 事务的隔离级别(对应TransactionDefinition下定义的5个以**ISOLATION_**开头的常量)
- DEFAULT:采用DB默认事务隔离级别,Mysql为REPEATABLE_READ,Oracle为READ_COMMITTED
- **READ_UNCOMMITTED:**读未提交。未解决任何并发问题
- **READ_COMMITTED:**读已提交。解决脏读,存在不可重复读和幻读
- **REPEATABLE_READ:**可重复读。解决脏读、不可重复读,存在幻读
- **SERIALIZABLE:**串行化。不存在并发问题
- 事务的超时时间(TIMEOUT_DEFAULT):表示一个事务最长的执行时间,如果超时就回滚,单位:秒(整数值),默认-1不限时
- 事务的传播行为(有7个):控制业务方法是否有事务,有什么样的事务(对应TransactionDefinition下定义的7个以**PROPAGATION_**开头的常量)
- **PROPAGATION_REQUIRED:**指定的方法必须在事务内执行,若存在当前事务则加入,不存在则创建一个新事务(最常见的选择,也是Spring默认的事务传播行为)
- **PROPAGATION_REQUIRES_NEW:**总是新建一个事务,若当前存在事务,就将当前事务挂起,直到新事物执行完毕
- **PROPAGATION_SUPPORTS:**若存在当前事务就加入,不存在就以非事务方式执行
- PROPAGATION_MANDATORY
- PROPAGATION_NESTED
- PROPAGATION_NEVER
- PROPAGATION_NOT_SUPPORTED
- 事务的隔离级别(对应TransactionDefinition下定义的5个以**ISOLATION_**开头的常量)
- 事务提交、回滚时机
- 业务方法正确执行完毕,没有抛出异常,spring自动提交事务
- 业务方法执行过程中抛出了运行时异常(RuntimeException及其子类)或ERROR,spring自动回滚事务
- 业务方法执行过程中抛出了非运行时异常(主要是受查异常),也会提交事务
- 受查异常:写代码时必须要捕获的异常(IOException、SQLException…)
2、Spring事务处理方案
1、注解方案(适合中小型项目使用,本文重点介绍)
- Spring框架自己用AOP实现业务方法增加事务的功能,使用**@Transactional**注解添加事务
- @Transactional注解是Spring框架提供的注解,放在public方法上面,表示当前方法开启了事务
- 可以给@Transactional注解的属性赋值,表示具体的隔离级别、传播行为、异常信息等
2、使用aspectJ框架(适合大型项目)
在Spring配置文件中声明类、方法需要的事务,达到业务方法和事务配置完全分离。一键跳转~
1、@Transactional属性
- **propagation:**事务传播行为。类型为Propagation枚举,默认值为Propagation.REQUIRED
- **isolation:**事务隔离级别。类型为Isolation枚举,默认值为Isolation.DEFAULT
- **readOnly:**设置该方法对数据库的操作是否是只读的。类型为boolean,默认值为false
- **timeout:**设置本操作与数据库连接的超时时限。单位为秒,类型为int,默认值为-1(无时限)
- **rollbackFor:**设置需要回滚的异常类。类型为Class[],默认值为空数组(只有一个异常类时可不用数组)
- **rollbackForClassName:**设置需要回滚的异常类名字。类型为String[],默认值为空数组(只有一个异常类名字时可不用数组)
- **notRollbackFor:**设置不需要回滚的异常类。类型为Class[],默认值为空数组(只有一个异常类时可不用数组)
- **notRollbackForClassName:**设置不需要回滚的异常类名字。类型为String[],默认值为空数组(只有一个异常类名字时可不用数组)
2、@Transactional的使用步骤
-
声明事务管理器对象
<bean id="xxx" class="...DataSourceTransactionManager" />
-
开启事务注解驱动
-
告诉spring框架爹要用注解的方式管理事务
-
spring使用AOP机制,创建@Transactional所标注类的代理对象,给方法加入事务功能
-
spring给业务方法加入事务:使用AOP环绕通知,运行方法前开启事务,运行方法后提交或回滚事务
-
@Around(value = "execution(..)") Object myAround(){ // TODO: Spring开启事务 try{ // TODO: 执行业务方法 // TODO: Spring提交事务 } catch(Exception e){ // TODO: Spring回滚事务 } }
-
-
-
在业务方法上加@Transactional注解
3、实现步骤
- 创建maven项目
- 加入maven依赖
- spring依赖
- mybatis依赖
- mysql驱动
- spring的事务依赖
- mybatis和spring集成依赖(mybatis官方提供的,用来在spring项目中创建mybatis的SqlSessionFactory、dao对象的)
- 创建实体类
- 创建dao接口及mapper文件
- 创建mybatis主配置文件
- 创建Service接口及实现类,属性包含dao
- 创建spring配置文件(声明mybatis对象交给spring创建)
- 数据源(dataSource)
- SqlSessionFactory
- Dao对象
- 声明自定义的service
- 创建测试类,获取service对象,通过service对象完成对数据库的访问
4、项目结构
*MySql-Table-goods
*MySql-Table-sale
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>hom.wang</groupId>
<artifactId>st-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>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.3.18</version>
</dependency>
<!-- spring事务 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<version>5.3.18</version>
</dependency>
<!-- jdbc -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.3.18</version>
</dependency>
<!-- mybatis依赖 -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.7</version>
</dependency>
<!-- mybatis和spring集成 -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>2.0.6</version>
</dependency>
<!-- mysql驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.29</version>
</dependency>
<!-- ali的数据库连接池 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.6</version>
</dependency>
</dependencies>
<build>
<!--目的是把src/main/java目录下的.xml文件包含到输出结果中(输出到classes目录下)
[※※※ 个人配置此项并未生效...完全扫描不到... ※※※]-->
<resources>
<resource>
<!--所在目录-->
<directory>src/main/java</directory>
<!--包括目录下的.properties/.xml文件都会扫描到-->
<includes>
<include>**/*.xml</include>
<!--<include>**/*.properties</include>-->
</includes>
<filtering>false</filtering>
</resource>
</resources>
<!--指定jdk版本-->
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
</plugins>
</build>
</project>
data.Goods.java
package hom.wang.data;
import java.math.BigDecimal;
public class Goods {
private String goodsId;
private String goodsName;
private Integer goodsAmount;
private BigDecimal goodsPrice;
}
data.Sale.java
package hom.wang.data;
public class Sale {
private Integer saleId;
private String goodsId;
private Integer saleNum;
}
dao.Goods.java
package hom.wang.dao;
import hom.wang.data.Goods;
public interface GoodsDao {
int updateGoods(Goods goods);
Goods selectGoodsById(Goods goods);
}
dao.GoodsMapper.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="hom.wang.dao.GoodsDao">
<update id="updateGoods">
update goods
set goods_amount = #{goodsAmount}
where goods_id = #{goodsId}
</update>
<resultMap id="goodsMap" type="hom.wang.data.Goods">
<id property="goodsId" column="goods_id" />
<result property="goodsName" column="goods_name" />
<result property="goodsAmount" column="goods_amount" />
<result property="goodsPrice" column="goods_price" />
</resultMap>
<select id="selectGoodsById" resultMap="goodsMap">
select
goods_id,
goods_name,
goods_amount,
goods_price
from goods
where goods_id = #{goodsId}
</select>
</mapper>
dao.SaleDao.java
package hom.wang.dao;
import hom.wang.data.Sale;
public interface SaleDao {
int insertSale(Sale sale);
}
dao.SaleMapper.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="hom.wang.dao.SaleDao">
<insert id="insertSale">
insert into sale(goods_id, sale_num) values(#{goodsId}, #{saleNum})
</insert>
</mapper>
exce.NotEnoughException.java
package hom.wang.exce;
/* 库存不足异常 */
public class NotEnoughException extends RuntimeException {
public NotEnoughException() {
}
public NotEnoughException(String message) {
super(message);
}
}
service.impl.BuyServiceImpl.java
package hom.wang.service.impl;
import hom.wang.dao.GoodsDao;
import hom.wang.dao.SaleDao;
import hom.wang.data.Goods;
import hom.wang.data.Sale;
import hom.wang.exce.NotEnoughException;
import hom.wang.service.BuyService;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
public class BuyServiceImpl implements BuyService {
GoodsDao goodsDao;
SaleDao saleDao;
/**
* rollbackFor:发生指定异常一定回滚
* spring处理逻辑:
* 1、首先判断抛出异常是否是rollbackFor包含的,是就回滚
* 2、不在rollbackFor数组中则判断是否是RuntimeException,是就回滚
*/
/* @Transactional(
propagation = Propagation.REQUIRED,
isolation = Isolation.DEFAULT,
readOnly = false,
rollbackFor = {
NullPointerException.class,
NotEnoughException.class
}
)*/
// 上面配置的都是默认值,故而直接使用@Transactional就可以
@Transactional
@Override
public void buy(Sale sale) {
System.out.println("============ buy start =============");
// 1、判断库存是否充足
Goods goods = new Goods();
goods.setGoodsId(sale.getGoodsId());
goods = goodsDao.selectGoodsById(goods);
if(null == goods){
throw new NullPointerException("编号[" + sale.getGoodsId() + "]商品不存在!");
}
if(goods.getGoodsAmount() < sale.getSaleNum()){
throw new NotEnoughException("库存不足!");
}
// 2、减少库存
goods.setGoodsAmount(goods.getGoodsAmount() - sale.getSaleNum());
goodsDao.updateGoods(goods);
// 3、增加销售记录
saleDao.insertSale(sale);
System.out.println("============ buy end =============");
}
public void setGoodsDao(GoodsDao goodsDao) {
this.goodsDao = goodsDao;
}
public void setSaleDao(SaleDao saleDao) {
this.saleDao = saleDao;
}
}
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>
<!--设置别名-->
<typeAliases>
<!--name:实体类所在包名-->
<package name="hom.wang.data"/>
</typeAliases>
<mappers>
<!-- 配置文件导入全部写在这里 -->
<mapper resource="hom/wang/dao/GoodsMapper.xml"/>
<mapper resource="hom/wang/dao/SaleMapper.xml"/>
<!--name:包名(这个包所有.xml文件一次性全部加载)-->
<!--<package name="hom.wang.dao"/>-->
</mappers>
</configuration>
jdbc.properties
jdbc.url=jdbc:mysql://127.0.0.1:3306/ms_user
jdbc.username=root
jdbc.password=root
jdbc.maxActive=20
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"></context:property-placeholder>
<!--声明数据源DataSource,用于连接数据库-->
<bean id="myDataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
<!--使用属性配置文件中的数据,语法:${key}-->
<property name="url" value="${jdbc.url}"></property>
<property name="username" value="${jdbc.username}"></property>
<property name="password" value="${jdbc.password}"></property>
<property name="maxActive" value="${jdbc.maxActive}"></property>
</bean>
<!--声明mybatis提供的SqlSessionFactoryBean,用于创建SqlSessionFactory-->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="myDataSource"></property>
<property name="configLocation" value="classpath:mybatis.xml"></property><!--在spring配置文件中指定其他配置文件路径需要使用 classpath:-->
</bean>
<!--创建dao对象,使用SqlSession的getMapper(UserDao.class)
MapperScannerConfigurer:在内部调用getMapper()生成每个dao接口的代理对象
-->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<!--指定SqlSessionFactory的id-->
<property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"></property>
<!--指定包名(dao所在的包名)
MapperScannerConfigurer会扫描这个包中的所有接口,
把每个接口都执行一次 getMapper()方法,得到每个接口的dao对象。
创建好的dao对象放入到 spring容器中
-->
<property name="basePackage" value="hom.wang.dao"></property>
</bean>
<bean id="buyService" class="hom.wang.service.impl.BuyServiceImpl">
<property name="goodsDao" ref="goodsDao"></property>
<property name="saleDao" ref="saleDao"></property>
</bean>
<!--使用Spring事务管理-->
<!--1、声明事务管理器-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<!--连接的数据库,指定数据源-->
<property name="dataSource" ref="myDataSource" />
</bean>
<!--2、开启事务注解驱动,告诉Spring使用注解管理事务,创建代理对象-->
<!--springframework.org/schema/tx/spring-tx
transaction-manager:配置事务管理器对象
-->
<tx:annotation-driven transaction-manager="transactionManager" />
</beans>
MyTest.java
package hom.wang;
import hom.wang.data.Sale;
import hom.wang.service.BuyService;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class MyTest {
@Test
public void test(){
ApplicationContext ac =
new ClassPathXmlApplicationContext("applicationContext.xml");
BuyService buyService = (BuyService) ac.getBean("buyService");
//buyService的代理对象是:com.sun.proxy.$Proxy17,jdk动态代理对象
System.out.println("buyService的代理对象是:" + buyService.getClass().getName());
Sale sale = new Sale();
sale.setGoodsId("p_yz_00002");
sale.setSaleNum(1);
buyService.buy(sale);
}
}
*没错,最终结果正如你所预料的!