文章目录
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.