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工具类中的某个方法的名称改变了,则所有用到这个方法的地方都会报红,所以应该尽量保证方法之间的独立,让它更加的灵活,而不是有这种紧密的依赖关系。

那么能不能在不改变原有代码的情况下仍然能实现事务的支持呢?动态代理可以解决该问题。

 

posted on 2021-04-07 11:48  周文豪  阅读(160)  评论(0编辑  收藏  举报