Talk is cheap , show me your code!
欢迎来到付振南Java博客,让我们一起学习Java吧!

spring之声明式事务控制(基于xml)

今天来给大家讲解一下spring的声明式事务控制。

那么什么是事务控制呢?讲这个之前,我们来想象一下这么一个场景。假设A有1000块钱,B也有1000块钱,A要给B转账100块钱,A转完了之后,在更新完A的账户时,系统发生了错误,也就是说A钱已经扣了,B账户还没更新,也就是钱没到账,结果毫无疑问,A余额还剩900,B余额还是1000,转账的100元不翼而飞,这显然不是我们想要的结果,那么我们该如何解决这个问题呢?所以这就得用到我们的事务控制了!我们用代码来说话。

导入maven坐标:

<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<version>5.0.2.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.0.2.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.0.2.RELEASE</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.11</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.8.7</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>5.0.2.RELEASE</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
</dependencies>

首先我们创建一个domain包,用作创建账户的实体类Account,Account类中有id,name,money三个属性,并且重写toString方法,代码如下:

package cn.fuzhennan.domain;

import java.io.Serializable;

/**
* 账户的实体类
*/
public class Account implements Serializable {
private Integer id;
private String name;
private Float money;

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

public void setName(String name) {
this.name = name;
}

public void setMoney(Float money) {
this.money = money;
}

public Integer getId() {
return id;
}

public String getName() {
return name;
}

public Float getMoney() {
return money;
}

@Override
public String toString() {
return "Account{" +
"id=" + id +
", name='" + name + '\'' +
", money=" + money +
'}';
}
}

然后我们创建一个持久层dao包,写一个账户类接口IAccountDao,里面写一个根据姓名查找账户的findAccountByName方法和一个更新账户的updateAccount方法,代码如下:

package cn.fuzhennan.dao;

import cn.fuzhennan.domain.Account;

/**
 * @author fuzhennan
 * 持久层接口
 */
public interface IAccountDao{
    /**
     * 根据名称查找
     * @return
     */
    Account findAccountByName(String accountName);

    /**
     * 更新账户
     */
    void updateAccount(Account account);
}

然后写一个该接口的实现类AccountDaoImpl,在这里我们就用jdbcTemplate的query方法来写数据库语句,所以我们需要实例化一个JdbcTemplate对象,并且写它的set方法,用来给spring注入,代码如下:

package cn.fuzhennan.dao.impl;

import cn.fuzhennan.dao.IAccountDao;
import cn.fuzhennan.domain.Account;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;

import java.util.List;

/** 账户持久层实现类
 * @author fuzhennan
 */
public class AccountDaoImpl implements IAccountDao {
private JdbcTemplate jdbcTemplate;

public void setJdbcTemplate(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}

@Override
public Account findAccountByName(String accountName) {
List<Account> accounts = jdbcTemplate.query("select * from account where name = ?", new BeanPropertyRowMapper<Account>(Account.class),accountName);
if (accounts.isEmpty()){
return null;
}
if (accounts.size()>1){
System.out.println("结果集不唯一");
}
return accounts.get(0);
}

@Override
public void updateAccount(Account account) {
jdbcTemplate.update("update account set name = ? , money = ? where id = ?",account.getName(),account.getMoney(),account.getId());
}
}

写完了持久层的代码,我们再来写业务实现类代码,新建一个service包,在该包下创建一个业务层接口IAccountService,只需要一个转账的业务方法即可,代码如下:

package cn.fuzhennan.service;

import cn.fuzhennan.domain.Account;

/**
 * 业务层接口
 */
public interface IAccountService {
    /**
     * 转账业务
     * @param sourceName 转出账户
     * @param targetName 转入账户
     * @param money 转账金额
     */
    void transfer(String sourceName,String targetName,Float money);
}

然后写它的实现类AccountServiceImpl,并且实例化一个IAccountDao对象,并且写上它的set方法等着spring注入,并且故意在更新完A的账户后,写一个除数为0的错误代码,完整代码如下:

package cn.fuzhennan.service.impl;

import cn.fuzhennan.dao.IAccountDao;
import cn.fuzhennan.domain.Account;
import cn.fuzhennan.service.IAccountService;

import java.util.List;

/**
 * 账户的业务层实现类
 * 事务控制应该都是在业务层
 * @author fuzhennan
 */
public class AccountServiceImpl implements IAccountService{

    private IAccountDao accountDao;

    public void setAccountDao(IAccountDao accountDao) {
        this.accountDao = accountDao;
    }

    @Override
    public void transfer(String sourceName, String targetName, Float money) {
        System.out.println("transfer....");
        //1.根据名称查询转出账户
        Account source = accountDao.findAccountByName(sourceName);
        //2.根据名称查询转入账户
        Account target = accountDao.findAccountByName(targetName);
        //3.转出账户减钱
        source.setMoney(source.getMoney()-money);
        //4.转入账户加钱
        target.setMoney(target.getMoney()+money);
        //5.更新转出账户
        accountDao.updateAccount(source);

            int i=1/0;

        //6.更新转入账户
        accountDao.updateAccount(target);
    }
}

全部写完之后我们接下来开始配置spring的配置文件,首先引入约束:

<?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"
       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">
</beans>

然后就是spring的依赖注入了,不会依赖注入的看这篇文章。

<!-- 配置业务层 -->
<bean id="accountService" class="cn.fuzhennan.service.impl.AccountServiceImpl">
<!-- 注入dao -->
<property name="accountDao" ref="accountDao"/>
</bean>

<!-- 配置持久层-->
<bean id="accountDao" class="cn.fuzhennan.dao.impl.AccountDaoImpl">
<!-- 注入jdbcTemplate -->
<property name="jdbcTemplate" ref="jdbcTemplate"/>
</bean>
<!-- 配置jdbcTemplate -->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<!-- 注入数据源 -->
<property name="dataSource" ref="dataSource"/>
</bean>

<!-- 配置数据源 -->
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/test?serverTimezone=UTC&amp;useSSL=false"/>
<property name="username" value="root"/>
<property name="password" value="root"/>
</bean>

依赖全部注入完了,接下来就是我们的重点了,事务控制!

spring中基于xml的声明式事务控制的配置步骤
1.配置事务管理器,注入数据源
2.配置事务通知
3.配置aop的通用切入点表达式
4.建立事务通知和切入点表达式的关系
5.配置事务的属性 (在事务通知中配置)

第一步配置事务管理器并且注入数据源:

<!-- 配置事务管理器 -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <!-- 注入数据源 -->
    <property name="dataSource" ref="dataSource"/>
</bean>

第二步配置事务通知,它有一个transaction-manager属性用来指定事务管理器。

<!-- 配置事务通知 -->
<tx:advice id="txAdvice" transaction-manager="transactionManager">
</tx:advice>

然后配置AOP的通用切入表达式,并且通过tx:advisor标签的advice-ref属性来指定事务通知,关于AOP的知识就不多讲了,不会的看这篇文章

<!-- 配置aop的通用切入点表达式 -->
<aop:config>
    <!-- 配置通用切入点表达式 -->
    <aop:pointcut id="pt1" expression="execution(* cn.fuzhennan.service.impl.*.*(..))"/>
    <aop:advisor advice-ref="txAdvice" pointcut-ref="pt1"/>
</aop:config>

最后一步就是在事务通知的标签中给具体方法配置事务的属性了。事务通知总共有以下六个属性:

isolation:用于指定事务的隔离级别,默认值时DEFAULT,表示使用数据库的默认隔离级别
propagation:用于指定事物的传播行为,默认值时REQUIRED,并表示一定会有事务,增删改的选择,查询方法可以选择SUPPORTS
read-only:用于指定事务是否只读,只有查询方法才能设置为true,默认值是false,表示读写
no-rollback-for:用于指定一个异常,当产生该异常时,事务回滚,产生其他异常时,事务不回滚,没有默认值,表示任何异常都回滚
rollback-for:用于指定一个异常,当产生该异常时,事务不回滚,产生其他异常时,事务回滚,没有默认值,表示任何异常都回滚
timeout:用于指定事务的超时时间,默认值是-1,表示永不超时,如果指定了数值,以秒为单位

在这里我们写一个通配方法和一个查找方法就行:

<!-- 配置事务通知 -->
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<!-- 配置事务属性 -->
<tx:attributes>
<tx:method name="*" propagation="REQUIRED" read-only="false"/>
<tx:method name="find*" propagation="SUPPORTS" read-only="true"/>
</tx:attributes>
</tx:advice>

到这里就大功告成了,我们来编写一个测试类来测试一下。

package cn.fuzhennan.test;

import cn.fuzhennan.domain.Account;
import cn.fuzhennan.service.IAccountService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import java.util.List;

/**
* 使用Junit单元测试
* 测试配置
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:ApplicationContext.xml")
public class AccountServiceTest {
@Autowired
private IAccountService as=null;

@Test
public void testTransfer(){
as.transfer("aaa","bbb",100f);
}
}

运行testTransfer转账方法之前,我们先来看一下数据库:

aaa有1000块钱,bbb有1000块钱,然后我们运行。

这时我们发现程序有一个/by zero的异常,再来看看数据库aaa的钱没有有减少呢?bbb的钱有没有增加呢?

刷新之后我们发现aaa和bbb的钱是原封不动的,说明我们通过spring的声明式事务控制成功避免了这个错误。我们再来把那行错误代码注释,在运行一边看看结果。

程序没有任何异常

在数据库中aaa的钱减少了100,bbb增加了100,这完全符合我们的需求的。

以上就是spring中利用xml的方式配置声明式事务控制的全部过程啦。完整项目结构图如下:

有兴趣的同学可以看这篇文章,基于注解的spring声明式事务控制

赞(0) 打赏
未经允许不得转载:付振南Java博客 » spring之声明式事务控制(基于xml)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

微信扫一扫打赏