1. 初始化项目
1.1 新建一个项目
- 使用
maven
创建一个不使用骨架的项目。
1.2 导入依赖
- 打包方式使用
jar
,导入需要使用的依赖。属性maven
项目,导入依赖。
![](https://img.haomeiwen.com/i17394504/f562ff172f4ca575.png)
<packaging>jar</packaging>
<dependencies>
<!-- mysql驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.6</version>
</dependency>
<!-- spring 框架 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.0.5.RELEASE</version>
</dependency>
<!--dbUtils -->
<dependency>
<groupId>commons-dbutils</groupId>
<artifactId>commons-dbutils</artifactId>
<version>1.6</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
<dependency>
<groupId>com.mchange</groupId>
<artifactId>c3p0</artifactId>
<version>0.9.5.2</version>
</dependency>
<!-- Spring整合Junit单元测试jar包 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>5.0.2.RELEASE</version>
</dependency>
</dependencies>
1.3 创建account
实体类
public class Account {
private Integer id;
private String name;
private float money;
// 省略setter 和 getter 方法
}
1.4 创建持久层接口以及实现类
- 持久层接口
AccountDao
public interface AccountDao {
/**
* 更新账户信息
* @param account
*/
void updateAccount(Account account);
/**
* 根据账户名称查询账户
* @param accountName
* @return
*/
Account findByName(String accountName);
}
- 创建持久层实现类
AccountDaoImpl
public class AccountDaoImpl implements AccountDao {
public void updateAccount(Account account) {
try {
queryRunner.update("update account set name = ? , money = ? where id = ? ", account.getName(), ac
} catch (SQLException e) {
e.printStackTrace();
}
}
/**
* 根据账户名称查询账户信息
*
* @param accountName
* @return
*/
public Account findByName(String accountName) {
try {
List<Account> accounts = queryRunner.query("select * from account where name = ? ", new
// 判断查询的结果集
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);
}
}
}
1.5 创建业务层接口及实现类
- 创建业务层接口
AccountService
public interface AccountService {
/**
* 更新账户信息
* @param account
*/
void updateAccount(Account account);
/**
* 转账案例
* @param sourceName 转出账户
* @param targetName 转入账户
* @param money 转出金额
*/
void transfer(String sourceName , String targetName, Float money);
}
- 创建持久层实现类
AccountServiceImpl
public class AccountServiceImpl implements AccountService {
private AccountDao accountDao;
public void updateAccount(Account account) {
accountDao.updateAccount(account);
}
/**
* @param sourceName 转出账户
* @param targetName 转入账户
* @param money 转出金额
*/
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);
}
}
1.6 编写 bean.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"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="accountService" class="com.lyp.service.impl.AccountServiceImpl">
<property name="accountDao" ref="accountDao"/>
</bean>
<bean id="accountDao" class="com.lyp.dao.impl.AccountDaoImpl">
<property name="queryRunner" ref="queryRunner"/>
</bean>
<bean id="queryRunner" class="org.apache.commons.dbutils.QueryRunner">
<constructor-arg name="ds" ref="ds"/>
</bean>
<bean id="ds" class="com.mchange.v2.c3p0.ComboPooledDataSource">
<property name="driverClass" value="com.mysql.jdbc.Driver"/>
<property name="jdbcUrl" value="jdbc:mysql://localhost:3306/spring"/>
<property name="user" value="root"/>
<property name="password" value="xxxx"/>
</bean>
</beans>
1.7 测试 transfer
方法
- 在测试类中编写代码 对
transfer
方法进行测试
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:bean.xml")
public class SpringCrudTest {
@Autowired
private AccountService as;
@Test
public void testTransfer() {
as.transfer("aaa", "bbb", 100.0f);
}
}
- 此时测试是没有任何问题的,但是如果其中出现任何的异常,将会违背数据库的一致性原则。例如在 两个 account 更新数据之间加入一个造成异常信息的代码,此时这种情况就会出现。转出 account 减 ,但是转入 account 没有加。
![](https://img.haomeiwen.com/i17394504/9ba1c3548cadab5f.png)
2. 案例分析
-
问题 : 在transfer中方法中,每个与数据库的操作都分被获取了一个连接对象。这样每个对象都会进行一次提交。但是如果某个连接提交之前出现异常,该异常之后的连接事务将不会被提交。这样就会导致一致性出现问题。
-
解决 : 需要使用ThreadLocal对象将Connection和当前线程绑定,从而是一个线程中只有一个能控制事务的对象。多次操作使用同一个事务,要发生就同时发生。不发生就统一不发生。
-
事务的控制都应该是在业务层的。
3. 解决办法
3.1 新建一个获取连接的工具类
- 连接的工具类:用于在数据源中获取一个连接,并且实现和线程的绑定。
public class ConnectionUtils {
private ThreadLocal<Connection> tl = new ThreadLocal<Connection>();
private DataSource dataSource;
/**
* 使用注入的方式注入数据源
*
* @param dataSource
*/
public void setDataSource(DataSource dataSource) {
this.dataSource = dataSource;
}
public Connection getTreadLocalConnection() {
try {
// 1. 首先,从ThreadLocal上获取
Connection conn = tl.get();
// 2. 判断当前线程上是否有连接
if (conn == null) {
// 3. 如果没有连接将在数据源中获取一个连接存入到当前线程中
conn = dataSource.getConnection();
tl.set(conn);
}
// 4. 返回conn
return conn;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* 将线程与连接进行解绑
*/
public void removeConn() {
tl.remove();
}
}
3.2 创建一个事务管理的工具类 TransactionManager
- 和事务管理相关的工具类:包含开启事务、提交事务、回滚事务、释放连接。
public class TransactionManager {
private ConnectionUtils cu;
/**
* 用于注入 ConnectionUtils 对象
*
* @param cu
*/
public void setCu(ConnectionUtils cu) {
this.cu = cu;
}
/**
* 开启事务
*/
public void beginTransaction() {
try {
/* 设置事务提交方式为手动提交 */
cu.getTreadLocalConnection().setAutoCommit(false);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 回滚事务
*/
public void rollbackTransaction() {
try {
/* 设置事务提交方式为手动提交 */
cu.getTreadLocalConnection().rollback();
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 提交事务
*/
public void commitTransaction() {
try {
/* 设置事务提交方式为手动提交 */
cu.getTreadLocalConnection().commit();
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 释放连接
*/
public void closeTransaction() {
try {
/* 设置事务提交方式为手动提交 */
cu.getTreadLocalConnection().close();
cu.removeConn();
} catch (Exception e) {
e.printStackTrace();
}
}
}
- 细节:我们使用的连接都使用了连接池,连接池消耗时间和获取连接的部分放到应用加载的一开始。在web工程中,当我们启动
tomcat
时加载应用是创建一些连接,当我们在后续中获取连接时不再与数据库获取连接,保证在使用Connection
的执行效率。
3.3 在连接管理工具类中创建一个方法用于将线程和连接进行解绑操作
- 创建一个方法用于将线程与已经关闭的连接进行解绑。
/**
* 将线程与连接进行解绑
*/
public void removeConn() {
tl.remove();
}
3.4 编写业务层和持久层事务控制代码
- 在业务层加入事务管理对象对事务进行管理。并提供依赖注入的
setter
方法
private TransactionManager txManager;
/**
* 使用注入的方式进行注入
*
* @param txManager
*/
public void setTxManager(TransactionManager txManager) {
this.txManager = txManager;
}
- 进行事务管理之后的
transfer
方法
/**
* @param sourceName 转出账户
* @param targetName 转入账户
* @param money 转出金额
*/
public void transfer(String sourceName, String targetName, Float money) {
try {
// 1. 开启事务
txManager.beginTransaction();
// 2. 执行操作
// 2.1. 根据名称查询转出账户 名称
Account source = accountDao.findByName(sourceName);
// 2.2. 根据名称查询转入账户名称
Account target = accountDao.findByName(targetName);
// 2.3. 转出账户减钱
source.setMoney(source.getMoney() - money);
// 2.4. 转入账户加钱
target.setMoney(target.getMoney() + money);
// 2.5. 更新装出账户
accountDao.updateAccount(source);
// 添加一个异常
int i = 1 / 0;
// 2.6. 更新转入账户
accountDao.updateAccount(target);
// 3. 提交事务
txManager.commitTransaction();
// 4. 返回结果集
} catch (Exception e) {
// 5. 回滚事务
throw new RuntimeException(e);
} finally {
// 6. 释放连接
txManager.closeTransaction();
}
}
- 修改持久层的代码 ,持久层获取连接的方式不再是使用
QueryRunner
对象获取,而是通过ConnectionUtils对象获取。
/* 由于使用了事务控制QueryRunner对象中的连接不能再自个获取自个的了 而是从ConnectionUtils中取 */
private ConnectionUtils connectionUtils;
/**
* 为 注入 ConnectionUtils 对象 添加set方法
*
* @param connectionUtils
*/
public void setConnectionUtils(ConnectionUtils connectionUtils) {
this.connectionUtils = connectionUtils;
}
-
在持久层中QueryRunner的方法中加入该参数用于获取连接
在持久层中QueryRunner的方法中加入该参数用于获取连接
-
修改bean.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"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="accountService" class="com.lyp.service.impl.AccountServiceImpl">
<property name="accountDao" ref="accountDao"/>
<property name="txManager" ref="txManager"/>
</bean>
<bean id="accountDao" class="com.lyp.dao.impl.AccountDaoImpl">
<property name="queryRunner" ref="queryRunner"/>
<property name="connectionUtils" ref="connectionUtils"/>
</bean>
<bean id="queryRunner" class="org.apache.commons.dbutils.QueryRunner"/>
<bean id="connectionUtils" class="com.lyp.utils.ConnectionUtils">
<property name="dataSource" ref="ds"/>
</bean>
<bean id="txManager" class="com.lyp.utils.TransactionManager">
<property name="cu" ref="connectionUtils"/>
</bean>
<bean id="ds" class="com.mchange.v2.c3p0.ComboPooledDataSource">
<property name="driverClass" value="com.mysql.jdbc.Driver"/>
<property name="jdbcUrl" value="jdbc:mysql://localhost:3306/spring"/>
<property name="user" value="root"/>
<property name="password" value="****"/>
</bean>
</beans>
3.5 测试代码并分析当前代码中存在的问题
- 测试成功!
- 存在问题:代码重复,且存在相互依赖,方法之间的依赖严重。
- 解决办法:动态代理。
4. demo 地址
- demo地址 : spring修改数据库操作,实现事务简单控制.
网友评论