1、 创建maven的jar工程,添加依赖jar包
<dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>5.0.2.RELEASE</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-test</artifactId> <version>5.0.2.RELEASE</version> </dependency> <dependency> <groupId>commons-dbutils</groupId> <artifactId>commons-dbutils</artifactId> <version>1.4</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.6</version> </dependency> <dependency> <groupId>c3p0</groupId> <artifactId>c3p0</artifactId> <version>0.9.1.2</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <configuration> <source>6</source> <target>6</target> </configuration> </plugin> </plugins> </build>
2、创建数据库eesy下的account1表
3、创建Account实体类
public class Account implements Serializable { private Integer id; private String name; private Float money; public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public Float getMoney() { return money; } public void setMoney(Float money) { this.money = money; } @Override public String toString() { return "Account{" + "id=" + id + ", name='" + name + '\'' + ", money=" + money + '}'; } }
4、写业务层接口IAccountService
public interface IAccountService { void transfer(String sourceName,String targetName,Float money); void updateAccount(Account account); }
5、 写业务层接口的实现类,在类中添加set方法以XML的方式注入依赖
public class AccountServiceImpl_OLD implements IAccountService{ private IAccountDao accountDao;public void setAccountDao(IAccountDao accountDao) { this.accountDao = accountDao; } @Override public void transfer(String sourceName, String targetName, Float money) { try {//2.执行操作 //2.1根据名称查询转出账户 Account source = accountDao.findAccountByName(sourceName); //2.2根据名称查询转入账户 Account target = accountDao.findAccountByName(targetName); //2.3转出账户减钱 source.setMoney(source.getMoney()-money); //2.4转入账户加钱 target.setMoney(target.getMoney()+money); //2.5更新转出账户 accountDao.updateAccount(source);//2.6更新转入账户 accountDao.updateAccount(target); }catch (Exception e){ e.printStackTrace(); } } @Override public void updateAccount(Account account) { try { accountDao.updateAccount(account); }catch (Exception e){ throw new RuntimeException(e); } } }
6、创建持久层接口IAccountDao
public interface IAccountDao { Account findAccountByName(String accountName); void updateAccount(Account account); }
7、创建持久层实现类AccountDaoImpl
public class AccountDaoImpl implements IAccountDao { private QueryRunner runner; public void setRunner(QueryRunner runner) { this.runner = runner; } @Override public Account findAccountByName(String accountName) { try{ List<Account> accounts = runner.query( "select * from account1 where name = ? ", new BeanListHandler<Account>(Account.class),accountName); if(accounts == null || accounts.size() == 0){ return null; } if(accounts.size() > 1){ throw new RuntimeException("结果集不唯一,数据有问题"); } return accounts.get(0); }catch (Exception e) { throw new RuntimeException(e); } } @Override public void updateAccount(Account account) { try{ runner.update("update account1 set name=?,money=? where id=?",account.getName(),account.getMoney(),account.getId()); }catch (Exception e) { throw new RuntimeException(e); } } }
8、创建bean.xml文件,导入约束,配置bean对象
<?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"> <!-- 配置Service --> <bean id="accountService" class="com.itheima.service.impl.AccountServiceImpl_OLD"> <!-- 注入dao --> <property name="accountDao" ref="accountDao"></property> </bean> <!--配置Dao对象--> <bean id="accountDao" class="com.itheima.dao.impl.AccountDaoImpl"> <!-- 注入QueryRunner --> <property name="runner" ref="runner"></property> </bean> <!--配置QueryRunner--> <bean id="runner" class="org.apache.commons.dbutils.QueryRunner" scope="prototype"> <!--注入数据源--> <constructor-arg name="ds" ref="dataSource"></constructor-arg> </bean> <!-- 配置数据源 --> <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource"> <!--连接数据库的必备信息--> <property name="driverClass" value="com.mysql.jdbc.Driver"></property> <property name="jdbcUrl" value="jdbc:mysql://localhost:3306/eesy"></property> <property name="user" value="root"></property> <property name="password" value="123456"></property> </bean> </beans>
9、使用Junit单元测试,测试我们的配置
@RunWith(SpringJUnit4ClassRunner.class)//替换成能创建容器的main方法 @ContextConfiguration(locations = "classpath:bean.xml")//通过XML方式创建容器 public class AccountServiceTest { @Autowired private IAccountService as; @Test public void testTransfer(){ as.transfer("aaa","bbb",100f); } }
结果为:
测试发现转账成功,但是这只是一个假象,这只是建立在一切正常的情况下,我们在transfer方法中添加int i=1/0
@Override public void transfer(String sourceName, String targetName, Float money) { try { //2.执行操作 //2.1根据名称查询转出账户 Account source = accountDao.findAccountByName(sourceName); //2.2根据名称查询转入账户 Account target = accountDao.findAccountByName(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); }catch (Exception e){ e.printStackTrace(); } }
如果在更新转出账户之后出现了异常,则转出账户的钱少了100,而准入账户的钱未增加100.违反了事务的一致性,
报错如下:
分析:
以上问题不是没有事务造成的,因为增删改方法都可以执行,没有事务就代表无法提交,无法提交的话,事务只能回滚。mybatis默认事务是开启的,在mysql数据库中,事务默认自动提交,即成功了就会提交事务。
那是什么造成的呢?
QueryRunner对象是一个多例对象,每次都会创建一个新的,并且在执行操作的时候都会从数据源中获取一个连接。通过代码可以看出转账操作与数据库交互了四次:
1、 根据名称查询转出账户
2、 根据名称查询转入账户
3、 更新转出账户
4、 更新转入账户
每次与数据库交互都会获取一个连接,即一共有四个connetion, 每个Connection都有一个自己独立的事务,mybatis默认事务是开启的,成功了就会提交事务,故前面三次与数据库的交互都提交了事务,由于出现了异常,第四次与数据库交互没有成功则不能提交事务。已经提交的就结束了,没有提交的就不会执行了。
解决问题的思路是:四次与数据库的交互应该由同一个Connection来控制,即只有一个Connection对象,要成功则这些操作一起成功,要失败则一起失败,如何实现让这些操作使用同一个Connection呢?需要使用ThreadLocal对象把同一个Connection与当前线程绑定,从而使一个线程中只有一个能控制事务的对象,这样就实现了多次操作使用同一个事务,要发生就全部发生,要不发生就都不发生。
通过以上分析,需要对代码进行调整:
1、事务的控制应该在业务层(昨天讲解的时候,事务都在持久层)
2、创建连接的工具类ConnectionUtils,它用于返回当前线程的连接。
改造
1、先从数据源中获取一个连接,并且把连接存入ThreadLocal中从而实现连接与线程的绑定。
/** * 连接的工具类,它用于从数据源中获取一个连接,并且实现和线程的绑定 */ public class ConnectionUtils { private ThreadLocal<Connection> tl = new ThreadLocal<Connection>(); private DataSource dataSource; public void setDataSource(DataSource dataSource) { this.dataSource = dataSource; } // 获取当前线程上的连接 public Connection getThreadConnection() { try{ //1.先从ThreadLocal上获取连接 Connection conn = tl.get(); //2.判断当前线程上是否有连接 if (conn == null) { //3.从数据源(连接池)中获取一个连接, conn = dataSource.getConnection(); //并且存入ThreadLocal中 tl.set(conn); } //4.返回当前线程上的连接 return conn; }catch (Exception e){ throw new RuntimeException(e); } } //把连接和线程解绑 public void removeConnection(){ tl.remove(); } }
getThreadConnection()方法实现了当前线程上有一个连接了。
做个不恰当的比喻,从表面上看ThreadLocal相当于维护了一个map,key就是当前的线程,value就是需要存储的对象。
控制事务:1、先将自动提交改成手动提交,即setAutoCommit(false),2、通过commit和rollback方法对事务进行提交或回滚,
2、创建事务管理相关的工具类TransactionManager,它包含了开启事务、提交事务、回滚事务、释放连接这四个事务操作的方法,先通过ConnectionUtils工具类获取当前线程的连接Connection,
/** * 和事务管理相关的工具类,它包含了,开启事务,提交事务,回滚事务和释放连接 */ public class TransactionManager { private ConnectionUtils connectionUtils; public void setConnectionUtils(ConnectionUtils connectionUtils) { this.connectionUtils = connectionUtils; } public void beginTransaction(){// 开启事务 try { connectionUtils.getThreadConnection().setAutoCommit(false); }catch (Exception e){ e.printStackTrace(); } } public void commit(){//提交事务 try { connectionUtils.getThreadConnection().commit(); }catch (Exception e){ e.printStackTrace(); } } public void rollback(){//回滚事务 try { connectionUtils.getThreadConnection().rollback(); }catch (Exception e){ e.printStackTrace(); } } public void release(){//释放连接 try { connectionUtils.getThreadConnection().close();//还回连接池中 connectionUtils.removeConnection();//把连接和线程解绑 }catch (Exception e){ e.printStackTrace(); } } }
我们的连接都使用了连接池,服务器也有一个池的技术叫线程池,它的特点是当tomcat初始化一大堆线程放在一个容器中,每次访问都是从线程池中拿出一个线程给我们使用,线程池中的线程和连接池中的连接一样,调用close方法并不是关闭,而是还回线程池中。由于线程中绑定了一个连接,当我们把连接还回连接池并把线程还回池中时,线程上时有连接的,只不过连接已经被关闭(还回池中)了,当我们下次再获取这个线程并判断线程上面有没有连接时,你得到的结果一定是有,但是这个连接已经不能用了,因为这个连接已经还回连接池中了,所以我们在用完线程之后,应该把线程与连接进行解绑。当然现在是java工程,不涉及这个问题,而web应用开发(JavaEE)就会涉及这个问题。
事务控制就是在service中进行四个事务操作,在dao中改为一个连接。
3、对service进行改造:进行事务控制。service中需要用到TransactionManager工具类中的开启事务、提交事务、回滚事务、释放连接这四个事务操作的方法,故在service中要添加TransactionManager对象。现在可以对新增账户、修改账户、删除账户、查询所有账户、查询一个账户在业务层进行事务控制。以下为对转账案例进行事务控制
public class AccountServiceImpl_OLD implements IAccountService{ private IAccountDao accountDao; private TransactionManager txManager; public void setTxManager(TransactionManager txManager) { this.txManager = txManager; } public void setAccountDao(IAccountDao accountDao) { this.accountDao = accountDao; } @Override public void transfer(String sourceName, String targetName, Float money) { try { txManager.beginTransaction();//1.开启事务 //2.执行操作 //2.1根据名称查询转出账户 Account source = accountDao.findAccountByName(sourceName); //2.2根据名称查询转入账户 Account target = accountDao.findAccountByName(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); txManager.commit();//3.提交事务 }catch (Exception e){ txManager.rollback();//4.回滚操作 e.printStackTrace(); }finally { txManager.release();//5.将连接还回连接池中并将连接与线程解绑 } } @Override public void updateAccount(Account account) { try { //1.开启事务 txManager.beginTransaction(); //2.执行操作 accountDao.updateAccount(account); //3.提交事务 txManager.commit(); }catch (Exception e){ e.printStackTrace(); //4.回滚操作 txManager.rollback(); }finally { //5.释放连接 txManager.release(); } } }
4、改造dao的实现类:每条sql语句处于独立的事务改造成多条sql语句共用同一个事务。即不在QueryRunner中注入DataSource参数,这样就不是每次执行一条sql语句就获取一个新的连接,即每次与数据库交互都处于独立的事务之中,而是改成四次与数据库的交互应该由同一个Connection来控制,即只有一个Connection对象,该connection为与当前线程绑定的连接对象。
如果在QueryRunner构造函数中传递了参数DataSource,
<!--配置QueryRunner--> <bean id="runner" class="org.apache.commons.dbutils.QueryRunner" scope="prototype"> <!--注入数据源--> <constructor-arg name="ds" ref="dataSource"></constructor-arg> </bean>
则每次都会从连接池获取一个新的连接:Conn =dataSource.getConnection();获取连接之后就执行sql语句,则每条sql语句处于独立的事务之中,这种方式不能实现web阶段的转账,所以说不传DataSource来创建QueryRunner对象,
改为以下:
<!--配置QueryRunner--> <bean id="runner" class="org.apache.commons.dbutils.QueryRunner" scope="prototype"></bean>
就是为了保证多条语句共用同一个事务,就是为了实现像转账那样一个业务中有多次与数据库交互的过程。
但是如果QueryRunner构造函数中不传入DataSource参数,那么dao中的操作会没有Connection,然后在新增账户、修改账户、删除账户、查询所有账户、查询一个账户等方法中选择带有connection对象的query方法,这样一条sql语句或多条sql语句都可以实现事务控制。
public class AccountDaoImpl implements IAccountDao { private QueryRunner runner; public void setRunner(QueryRunner runner) { this.runner = runner; } private ConnectionUtils connectionUtils; public void setConnectionUtils(ConnectionUtils connectionUtils) { this.connectionUtils = connectionUtils; } @Override public Account findAccountByName(String accountName) { try{ List<Account> accounts = runner.query(connectionUtils.getThreadConnection(), "select * from account1 where name = ? ", new BeanListHandler<Account>(Account.class),accountName); if(accounts == null || accounts.size() == 0){ return null; } if(accounts.size() > 1){ throw new RuntimeException("结果集不唯一,数据有问题"); } return accounts.get(0); }catch (Exception e) { throw new RuntimeException(e); } } @Override public void updateAccount(Account account) { try{ runner.update(connectionUtils.getThreadConnection(),"update account1 set name=?,money=? where id=?",account.getName(),account.getMoney(),
account.getId()); }catch (Exception e) { throw new RuntimeException(e); } } }
5、修改bean.xml。
先配置事务管理器TransactionManager和ConnectionUtils工具类
<!-- 配置Connection的工具类 ConnectionUtils --> <bean id="connectionUtils" class="com.itheima.utils.ConnectionUtils"> <!-- 注入数据源--> <property name="dataSource" ref="dataSource"></property> </bean> <!-- 配置事务管理器--> <bean id="txManager" class="com.itheima.utils.TransactionManager"> <!-- 注入ConnectionUtils --> <property name="connectionUtils" ref="connectionUtils"></property> </bean>
在service中注入新建的依赖TransactionManager
<!-- 配置Service --> <bean id="accountService" class="com.itheima.service.impl.AccountServiceImpl_OLD"> <!-- 注入dao --> <property name="accountDao" ref="accountDao"></property> <!-- 注入事务管理器 --> <property name="txManager" ref="txManager"></property> </bean>
在sdao中注入新建的依赖ConnectionUtils:
<!--配置Dao对象--> <bean id="accountDao" class="com.itheima.dao.impl.AccountDaoImpl"> <!-- 注入QueryRunner --> <property name="runner" ref="runner"></property> <!-- 注入ConnectionUtils --> <property name="connectionUtils" ref="connectionUtils"></property> </bean>
再次进行测试,当没有添加int i=1/0时,正常转账。 如果在更新转出账户之后出现了异常,则转出账户的钱没少100,而转入账户的钱未增加100.保证了事务的一致性,
上面虽然实现了事务控制,但是service的实现类中多了很多重复的代码。
虽然事务控制由持久层回到了业务层,转账可以正常执行了,但是存在三个问题:
1、 配置变得非常麻烦,
2、 service的实现类有很多重复代码,不简洁,
3、 方法之间的依赖,如果将TransactionManager工具类中的某个方法的名称改变了,则所有用到这个方法的地方都会报红,所以应该尽量保证方法之间的独立,让它更加的灵活,而不是有这种紧密的依赖关系。
那么能不能在不改变原有代码的情况下仍然能实现事务的支持呢?动态代理可以解决该问题。