1 案例中的问题
1.1 示例代码
- 示例:
package com.sunxiaping.spring5.service.impl; import com.sunxiaping.spring5.dao.IAccountDao; import com.sunxiaping.spring5.domain.Account; import com.sunxiaping.spring5.service.IAccountService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.List; @Service public class AccountServiceImpl implements IAccountService { @Autowired private IAccountDao accountDao; @Override public void save(Account account) { accountDao.save(account); } @Override public void update(Account account) { accountDao.update(account); } @Override public void delete(Integer id) { accountDao.delete(id); } @Override public Account findById(Integer id) { return accountDao.findById(id); } @Override public List<Account> findAll() { return accountDao.findAll(); } }
1.2 问题
- 事务被自动控制了。换言之,我们使用的就是Connection对象的setAutoCommit(true)。
- 这种方式控制事务,如果我们每次都执行一条SQL语句,没有什么问题,但是如果业务方法一次要执行多条SQL语句,这种方式就无法实现功能了。
1.3 出现问题的示例代码
- 示例:
- IAccountService.java
package com.sunxiaping.spring5.service; import com.sunxiaping.spring5.domain.Account; import java.util.List; public interface IAccountService { /** * 保存账户 * * @param account */ void save(Account account); /** * 更新账户 * * @param account */ void update(Account account); /** * 删除账户 * * @param id */ void delete(Integer id); /** * 根据主键查询账户信息 * * @param id * @return */ Account findById(Integer id); /** * 查询所有 * * @return */ List<Account> findAll(); /** * 转账 * @param sourceName * @param targetName * @param money */ void transfer(String sourceName, String targetName, Double money); }
- AccountServiceImpl.java
package com.sunxiaping.spring5.service.impl; import com.sunxiaping.spring5.dao.IAccountDao; import com.sunxiaping.spring5.domain.Account; import com.sunxiaping.spring5.service.IAccountService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.List; @Service public class AccountServiceImpl implements IAccountService { @Autowired private IAccountDao accountDao; @Override public void save(Account account) { accountDao.save(account); } @Override public void update(Account account) { accountDao.update(account); } @Override public void delete(Integer id) { accountDao.delete(id); } @Override public Account findById(Integer id) { return accountDao.findById(id); } @Override public List<Account> findAll() { return accountDao.findAll(); } @Override public void transfer(String sourceName, String targetName, Double money) { Account source = accountDao.findByName(sourceName); Account target = accountDao.findByName(targetName); source.setMoney(source.getMoney() - money); target.setMoney(target.getMoney() + money); accountDao.update(source); int i = 1 / 0;//模拟转账异常 accountDao.update(target); } }
- IAccountDao.java
package com.sunxiaping.spring5.dao; import com.sunxiaping.spring5.domain.Account; import java.util.List; /** * 账户的持久层接口 */ public interface IAccountDao { /** * 保存账户 * * @param account */ void save(Account account); /** * 更新账户 * * @param account */ void update(Account account); /** * 删除账户 * * @param id */ void delete(Integer id); /** * 根据主键查询账户信息 * * @param id * @return */ Account findById(Integer id); /** * 查询所有 * * @return */ List<Account> findAll(); /** * 根据名称获取账户信息 * * @param name * @return */ Account findByName(String name); }
- AcccountDaoImpl.java
package com.sunxiaping.spring5.dao.impl; import com.sunxiaping.spring5.dao.IAccountDao; import com.sunxiaping.spring5.domain.Account; import org.apache.commons.dbutils.QueryRunner; import org.apache.commons.dbutils.handlers.BeanHandler; import org.apache.commons.dbutils.handlers.BeanListHandler; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Repository; import java.sql.SQLException; import java.util.List; @Repository public class AccountDaoImpl implements IAccountDao { @Autowired private QueryRunner queryRunner; @Override public void save(Account account) { try { queryRunner.update("insert into account(name,money) values (?,?)", account.getName(), account.getMoney()); } catch (SQLException e) { e.printStackTrace(); } } @Override public void update(Account account) { try { queryRunner.update("update account set name=?,money=? where id = ?", account.getName(), account.getMoney(), account.getId()); } catch (SQLException e) { e.printStackTrace(); } } @Override public void delete(Integer id) { try { queryRunner.update("delete from account where id = ?", id); } catch (SQLException e) { e.printStackTrace(); } } @Override public Account findById(Integer id) { try { return queryRunner.query("select * from account where id = ?", new BeanHandler<>(Account.class), id); } catch (SQLException e) { e.printStackTrace(); } return null; } @Override public List<Account> findAll() { try { return queryRunner.query("select * from account", new BeanListHandler<>(Account.class)); } catch (SQLException e) { e.printStackTrace(); } return null; } @Override public Account findByName(String name) { try { List<Account> accountList = queryRunner.query("select * from account where name = ? ", new BeanListHandler<>(Account.class), name); if (null == accountList || accountList.size() == 0) { return null; } else { return accountList.get(0); } } catch (SQLException e) { e.printStackTrace(); } return null; } }
- 测试:
package com.sunxiaping; import com.sunxiaping.spring5.config.SpringConfiguration; import com.sunxiaping.spring5.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(classes = SpringConfiguration.class) public class spring5Test { @Autowired private IAccountService accountService; @Test public void test() { accountService.transfer("aaa","bbb",100d); } }
- 业务层实现类中的转账方法:
@Override public void transfer(String sourceName, String targetName, Double money) { Account source = accountDao.findByName(sourceName); Account target = accountDao.findByName(targetName); source.setMoney(source.getMoney() - money); target.setMoney(target.getMoney() + money); accountDao.update(source); int i = 1 / 0;//模拟转账异常 accountDao.update(target); }
- 当我们执行的时候,由于执行有异常,转账失败。但是因为我们是每次执行持久层的方法都是独立事务,导致无法实现事务控制(不符合事务的一致性)。
2 问题的解决
2.1 思路
- 让业务层来控制事务的提交和回滚。
2.2 示例代码
- Connection.java
package com.sunxiaping.spring5.utils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import javax.sql.DataSource; import java.sql.Connection; import java.sql.SQLException; /** * Connection的工具类 */ @Component public class ConnectionUtils { private ThreadLocal<Connection> threadLocal = new ThreadLocal<>(); @Autowired private DataSource dataSource; /** * 获取当前线程上的连接 * * @return */ public Connection getThreadConnection() { //先从ThreadLocal中获取连接 Connection connection = threadLocal.get(); //如果连接不存在 if (connection == null) { try { //从连接池中获取连接 connection = dataSource.getConnection(); //将连接存放到ThreadLocal中 threadLocal.set(connection); } catch (SQLException e) { throw new RuntimeException(e); } } return connection; } /** * 从ThreadLocal中将连接移除 */ public void removeConnection() { threadLocal.remove(); } }
- TransactionManager.java
package com.sunxiaping.spring5.utils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import java.sql.Connection; import java.sql.SQLException; /** * 事务控制器:事务管理的工具类。包含了开启事务、提交事务、回滚事务和释放连接。 */ @Component public class TransactionManager { @Autowired private ConnectionUtils connectionUtils; /** * 开始事务 */ public void beginTransaction() { Connection connection = connectionUtils.getThreadConnection(); try { connection.setAutoCommit(false); } catch (SQLException e) { throw new RuntimeException(e); } } /** * 提交事务 */ public void commit() { Connection connection = connectionUtils.getThreadConnection(); try { connection.commit(); } catch (SQLException e) { throw new RuntimeException(e); } } /** * 回滚事务 */ public void rooback() { Connection connection = connectionUtils.getThreadConnection(); try { connection.rollback(); } catch (SQLException e) { throw new RuntimeException(e); } } /** * 释放连接 */ public void close() { Connection connection = connectionUtils.getThreadConnection(); try { //释放连接后,需要将此连接和当前线程解绑 connection.close(); connectionUtils.removeConnection(); } catch (SQLException e) { throw new RuntimeException(e); } } }
- IAccountService.java
package com.sunxiaping.spring5.config; import com.mchange.v2.c3p0.ComboPooledDataSource; import org.apache.commons.dbutils.QueryRunner; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.PropertySource; import javax.sql.DataSource; import java.beans.PropertyVetoException; @PropertySource("classpath:jdbc.properties") public class JdbcConfig { @Value("${jdbc.driverClass}") private String driverClass; @Value("${jdbc.url}") private String jdbcUrl; @Value("${jdbc.user}") private String user; @Value("${jdbc.password}") private String password; @Bean public DataSource dataSource() { try { ComboPooledDataSource dataSource = new ComboPooledDataSource(); dataSource.setJdbcUrl(jdbcUrl); dataSource.setDriverClass(driverClass); dataSource.setUser(user); dataSource.setPassword(password); return dataSource; } catch (PropertyVetoException e) { throw new RuntimeException(e); } } @Bean public QueryRunner queryRunner() { return new QueryRunner(); } }
- AccountServiceImpl.java
package com.sunxiaping.spring5.service.impl; import com.sunxiaping.spring5.dao.IAccountDao; import com.sunxiaping.spring5.domain.Account; import com.sunxiaping.spring5.service.IAccountService; import com.sunxiaping.spring5.utils.TransactionManager; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.List; @Service public class AccountServiceImpl implements IAccountService { @Autowired private TransactionManager transactionManager; @Autowired private IAccountDao accountDao; @Override public void save(Account account) { try { transactionManager.beginTransaction(); accountDao.save(account); transactionManager.commit(); } catch (Exception e) { transactionManager.rooback(); throw new RuntimeException(e); } finally { transactionManager.close(); } } @Override public void update(Account account) { try { transactionManager.beginTransaction(); accountDao.update(account); transactionManager.commit(); } catch (Exception e) { transactionManager.rooback(); throw new RuntimeException(e); } finally { transactionManager.close(); } } @Override public void delete(Integer id) { try { transactionManager.beginTransaction(); accountDao.delete(id); transactionManager.commit(); } catch (Exception e) { transactionManager.rooback(); throw new RuntimeException(e); } finally { transactionManager.close(); } } @Override public Account findById(Integer id) { try { transactionManager.beginTransaction(); Account account = accountDao.findById(id); transactionManager.commit(); return account; } catch (Exception e) { transactionManager.rooback(); throw new RuntimeException(e); } finally { transactionManager.close(); } } @Override public List<Account> findAll() { try { transactionManager.beginTransaction(); List<Account> accountList = accountDao.findAll(); transactionManager.commit(); return accountList; } catch (Exception e) { transactionManager.rooback(); throw new RuntimeException(e); } finally { transactionManager.close(); } } @Override public void transfer(String sourceName, String targetName, Double money) { try { transactionManager.beginTransaction(); Account source = accountDao.findByName(sourceName); Account target = accountDao.findByName(targetName); source.setMoney(source.getMoney() - money); target.setMoney(target.getMoney() + money); accountDao.update(source); int i = 1 / 0;//模拟转账异常 accountDao.update(target); transactionManager.commit(); } catch (Exception e) { transactionManager.rooback(); throw new RuntimeException(e); } finally { transactionManager.close(); } } }
- IAccountDao.java
package com.sunxiaping.spring5.dao; import com.sunxiaping.spring5.domain.Account; import java.util.List; /** * 账户的持久层接口 */ public interface IAccountDao { /** * 保存账户 * * @param account */ void save(Account account); /** * 更新账户 * * @param account */ void update(Account account); /** * 删除账户 * * @param id */ void delete(Integer id); /** * 根据主键查询账户信息 * * @param id * @return */ Account findById(Integer id); /** * 查询所有 * * @return */ List<Account> findAll(); /** * 根据名称获取账户信息 * * @param name * @return */ Account findByName(String name); }
- AccountDaoImpl.java
package com.sunxiaping.spring5.dao.impl; import com.sunxiaping.spring5.dao.IAccountDao; import com.sunxiaping.spring5.domain.Account; import com.sunxiaping.spring5.utils.ConnectionUtils; import org.apache.commons.dbutils.QueryRunner; import org.apache.commons.dbutils.handlers.BeanHandler; import org.apache.commons.dbutils.handlers.BeanListHandler; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Repository; import java.sql.SQLException; import java.util.List; @Repository public class AccountDaoImpl implements IAccountDao { @Autowired private QueryRunner queryRunner; @Autowired private ConnectionUtils connectionUtils; @Override public void save(Account account) { try { queryRunner.update(connectionUtils.getThreadConnection(), "insert into account(name,money) values (?,?)", account.getName(), account.getMoney()); } catch (SQLException e) { e.printStackTrace(); } } @Override public void update(Account account) { try { queryRunner.update(connectionUtils.getThreadConnection(), "update account set name=?,money=? where id = ?", account.getName(), account.getMoney(), account.getId()); } catch (SQLException e) { e.printStackTrace(); } } @Override public void delete(Integer id) { try { queryRunner.update(connectionUtils.getThreadConnection(), "delete from account where id = ?", id); } catch (SQLException e) { e.printStackTrace(); } } @Override public Account findById(Integer id) { try { return queryRunner.query(connectionUtils.getThreadConnection(), "select * from account where id = ?", new BeanHandler<>(Account.class), id); } catch (SQLException e) { e.printStackTrace(); } return null; } @Override public List<Account> findAll() { try { return queryRunner.query(connectionUtils.getThreadConnection(), "select * from account", new BeanListHandler<>(Account.class)); } catch (SQLException e) { e.printStackTrace(); } return null; } @Override public Account findByName(String name) { try { List<Account> accountList = queryRunner.query(connectionUtils.getThreadConnection(), "select * from account where name = ? ", new BeanListHandler<>(Account.class), name); if (null == accountList || accountList.size() == 0) { return null; } else { return accountList.get(0); } } catch (SQLException e) { e.printStackTrace(); } return null; } }
- JdbcConfig.java
package com.sunxiaping.spring5.config; import com.mchange.v2.c3p0.ComboPooledDataSource; import org.apache.commons.dbutils.QueryRunner; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.PropertySource; import javax.sql.DataSource; import java.beans.PropertyVetoException; @PropertySource("classpath:jdbc.properties") public class JdbcConfig { @Value("${jdbc.driverClass}") private String driverClass; @Value("${jdbc.url}") private String jdbcUrl; @Value("${jdbc.user}") private String user; @Value("${jdbc.password}") private String password; @Bean public DataSource dataSource() { try { ComboPooledDataSource dataSource = new ComboPooledDataSource(); dataSource.setJdbcUrl(jdbcUrl); dataSource.setDriverClass(driverClass); dataSource.setUser(user); dataSource.setPassword(password); return dataSource; } catch (PropertyVetoException e) { throw new RuntimeException(e); } } @Bean public QueryRunner queryRunner() { return new QueryRunner(); } }
- SpringConfiguration.java
package com.sunxiaping.spring5.config; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; @Configuration @ComponentScan("com.sunxiaping.spring5") @Import(JdbcConfig.class) public class SpringConfiguration { }