一.声明式事务
1.1 概述
注意:spring 声明式事务的底层就是AOP
1.2 配置ioc的标签
1.3 配置事务的流程
1、配置事务管理器
2、配置事务的通知
此时我们需要导入事务的约束 tx名称空间和约束,同时也需要aop的
使用tx:advice标签配置事务通知
属性:
id:给事务通知起一个唯一标识
transaction-manager:给事务通知提供一个事务管理器引用
3、配置AOP中的通用切入点表达式
4、建立事务通知和切入点表达式的对应关系
5、配置事务的属性
是在事务的通知tx:advice标签的内部
二.声明式事务的案例
2.1 案例背景描述:
数据表:
北京,上海的钱均为500 ,现在实现北京向上海进行转账50,正确的结果为北京450,上海550,
但是现在没有进行事务管理,在北京向上海进行转账,实现北京进行了减款操作,但是在上海进行加钱操作之前,出现了报错,后面代码无法执行,造成北京450,上海还是500
需要进行事务管理,出现异常进行回滚。
2.2 项目搭建
2.3 pom文件依赖的建立
<!-- log4j的日志-->
<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.0.2.RELEASE</version>
</dependency>
<!-- spring-jdbc -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.0.2.RELEASE</version>
</dependency>
<!-- spring的事务管理-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<version>5.0.2.RELEASE</version>
</dependency>
<!-- mysql的驱动包-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.6</version>
</dependency>
2.4 编写dao层
1.接口层
package com.ljf.spring.tx.xml.dao;
import com.ljf.spring.tx.xml.domain.Account;
public interface AccountDao {
/**
* 根据名称查询账户
* @param accountName
* @return
*/
Account findAccountByName(String accountName);
/**
* 更新账户
* @param account
*/
void updateAccount(Account account);
}
2.实现层
package com.ljf.spring.tx.xml.dao.impl;
import com.ljf.spring.tx.xml.dao.AccountDao;
import com.ljf.spring.tx.xml.domain.Account;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.support.JdbcDaoSupport;
import java.util.List;
/**
* @ClassName: AccountDaoImpl
* @Description: TODO
* @Author: liujianfu
* @Date: 2021/02/22 18:02:28
* @Version: V1.0
**/
public class AccountDaoImpl extends JdbcDaoSupport implements AccountDao {
@Override
public Account findAccountByName(String accountName) {
List<Account> accounts = super.getJdbcTemplate().query("select id id ,account_name accountName,money money from tb_account where account_name = ?",new BeanPropertyRowMapper<Account>(Account.class),accountName);
if(accounts.isEmpty()){
return null;
}
if(accounts.size()>1){
throw new RuntimeException("结果集不唯一");
}
return accounts.get(0);
}
@Override
public void updateAccount(Account account) {
super.getJdbcTemplate().update("update tb_account set account_name=?,money=? where id=?",account.getAccountName(),account.getMoney(),account.getId());
}
}
2.5 编写service层
1.编写接口
package com.ljf.spring.tx.xml.service;
public interface AccountService {
/**
* 转账
* @param sourceName 转成账户名称
* @param targetName 转入账户名称
* @param money 转账金额
*/
public void transferMoney(String sourceName, String targetName, Double money);
}
2.编写实现层
package com.ljf.spring.tx.xml.service.impl;
import com.ljf.spring.tx.xml.dao.AccountDao;
import com.ljf.spring.tx.xml.domain.Account;
import com.ljf.spring.tx.xml.service.AccountService;
/**
* @ClassName: AccountServiceImpl
* @Description: TODO
* @Author: liujianfu
* @Date: 2021/02/22 17:52:56
* @Version: V1.0
**/
public class AccountServiceImpl implements AccountService {
//spring的ioc 通过setter方法注入,属性名为actDao和spring-beans.xml中的标签名的id对应
private AccountDao actDao;
public AccountDao getActDao() {
return actDao;
}
public void setActDao(AccountDao actDao) {
this.actDao = actDao;
}
/** 北京,上海的钱均为500 ,现在实现北京向上海进行转账50,正确的结果为北京450,上海550,
* 但是现在没有进行事务管理,在北京向上海进行转账,实现北京进行了减款操作,但是在上海进行加钱操作之前,出现了报错,后面代码无法执行,
* 造成北京450,上海还是500
* 转账
* @param sourceName 转成账户名称
* @param targetName 转入账户名称
* @param money 转账金额
*/
@Override
public void transferMoney(String sourceName, String targetName, Double money) {
System.out.println("正在实现转账功能 transfer....");
//2.1根据名称查询转出账户
Account source = actDao.findAccountByName(sourceName);
//2.2根据名称查询转入账户
Account target = actDao.findAccountByName(targetName);
//2.3转出账户减钱
System.out.println(":"+(source.getMoney()-money));
source.setMoney(source.getMoney()-money);
//2.4转入账户加钱
target.setMoney(target.getMoney()+money);
//2.5更新转出账户
actDao.updateAccount(source);
int i=1/0;
//2.6更新转入账户
actDao.updateAccount(target);
}
}
2.6 编写实体类
package com.ljf.spring.tx.xml.domain;
import java.io.Serializable;
/**
* @ClassName: Account
* @Description: TODO
* @Author: liujianfu
* @Date: 2021/02/19 23:21:27
* @Version: V1.0
**/
public class Account implements Serializable {
private Integer id;
private String accountName;
private Double money;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getAccountName() {
return accountName;
}
public void setAccountName(String accountName) {
this.accountName = accountName;
}
public Double getMoney() {
return money;
}
public void setMoney(Double money) {
this.money = money;
}
@Override
public String toString() {
return "Account{" +
"id=" + id +
", accountName='" + accountName + '\'' +
", money=" + money +
'}';
}
}
2.7 编写resource下的文件
1.jdbc.properteis
jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/nongda
jdbc.user=root
jdbc.password=
2.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:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx"
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/tx
http://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<context:property-placeholder location="classpath:spring-jdbc.properties"></context:property-placeholder>
<!-- 1.配置数据源-->
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName" value="${jdbc.driver}"></property>
<property name="url" value="${jdbc.url}"></property>
<property name="username" value="${jdbc.user}"></property>
<property name="password" value="${jdbc.password}"></property>
</bean>
<!-- 2.配置账户的持久层-->
<bean id="accountDao" class="com.ljf.spring.tx.xml.dao.impl.AccountDaoImpl">
<property name="dataSource" ref="dataSource"></property>
</bean>
<!-- 3.配置业务层-->
<bean id="acService" class="com.ljf.spring.tx.xml.service.impl.AccountServiceImpl">
<property name="actDao" ref="accountDao"></property>
</bean>
</beans>
2.8 调用
1.调用类
package com.ljf.spring.tx.xml;
import com.ljf.spring.tx.xml.service.AccountService;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
/**
* Hello world!
*
*/
public class App
{
public static void main( String[] args )
{
System.out.println( "Hello World!" );
//1.获取核心容器对象
ApplicationContext ac = new ClassPathXmlApplicationContext("applicationContext.xml");
//2.根据id获取Bean对象
AccountService as = (AccountService)ac.getBean("acService");//获取配置文件的bean标签的id,
as.transferMoney("北京","上海",50.0);
System.out.println(as);
}
}
2.执行结果
2.9 事务的配置
2.9.1 在pom添加依赖
<!-- spring 切面包-->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.8.7</version>
</dependency>
2.9.2 配置文件配置事务
<?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:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx"
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/tx
http://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<context:property-placeholder location="classpath:spring-jdbc.properties"></context:property-placeholder>
<!-- 1.配置数据源-->
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName" value="${jdbc.driver}"></property>
<property name="url" value="${jdbc.url}"></property>
<property name="username" value="${jdbc.user}"></property>
<property name="password" value="${jdbc.password}"></property>
</bean>
<!-- 2.配置账户的持久层-->
<bean id="accountDao" class="com.ljf.spring.tx.xml.dao.impl.AccountDaoImpl">
<property name="dataSource" ref="dataSource"></property>
</bean>
<!-- 3.配置业务层-->
<bean id="acService" class="com.ljf.spring.tx.xml.service.impl.AccountServiceImpl">
<property name="actDao" ref="accountDao"></property>
</bean>
<!-- spring中基于XML的声明式事务控制配置步骤
1、配置事务管理器
2、配置事务的通知
此时我们需要导入事务的约束 tx名称空间和约束,同时也需要aop的
使用tx:advice标签配置事务通知
属性:
id:给事务通知起一个唯一标识
transaction-manager:给事务通知提供一个事务管理器引用
3、配置AOP中的通用切入点表达式
4、建立事务通知和切入点表达式的对应关系
5、配置事务的属性
是在事务的通知tx:advice标签的内部
-->
<!-- 配置事务管理器 -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"></property>
</bean>
<!-- 配置事务的通知-->
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<!-- 配置事务的属性
isolation:用于指定事务的隔离级别。默认值是DEFAULT,表示使用数据库的默认隔离级别。
propagation:用于指定事务的传播行为。默认值是REQUIRED,表示一定会有事务,增删改的选择。查询方法可以选择SUPPORTS。
read-only:用于指定事务是否只读。只有查询方法才能设置为true。默认值是false,表示读写。
timeout:用于指定事务的超时时间,默认值是-1,表示永不超时。如果指定了数值,以秒为单位。
rollback-for:用于指定一个异常,当产生该异常时,事务回滚,产生其他异常时,事务不回滚。没有默认值。表示任何异常都回滚。
no-rollback-for:用于指定一个异常,当产生该异常时,事务不回滚,产生其他异常时事务回滚。没有默认值。表示任何异常都不回滚。
-->
<tx:attributes>
<tx:method name="*" propagation="REQUIRED" read-only="false" rollback-for="*"/>
<tx:method name="find*" propagation="SUPPORTS" read-only="true"></tx:method>
</tx:attributes>
</tx:advice>
<!-- 配置aop-->
<aop:config>
<!-- 配置切入点表达式-->
<aop:pointcut id="pt1" expression="execution(* com.ljf.spring.tx.xml.service.impl.*.*(..))"></aop:pointcut>
<!--建立切入点表达式和事务通知的对应关系 -->
<aop:advisor advice-ref="txAdvice" pointcut-ref="pt1"></aop:advisor>
</aop:config>
</beans>
2.9.3 调用
package com.ljf.spring.tx.xml;
import com.ljf.spring.tx.xml.service.AccountService;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
/**
* Hello world!
*
*/
public class App
{
public static void main( String[] args )
{
System.out.println( "Hello World!" );
//1.获取核心容器对象
ApplicationContext ac = new ClassPathXmlApplicationContext("applicationContext.xml");
//2.根据id获取Bean对象
AccountService as = (AccountService)ac.getBean("acService");//获取配置文件的bean标签的id,
as.transferMoney("北京","上海",50.0);
System.out.println(as);
}
}
报错了,刷新数据库,数据依然是500,进行了回滚
三.声明式事务不起作用的原因
在工作中,看过别人写的代码出现了事务不回滚的现象。当然,事务不回滚的都是采用的声明式事务或者是注解事务;编程式事务都是自己写代码手动回滚的,因此是不会出现不回滚的现象。
再说下 声明式 事务和注解事务回滚的原理:当被切面切中或者是加了注解的方法中抛出了RuntimeException异常时,Spring会进行事务回滚。默认 情况下是 捕获到方法的 RuntimeException 异常 ,也就是说抛出只要属于运行时的异常(即 RuntimeException及其子类 )都能回滚;但当抛出一个不属于运行时异常时,事务是不会回滚的。
下面说说我经常见到的3种事务不回滚的产生原因:
(1)声明式事务配置切入点表达式写错了,没切中Service中的方法
(2)Service方法中,把异常给try catch了,但catch里面只是打印了异常信息,没有手动抛出RuntimeException异常
(3)Service方法中,抛出的异常不属于运行时异常(如IO异常),因为Spring默认情况下是捕获到运行时异常就回滚
3.1 在service的实现层加try catch
package com.ljf.spring.tx.xml.service.impl;
import com.ljf.spring.tx.xml.dao.AccountDao;
import com.ljf.spring.tx.xml.domain.Account;
import com.ljf.spring.tx.xml.service.AccountService;
/**
* @ClassName: AccountServiceImpl
* @Description: TODO
* @Author: liujianfu
* @Date: 2021/02/22 17:52:56
* @Version: V1.0
**/
public class AccountServiceImpl implements AccountService {
//spring的ioc 通过setter方法注入,属性名为actDao和spring-beans.xml中的标签名的id对应
private AccountDao actDao;
public AccountDao getActDao() {
return actDao;
}
public void setActDao(AccountDao actDao) {
this.actDao = actDao;
}
/** 北京,上海的钱均为500 ,现在实现北京向上海进行转账50,正确的结果为北京450,上海550,
* 但是现在没有进行事务管理,在北京向上海进行转账,实现北京进行了减款操作,但是在上海进行加钱操作之前,出现了报错,后面代码无法执行,
* 造成北京450,上海还是500
* 转账
* @param sourceName 转成账户名称
* @param targetName 转入账户名称
* @param money 转账金额
*/
@Override
public void transferMoney(String sourceName, String targetName, Double money) {
System.out.println("正在实现转账功能 transfer....");
//2.1根据名称查询转出账户
Account source = actDao.findAccountByName(sourceName);
//2.2根据名称查询转入账户
Account target = actDao.findAccountByName(targetName);
//2.3转出账户减钱
System.out.println(":"+(source.getMoney()-money));
source.setMoney(source.getMoney()-money);
//2.4转入账户加钱
target.setMoney(target.getMoney()+money);
//2.5更新转出账户
actDao.updateAccount(source);
try{
int i=1/0;
}
catch (Exception e){
e.printStackTrace();
throw new RuntimeException();
}
//2.6更新转入账户
actDao.updateAccount(target);
}
}
3.2 调用
没有回滚,北京进行了减50,上海没有加50
3.3 解决办法
在try catch块中抛出运行时异常
package com.ljf.spring.tx.xml.service.impl;
import com.ljf.spring.tx.xml.dao.AccountDao;
import com.ljf.spring.tx.xml.domain.Account;
import com.ljf.spring.tx.xml.service.AccountService;
/**
* @ClassName: AccountServiceImpl
* @Description: TODO
* @Author: liujianfu
* @Date: 2021/02/22 17:52:56
* @Version: V1.0
**/
public class AccountServiceImpl implements AccountService {
//spring的ioc 通过setter方法注入,属性名为actDao和spring-beans.xml中的标签名的id对应
private AccountDao actDao;
public AccountDao getActDao() {
return actDao;
}
public void setActDao(AccountDao actDao) {
this.actDao = actDao;
}
/** 北京,上海的钱均为500 ,现在实现北京向上海进行转账50,正确的结果为北京450,上海550,
* 但是现在没有进行事务管理,在北京向上海进行转账,实现北京进行了减款操作,但是在上海进行加钱操作之前,出现了报错,后面代码无法执行,
* 造成北京450,上海还是500
* 转账
* @param sourceName 转成账户名称
* @param targetName 转入账户名称
* @param money 转账金额
*/
@Override
public void transferMoney(String sourceName, String targetName, Double money) {
System.out.println("正在实现转账功能 transfer....");
//2.1根据名称查询转出账户
Account source = actDao.findAccountByName(sourceName);
//2.2根据名称查询转入账户
Account target = actDao.findAccountByName(targetName);
//2.3转出账户减钱
System.out.println(":"+(source.getMoney()-money));
source.setMoney(source.getMoney()-money);
//2.4转入账户加钱
target.setMoney(target.getMoney()+money);
//2.5更新转出账户
actDao.updateAccount(source);
try{
int i=1/0;
}
catch (Exception e){
e.printStackTrace();
System.out.println("进行了事务的回滚");
throw new RuntimeException();
}
//2.6更新转入账户
actDao.updateAccount(target);
}
}
3.4 再次运行
事务进行了回滚,ok,配置的声明式事务正确,实现了回滚操作