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


    本教程源码请访问: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,从而解决上述问题。

  • 相关阅读:
    【转】JSON.parse() Unexpected token i in JSON at position 2 报错问题
    修改json对象的每一个值
    浏览器各个版本和系统(chrome/safari/edge/qq/360)
    数据库书籍推荐排行榜
    git-将dev代码合并到test
    npm install报错
    slice()和splice()区别
    在Eclipse中使用JUnit4进行单元测试(初级篇)
    [转载]Jmeter那点事·ForEach和If控制器
    java语言写文件内容
  • 原文地址:https://www.cnblogs.com/codeaction/p/12988739.html
Copyright © 2020-2023  润新知