Spring 框架学习8 - 对 Account 案例的修改

Spring 小案例的完善

1. 使用 Spring 集成 junit 来进行测试

使用 Spring 来集成 junit,就必须导入 Spring Context Framework,在 pom.xml 中添加一下依赖项,这里我才用的是最新的版本:

<!-- https://mvnrepository.com/artifact/org.springframework/spring-test -->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-test</artifactId>
    <version>5.1.9.RELEASE</version>
    <scope>test</scope>
</dependency>

然后使用 Runwith 和 ContextConfiguration 注解来配置环境。

package com.self.learning.spring.test;

import com.self.learning.spring.service.IAccountService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:bean.xml")
public class AccountServiceTest {
   

    @Autowired
    private IAccountService accountService;

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

然后,便可以愉快地进行测试了。

2. 添加转账方法

首先,在持久层 dao 接口 IAccountDao 中添加根据名字查询 Account 的方法:

    /**
     * 根据 name 查找 Account
     * @param name
     * @return
     */
   Account findByName(String name);

然后,在实现类 AccountDaoImp 中,实现这个方法:

    @Override
    public Account findByName(String name) {
   
        try {
   
            List<Account> accounts = runner.query("select * from account where name = ? ", new BeanListHandler<>(Account.class), name);
            if(accounts == null || accounts.size() == 0){
   
                return null;
            }
            if(accounts.size() > 1) {
   
                throw new RuntimeException("结果集不唯一");
            }
            return accounts.get(0);
        } catch (SQLException e) {
   
            throw new RuntimeException(e);
        }
    }

这里需要注意,由于我们只找一个 Account,如果同时存在两个或者不存在该账户,都要进行相应的处理。

接着,在业务层的接口 IAcountService 中,添加转账方法:

    /**
     * 转账方法
     * @param sourceName
     * @param targetName
     * @param money
     */
    void transfer(String sourceName, String targetName, Float money);

这里的转账方法,sourceName 是转出账户的名字,targetName 是转入账户的名字,而 money 就是转出的金钱了。

在其实现类 AccountServiceImpl 中实现该方法:

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

最后,在测试类中进行测试:

package com.self.learning.spring.test;

import com.self.learning.spring.service.IAccountService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:bean.xml")
public class AccountServiceTest {
   

    @Autowired
    private IAccountService accountService;

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

但是,这个方法目前存在错误。错误在,如果转账失败,就会出现数据库的变化和预期变化不一致的现象,破坏了数据库操作的原子性

3. 分析事务的问题并重新编写 ConnectionUtils

分析一下转账的实现类的方法,如果在中间添加一个错误的代码块:

    @Override
    public void transfer(String sourceName, String targetName, Float money) {
   
        // 1. 根据名称查询转出账户
        Account source = accountDao.findByName(sourceName);
        // 2. 根据名称查询转入账户
        Account target = accountDao.findByName(targetName);
        // 3. 转出账户减钱
        source.setMoney(source.getMoney() - money);
        // 4. 转入账户加钱
        target.setMoney(target.getMoney() + money);
        // 5. 更新转出账户
        accountDao.updateAccount(source);
        
        int i = 1/0; // 报错,因为除数不能为 0
        
        // 6. 更新转入账户
        accountDao.updateAccount(target);
    }

由于之前,我们在告知 Spring 创建 QueryRunner bean 的时候,设置的 scope 属性是 prototype(多例的)。所以,在执行转账操作的时候,一共进行了 4 次 bean 对象的创建。因为 QueryRunner 对象时多例的,所以只要有连接,就创建一个新的 bean 对象。所以,直到碰到错误代码块之前,所有的事务都已经提交,这样就会出现破坏事务原子性的问题。

所以我们在解决问题的时候,以上的连接,应该由同一个 connection 来控制。要成功一起成功,要失败一起失败。

所以我们要使用多线程中的 ThreadLocal。我们使用 ThreadLocal 对象吧 Connection 和 当前线程绑定,从而使一个线程中只有一个能控制事务的对象。

以下是操作:

在源包下创建一个 utils 包以存放一个数据库连接的工具类:

[外链图片转存失败(img-DxZTiu0c-1569154397550)(E:\Document\java\JAVE EE\笔记\框架\Spring\7. Spring 小案例的完善\03.分析事物的问题并重新编写 ConnectionUtils\分析事物的问题并重新编写 ConnectionUtils 01.png)]

然后,创建一个本地线程 ThreadLocal,这个线程池使用从数据源中获取一个连接,并且实现和线程的绑定:

package com.self.learning.spring.utils;

import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException;

/**
 * 连接的工具类,它用于从数据源中获取一个连接,并且
 * 实现和线程的绑定。
 */
public class ConnectionUtils {
   

    private ThreadLocal<Connection> tl = new ThreadLocal<Connection>();

    private DataSource dataSource;

    public void setTl(ThreadLocal<Connection> tl) {
   
        this.tl = tl;
    }

    /**
     * 获取当前线程上的连接
     *
     * @return
     */
    public Connection getThreadConnection() throws SQLException {
   
        // 1. 先从 ThreadLocal 上获取
        Connection conn = tl.get();
        // 2. 判断当前线程上是否有连接
        if (conn == null) {
   
            // 3. 从数据源中获取一个连接,并且存入 ThreadLocal 中
            conn = dataSource.getConnection();
            tl.set(conn);
        }
        // 4. 返回当前线程上的连接
        return conn;
    }

}

4. 编写事务管理工具类并分析连接和线程解绑

在编写完连接工具类之后,为了实现解除数据库事务的自动提交和事务的回滚。我们需要另一个事务管理工具类,在 utils 包中创建一个 TransactionManager 类用来管理事务:

package com.self.learning.spring.utils;

import java.sql.SQLException;

/**
 * 和事务管理相关的工具类,它包含了:开启事务、提交事务、回滚事务和释放连接
 */
public class TransactionManager {
   

    private ConnectionUtils connectionUtils;

    public void setConnectionUtils(ConnectionUtils connectionUtils) {
   
        this.connectionUtils = connectionUtils;
    }

    /**
     * 开启事务
     */
    public void beginTransaction() throws SQLException {
   
        // 将自动提交事务设置为手动
        connectionUtils.
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值