本教程源码请访问:tutorial_demo
上一篇教程我们使用纯注解方式结合Apache Commons DbUtils实现单表的CRUD操作,但是这篇教程里面的操作的是不支持事务的,在这片教程里我们根据现有的知识,将其改成支持事务的版本,为后续学习做准备。
一、转账操作问题分析
接下来我们实现一个转账操作,分析一下问题存在的问题。
1.1、在业务层接口IAccountService中添加相应的方法
//新增加的转账方法
void transfer(Integer srcId, Integer dstId, Float money);
1.2、在业务层实现类AccountServiceImpl中实现新添加的方法
//转账操作
@Override
public void transfer(Integer srcId, Integer dstId, Float money) {
//根据Id查询需要转账的用户
Account src = accountDao.findById(srcId);
Account dst = accountDao.findById(dstId);
if(src == null) {
throw new RuntimeException("转出用户不存在");
}
if(dst == null) {
throw new RuntimeException("转入用户不存在");
}
if(src.getMoney() < money) {
throw new RuntimeException("转出账户余额不足");
}
//一个钱减少,一个钱增加
src.setMoney(src.getMoney() - money);
dst.setMoney(dst.getMoney() + money);
//在数据库中更新
accountDao.update(src);
accountDao.update(dst);
}
1.3、在测试类中添加测试方法
@Test
public void testTrans() {
accountService.transfer(1, 2, 10F);
}
运行测试方法,测试成功。似乎没有问题。
1.4、Service层转账方法修改
//转账操作
@Override
public void transfer(Integer srcId, Integer dstId, Float money) {
//根据Id查询需要转账的用户
Account src = accountDao.findById(srcId);
Account dst = accountDao.findById(dstId);
if(src == null) {
throw new RuntimeException("转出用户不存在");
}
if(dst == null) {
throw new RuntimeException("转入用户不存在");
}
if(src.getMoney() < money) {
throw new RuntimeException("转出账户余额不足");
}
//一个钱减少,一个钱增加
src.setMoney(src.getMoney() - money);
dst.setMoney(dst.getMoney() + money);
//在数据库中更新
accountDao.update(src);
//新增加的内容,人为制造一个异常
int i = 100/0;
//在数据库中更新
accountDao.update(dst);
}
运行测试方法,程序出现异常,但是src在数据库中钱减少。
转账流程分析:
- 转账流程大的原则是源账户钱减少,目的账户钱增加;
- 源账户钱减少,目的账户钱增加的操作要么同时成功,要么同时失败;
- 为了满足同时成功,同时失败应该将相关操作包裹在同一个事务当中;
- 同一个事务操作应该使用同一个连接(Connection)。
目前存在的问题:当前转账流程没有开启事务,所有操作使用独立的连接。
二、代码升级
根据前面的分析,我们要对工程进行修改,使其支持事务,应该满足的原则如下:
- 在Service层处理事务(开启事务,提交,回滚);
- Dao层只负责数据库操作不负责业务,也就是说每个Dao操作只进行最细粒度的CRUD操作;
- Dao层不处理异常,出现的异常抛给Service层处理。
2.1、创建支持事务的工具类
package org.codeaction.util;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException;
@Component
public class JdbcUtils {
private static DataSource dataSource;
//用来保存当前线程的连接
private static ThreadLocal<Connection> tl = new ThreadLocal<Connection>();
//获取连接池对象
public static DataSource getDataSource() {
return dataSource;
}
//这个位置注意
@Autowired
public void setDataSource(DataSource dataSource) {
JdbcUtils.dataSource = dataSource;
}
//获取连接
public static Connection getConnection() throws SQLException {
System.out.println(dataSource);
Connection conn = tl.get();
if(conn == null) {
return dataSource.getConnection();
}
return conn;
}
//开启事务
public static void beginTransaction() throws SQLException {
Connection conn = tl.get();
if(conn != null) {
throw new SQLException("已经开启事务,不能重复开启");
}
conn = getConnection();
conn.setAutoCommit(false);
tl.set(conn);
}
//提交事务
public static void commitTransaction() throws SQLException {
Connection conn = tl.get();
if(conn == null) {
throw new SQLException("连接为空,不能提交事务");
}
conn.commit();
conn.close();
tl.remove();
}
//回滚事务
public static void rollbackTransaction() throws SQLException {
Connection conn = tl.get();
if (conn == null) {
throw new SQLException("连接为空,不能回滚事务");
}
conn.rollback();
conn.close();
tl.remove();
}
}
说明:
- 这个工程使用了ThreadLocal,保证能够在多线程环境下使用;
- dataSource属性没有在它上面使用@AutoWired,而是在它的set方法上使用了@AutoWired。原因是因为dataSource是static类型,static类型变量创建早于Spring容器的创建,类加载器加载静态变量时,Spring上下文尚未加载。所以类加载器不会在bean中正确注入静态类,并且会失败。
2.2、修改Dao的实现类
package org.codeaction.dao.impl;
import org.apache.commons.dbutils.QueryRunner;
import org.apache.commons.dbutils.handlers.BeanHandler;
import org.apache.commons.dbutils.handlers.BeanListHandler;
import org.codeaction.dao.IAccountDao;
import org.codeaction.domain.Account;
import org.codeaction.util.JdbcUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.List;
@Repository("accountDao")
public class AccountDaoImpl implements IAccountDao {
@Autowired
private QueryRunner queryRunner;
public void setQueryRunner(QueryRunner queryRunner) {
this.queryRunner = queryRunner;
}
@Override
public List<Account> findAll() throws SQLException {
Connection conn = JdbcUtils.getConnection();
String sql = "select * from account";
List<Account> list = queryRunner.query(conn, sql, new BeanListHandler<Account>(Account.class));
return list;
}
@Override
public Account findById(Integer id) throws SQLException {
Connection conn = JdbcUtils.getConnection();
String sql = "select * from account where id = ?";
Account account = queryRunner.query(conn, sql, new BeanHandler<Account>(Account.class), id);
return account;
}
@Override
public void save(Account account) throws SQLException {
Object[] params = {account.getName(), account.getMoney()};
Connection conn = JdbcUtils.getConnection();
String sql = "insert into account(name, money) values(?, ?)";
queryRunner.update(conn, sql, params);
}
@Override
public void update(Account account) throws SQLException {
Object[] params = {account.getName(), account.getMoney(), account.getId()};
Connection conn = JdbcUtils.getConnection();
String sql = "update account set name=?, money=? where id=?";
queryRunner.update(conn, sql, params);
}
@Override
public void delete(Integer id) throws SQLException {
Object[] params = {id};
Connection conn = JdbcUtils.getConnection();
String sql = "delete from account where id=?";
queryRunner.update(conn, sql, id);
}
}
2.3、修改Service层实现类
package org.codeaction.service.impl;
import org.codeaction.dao.IAccountDao;
import org.codeaction.domain.Account;
import org.codeaction.service.IAccountService;
import org.codeaction.util.JdbcUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.sql.SQLException;
import java.util.List;
@Service("accountService")
public class AccountServiceImpl implements IAccountService {
@Autowired
private IAccountDao accountDao;
public void setAccountDao(IAccountDao accountDao) {
this.accountDao = accountDao;
}
@Override
public List<Account> findAll() {
List<Account> list = null;
try {
//1.开启事务
JdbcUtils.beginTransaction();
//2.操作
list = accountDao.findAll();
//3.提交事务
JdbcUtils.commitTransaction();
} catch (Exception e) {
e.printStackTrace();
try {
//回滚事务
JdbcUtils.rollbackTransaction();
} catch (SQLException e1) {
e1.printStackTrace();
}
}
return list;
}
@Override
public Account findById(Integer id) {
Account account = null;
try {
//1.开启事务
JdbcUtils.beginTransaction();
//2.操作
account = accountDao.findById(id);
//3.提交事务
JdbcUtils.commitTransaction();
} catch (Exception e) {
e.printStackTrace();
try {
//回滚事务
JdbcUtils.rollbackTransaction();
} catch (SQLException e1) {
e1.printStackTrace();
}
}
return account;
}
@Override
public void save(Account account) {
try {
//1.开启事务
JdbcUtils.beginTransaction();
//2.操作
accountDao.save(account);
//3.提交事务
JdbcUtils.commitTransaction();
} catch (Exception e) {
e.printStackTrace();
try {
//回滚事务
JdbcUtils.rollbackTransaction();
} catch (SQLException e1) {
e1.printStackTrace();
}
}
}
@Override
public void update(Account account) {
try {
//1.开启事务
JdbcUtils.beginTransaction();
//2.操作
accountDao.update(account);
//3.提交事务
JdbcUtils.commitTransaction();
} catch (Exception e) {
e.printStackTrace();
try {
//回滚事务
JdbcUtils.rollbackTransaction();
} catch (SQLException e1) {
e1.printStackTrace();
}
}
}
@Override
public void delete(Integer id) {
try {
//1.开启事务
JdbcUtils.beginTransaction();
//2.操作
accountDao.delete(id);
//3.提交事务
JdbcUtils.commitTransaction();
} catch (Exception e) {
e.printStackTrace();
try {
//回滚事务
JdbcUtils.rollbackTransaction();
} catch (SQLException e1) {
e1.printStackTrace();
}
}
}
@Override
public void transfer(Integer srcId, Integer dstId, Float money) {
try {
//1.开启事务
JdbcUtils.beginTransaction();
//2.操作
Account src = accountDao.findById(srcId);
Account dst = accountDao.findById(dstId);
if(src == null) {
throw new RuntimeException("转出用户不存在");
}
if(dst == null) {
throw new RuntimeException("转入用户不存在");
}
if(src.getMoney() < money) {
throw new RuntimeException("转出账户余额不足");
}
src.setMoney(src.getMoney() - money);
dst.setMoney(dst.getMoney() + money);
accountDao.update(src);
//int x = 1/0;//注意这里
accountDao.update(dst);
//3.提交事务
JdbcUtils.commitTransaction();
} catch (Exception e) {
e.printStackTrace();
try {
//回滚事务
JdbcUtils.rollbackTransaction();
} catch (SQLException e1) {
e1.printStackTrace();
}
}
}
}
2.4、修改JdbcConfig配置类
package org.codeaction.config;
import com.mchange.v2.c3p0.ComboPooledDataSource;
import org.apache.commons.dbutils.QueryRunner;
import org.codeaction.util.JdbcUtils;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.sql.DataSource;
@Configuration
public class JdbcConfig {
@Value("${jdbc.driverClass}")
private String driverClass;
@Value("${jdbc.jdbcUrl}")
private String jdbcUrl;
@Value("${jdbc.user}")
private String user;
@Value("${jdbc.password}")
private String password;
@Bean("queryRunner")
public QueryRunner queryRunner() {
return new QueryRunner();
}
@Bean("dataSource")
public DataSource dataSource() {
ComboPooledDataSource dataSource = new ComboPooledDataSource();
try {
dataSource.setDriverClass(driverClass);
dataSource.setJdbcUrl(jdbcUrl);
dataSource.setUser(user);
dataSource.setPassword(password);
} catch (Exception e) {
e.printStackTrace();
}
return dataSource;
}
}
2.5、运行测试方法
@Test
public void testTrans() {
accountService.transfer(1, 2, 10F);
}
运行该测试方法转装能够成功;
取消2.4中int i = 1/0;
的注释,运行测试方法,出现异常,能够正常回滚。
2.6、目前代码存在的问题
目前的代码已经能够实现对事务的支持了,但是仍然存在一些问题,问题如下:
- 代码过于繁琐,Service层实现类相比于上一个版本太繁琐了,除了要负责业务还要负责事务的操作;
- 如果JdbcUtils类中的和事务操作相关的方法有一个修改,那么Service层代码相应的调用位置都要修改,如果项目很庞大,对于开发人员来说这简直就是噩梦。
后面的几节我们会深入的学习Spring AOP,从而解决上述问题。
来源:oschina
链接:https://my.oschina.net/u/4373067/blog/4295657