Spring08_纯注解实战_支持事务版本

折月煮酒 提交于 2020-08-11 16:10:02

本教程源码请访问: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在数据库中钱减少。

转账流程分析

  1. 转账流程大的原则是源账户钱减少,目的账户钱增加
  2. 源账户钱减少,目的账户钱增加的操作要么同时成功,要么同时失败
  3. 为了满足同时成功,同时失败应该将相关操作包裹在同一个事务当中;
  4. 同一个事务操作应该使用同一个连接(Connection)。

目前存在的问题:当前转账流程没有开启事务,所有操作使用独立的连接。

二、代码升级

根据前面的分析,我们要对工程进行修改,使其支持事务,应该满足的原则如下:

  1. 在Service层处理事务(开启事务,提交,回滚);
  2. Dao层只负责数据库操作不负责业务,也就是说每个Dao操作只进行最细粒度的CRUD操作;
  3. 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();
	}
}

说明

  1. 这个工程使用了ThreadLocal,保证能够在多线程环境下使用;
  2. 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、目前代码存在的问题

目前的代码已经能够实现对事务的支持了,但是仍然存在一些问题,问题如下:

  1. 代码过于繁琐,Service层实现类相比于上一个版本太繁琐了,除了要负责业务还要负责事务的操作;
  2. 如果JdbcUtils类中的和事务操作相关的方法有一个修改,那么Service层代码相应的调用位置都要修改,如果项目很庞大,对于开发人员来说这简直就是噩梦。

后面的几节我们会深入的学习Spring AOP,从而解决上述问题。

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!