5.2模拟电商示例--第5章Spring事务
Step0:新建数据库和表
电商实例-新建表sale电商实例-新建表goods
Step1:maven依赖pom.xml
<!--jdk属性信息-->
<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依赖-核心ioc-->
<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>8.0.23</version>
</dependency>
<!--阿里公司的数据库连接池-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.12</version>
</dependency>
</dependencies>
<build>
<!--加入maven插件,编译时扫描src/main/java目录中的xml文件-->
<resources>
<resource>
<directory>src/main/java</directory><!--所在的目录-->
<includes><!--包括目录下的.properties、xml文件都会扫描到-->
<include>**/*.properties</include>
<include>**/*.xml</include>
</includes>
<filtering>false</filtering>
</resource>
</resources>
<!--指定jdk的版本-->
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
</plugins>
</build>
Step2:创建实体类
public class Sale {
private Integer id;
private Integer gid;
private Integer nums;
创建set和get方法
}
public class Goods {
private Integer id;
private String name;
private Integer amount;
private Float price;
创建set和get方法
}
Step3:定义dao接口
public interface SaleDao {
//增加销售记录
int insertSale(Sale sale);
}
public interface GoodsDao {
//更行库存
//表示本次用户购买的商品信息,id、购买数量
int updateGoods(Goods goods);
//查询商品的信息
Goods selectGoods(Integer id);
}
Step4:定义dao接口对应的sql映射文件
SaleDao.xml
<!--唯一值的,可以自定义,但要求:使用dao接口的全限定名称。(包括包名、类名)-->
<mapper namespace="com.bjpowernode.dao.SaleDao">
<insert id="insertSale">
insert into sale(gid,nums) values (#{gid},#{nums})
</insert>
</mapper>
GoodsDao.xml
<!--唯一值的,可以自定义,但要求:使用dao接口的全限定名称。(包括包名、类名)-->
<mapper namespace="com.bjpowernode.dao.GoodsDao">
<select id="selectGoods" resultType="com.bjpowernode.domain.Goods">
select id,name,amount,price from goods where id=#{gid}
</select>
<update id="updateGoods">
update goods set amount = amount - #{amount} where id=#{id}
</update>
</mapper>
Step5:定义异常类
package com.bjpowernode.excep;
//自定义的运行时异常,继承RuntimeException
public class NotEnoughException extends RuntimeException{
//Ctrl+O键重写有参和无参的构造方法
public NotEnoughException() {
super();
}
public NotEnoughException(String message) {
super(message);
}
}
Step6:定义Service接口
package com.bjpowernode.service;
public interface BuyGoodsService {
//购买商品的方法,goodsId购买商品的id,nums购买商品的数量
void buy(Integer goodsId,Integer nums);
}
Step7:定义Service的实现类
public class BuyGoodsServiceImpl implements BuyGoodsService {
//因为以下两个功能需要dao的支持,故需要声明dao
private SaleDao saleDao;
private GoodsDao goodsDao;
//需要set方法完成属性赋值
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 buyGoods=new Goods();
buyGoods.setId(goodsID);
buyGoods.setAmount(nums);
goodsDao.updateGoods(buyGoods);
System.out.println("=====buy方法的结束=====");
}
}
Step8:修改spriong配置文件的内容
<!--
把数据库的配置信息,写在一个独立的文件,编译修改数据库的配置内容
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注入给DruidDataSource体哦那个连接数据库信息-->
<!--
使用属性配置文件中的数据,语法:${key}
-->
<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>
<!--声明mybatis中所提供能SqlSessionFactoryBean类,
这个类的内部是创建SqlSessionFactory的-->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<!--set注入,把数据库的连接池赋给dataSource属性-->
<property name="dataSource" ref="myDataSource"/>
<!--value:mybatis主配置文件的位置
configLocation是Resource类型的
-->
<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的容器中。-->
<property name="basePackage" value="com.bjpowernode.dao"/>
</bean>
<!--声明service-->
<bean id="buyService" class="com.bjpowernode.service.impl.BuyGoodsServiceImpl">
<property name="goodsDao" ref="goodsDao"/>
<property name="saleDao" ref="saleDao"/>
</bean>
Step9:定义测试类
public class MyTest {
@Test
public void test01(){
//读取配置文件
String config="applicationContext.xml";
ApplicationContext ctx = new ClassPathXmlApplicationContext(config);
//获取service对象
BuyGoodsService service= (BuyGoodsService) ctx.getBean("buyService");
//调用buy该方法
service.buy(1001,10);
}
}
Step10:aop给已经存在的代码额外增加事务功能
spring框架中提供的事务处理方案
1.适合中小项目使用,注解方案。
spring框架用aop功能实现给业务方法增加事务的功能,使用@Transactional注解增加事务。
@Transactional注解是spring框架的注解,放在public方法的上面,表示当前方法具有事务。可以给注解的属性赋值,表示具体的隔离级别、传播行为、异常信息等。
@Transactional的所有可选属性如下所示:
(1)propagation:用于设置事务传播属性。该属性类型为Propagation枚举,默认值为Propagation.REQUIRED。
(2))isolation:用于设置事务的隔离界别。该属性类型为Isolation枚举,默认值为Isolation.DEFAULT。
(3)readOnly:用于设置该方法对数据库的操作是否是只读的。该属性为boolean,默认值为false。
(4)timeout:用于设置本操作与数据库连接的超时时限。单位为秒,类型为inr,默认值为-1,即没有时限。
(5)rollbackFor:指定需要回滚的异常类。类型为Class[],默认值为空数组。当然,若只有一个异常类时,可以不适用数组。
(6)rollbackForClassName:指定需要回滚的异常类类名。类型为String[],默认值为空数组。
使用@Transactional的步骤:
(1)需要声明事务管理器对象
<bean id="xx" class="DataSourceTransactionManager">
<!--在applicationContext.xml文件-->
<!--使用spring的事务处理-->
<!--1.声明事务管理器-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<!--连接的数据库,指定数据源-->
<property name="dataSource" ref="myDataSource"/>
</bean>
(2)开启事务注解驱动,告诉pring框架使用注解的方式管理事务。
spring使用aop机制,创建@Transactional所在的类代理对象,给方法加入事务的功能。
(使用aop的环绕通知,在业务方法执行前,先开启事务,在业务方法之后提交或回滚事务)
@Around("要增加的事务功能的业务方法名")
object myAround(){
//spring开启事务
try{
buy(1001,10);
spring的事务管理器.commit();
}catch(Exception e){
spring的事务管理器.rollback();
}
}
<!--2.开启事务注解驱动,告诉spring使用注解管理事务,创建代理对象
transaction-manager:事务管理器对象的id
-->
<tx:annotation-driven transaction-manager="transactionManager"/>
开启事务注解驱动
annotation-ariven选错时-删除约束文件即可
(3)在方法的上面加入@Transactional
/*
* rollbackFor:表示发生指定的异常一定回滚
* 处理逻辑:
* 1)spring框架先检查方法抛出的异常是不是在rollbackFor的属性值中,
* 如果异常在rollbackFor列表中,则回滚。
* 2)如果抛出的异常不在rollbackFor列表中,spring会判断异常是不是RuntimeException,
* 如果是,则回滚。
* */
@Transactional(
propagation = Propagation.REQUIRED,
isolation = Isolation.DEFAULT,
readOnly = false,
rollbackFor = {
NullPointerException.class,
}
)
/*
* 工作中常用使用默认值做事务
* */
@Transactional
使用@Transactional注解
2.在大型项目中使用AspectJ的AOP配置管理事务
实现步骤:都是在xml配置文件中实现。
1)使用AspectJ框架,需要加入依赖
<!--aspectJ依赖-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>5.2.5.RELEASE</version>
</dependency>
2)声明事务管理器对象
<bean id="xx" class="DataSourceTransactionManager">
<!--声明式事务处理:和源代码完全分离的-->
<!--1.声明事务管理器对象-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<!--声明数据源-->
<property name="dataSource" ref="myDataSource"/>
</bean>
3)声明方法需要的事务类型(配置方法的事务属性[隔离级别、传播行为、超时])
使用aspectJ开启事务注册驱动advice选错时-删除约束文件即可
4)配置方法的事务属性
<!--2.声明方法需要的事务类型(配置方法的事务属性[隔离级别、传播行为、超时])
id:自定义名称,表示<tx:advice>和</tx:advice>之间的配置内容的
transaction-manager:事务管理器对象的id
-->
<tx:advice id="myAdvice" transaction-manager="transactionManager">
<!-- tx:attributes:配置事务属性 -->
<tx:attributes>
<!-- tx:method:给具体的方法配置事务属性,method可以有多个,分别给不同的方法设置事务属性
name:方法名称,1)完整的方法名称,不带有包和类
2)方法可以使用通配符,*表示任意字符
propagation:传播行为,枚举值
isolation:隔离级别
rollback-for:指定的异常类型,全限定类名,发生异常时一定回滚
-->
<tx:method name="buy" propagation="REQUIRED" isolation="DEFAULT"
rollback-for="java.lang.NullPointerException,com.bjpowernode.excep.NotEnoughException" />
</tx:attributes>
</tx:advice>
tx:method使用通配符指定多个方法
spring框架进行事务匹配的顺序:
先找完全匹配方法名,如上图 <tx:method name="buy"
再找带有通配符的,如上图<tx:method name="add* "
最后找只有通配符的,如上图<tx:method name=" * "
5)配置aop:指定哪些类要创建代理。
<!--配置aop-->
<aop:config>
<!--配置切入点表达式:指定哪些包中类要使用事务
id:切入点表达式的名称,唯一值
expression:切入点表达式,指定哪些类要使用事务,aspectJ会创建代理对象
*..service..*.*(..)表示:任意包及子包下的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>