JDBC的三层和事务控制

现有问题:用户交互代码与业务代码混杂在一起,将来更换交互方式势必会影响业务,耦合度高,
不利于重用与维护。
如图所示:在这里插入图片描述

1 三层结构

将访问数据库、业务处理、用户交互分为三个模块,遵循单一职责,各司其职,降低耦合。在这里插入图片描述
调用过程,如图所示:
在这里插入图片描述

1 业务层设计(service)

概念:根据业务需求,调用Dao实现具体业务功能的代码。
Service设计规范:

1.Service通常由接口+实现类组成,便于解耦合。
2.一个Service对应一张表的业务,例如t_person业务对应 PersonService,t_user对应 UserService3.Service接口的实现类命名规则:接口+impl,例如PersonService,实现类为 PersonServiceImpl4.所有接口放置在service包中,所有实现类放在service.impl子包中。
5.接口中方法的命名与业务相关例如注册 register,登陆 login 等 如果业务简单无法形容,参考Dao方法命名方式,替换首字母:
 1)添加:add为首单词。
 2)删除:remove为首单词。
 3)修改:change为首单词。
 4)查询:find为首单词。

演示:AccountService接口(方法从实际业务中抽取)的代码如下:

package com.txw.service;

import com.txw.entity.Account;
/**
 * 账户业务层
 * @Author Adair
 * @QQ:[email protected]
 * @Date 2021/10/19  上午 9:07
 */
@SuppressWarnings("all")    //  注解警告信息
public interface AccountService {
    
    

    /**
     * 注册
     * @param acc
     */
    public void register(Account acc) throws Exception;
    /**
     * 存款业务
     * @param username
     * @param password
     * @param money
     */
    public void deposit(String username,String password,double money) throws Exception;

    /**
     * 取款业务
      * @param username
     * @param password
     * @param money
     */
    public void withdraw(String username,String password, double money) throws Exception;

    /**
     * 转账业务
     * @param username
     * @param password
     * @param money
     * @param toUsername
     */
    public void transfer(String username,String password,String toUsername,Double money)throws Exception;
}

如图所示:
在这里插入图片描述
AccountServiceImpl实现类的代码如下:

package com.txw.service.impl;

import com.txw.dao.AccountDao;
import com.txw.dao.impl.AccountDaoImpl;
import com.txw.entity.Account;
import com.txw.service.AccountService;
/**
 * 账户业务层实现类
 * @Author Adair
 * @QQ:[email protected]
 * @Date 2021/10/19  上午 9:26
 */
@SuppressWarnings("all")    //  注解警告信息
public class AccountServiceImpl implements AccountService {
    
    

    /**
     * 注册
     * @param acc
     * @throws Exception
     */
    @Override
    public void register(Account acc) throws Exception {
    
    
        AccountDao ad = new AccountDaoImpl();
        // 1.验证用户是否已存在
        // 1-1调用dao查询是否有相同用户名Account信息
        Account account = ad.selectAccountByUsername(acc.getUsername());
        // 1-2如果account!=null 则证明已有相同用户名的Account信息,拒绝注册
        if(account!=null){
    
    
            System.out.println("用户名重复!");
            return;
        }
        // 2.插入Account信息
        ad.insertAccount(acc);
    }

    /**
     * 存款业务
     * @param username
     * @param password
     * @param money
     * @throws Exception
     */
    @Override
    public void deposit(String username, String password, double money) throws Exception {
    
    

    }

    /**
     * 取款业务
     * @param username
     * @param password
     * @param money
     * @throws Exception
     */
    @Override
    public void withdraw(String username, String password, double money) throws Exception {
    
    

    }

    /**
     *  转账业务
     * @param username
     * @param password
     * @param money
     * @param toUsername
     * @throws Exception
     */
    @Override
    public void transfer(String username, String password, String toUsername,Double money) throws Exception {
    
    
        try {
    
    
            AccountDao ad = new AccountDaoImpl();
            // 1.调用dao根据account_name与password查询一行数据
            Account accA = ad.selectAccountByUsernameAndPassword(username, password);
            // 2.验证用户名密码是否正确,如果不正确不允许转账
            if(accA==null){
    
    
                System.out.println("用户名密码错误!");
                return;
            }
            // 3.调用dao根据account_name查询一行数据
            Account accB = ad.selectAccountByUsername(toUsername);
            // 4.如果账户不存在,不允许转账
            if(accB==null){
    
    
                System.out.println("对方账户不存在!");
                return;
            }
            // 5.验证A账户余额是否充足,如果不充足,不允许转账
            if(accA.getBalance()<money){
    
    
                System.out.println("余额不足!");
                return;
            }
            // 6.扣除A账户余额
            accA.setBalance( accA.getBalance()- money );
            // 7.调用dao修改一行数据
            ad.updateAccount(accA);
            // 8.为B账户增加余额
            accB.setBalance( accB.getBalance()+ money );
            // 9.调用dao修改一行数据
            ad.updateAccount( accB );
        }catch(Exception e){
    
    
            e.printStackTrace();
        }
    }
}

如图所示:
在这里插入图片描述
编写Account的代码如下:

package com.txw.entity;

import java.io.Serializable;
/**
 * 账户实体类
 * @Author Adair
 * @QQ:[email protected]
 * @Date 2021/10/19  上午 9:12
 */
@SuppressWarnings("all")    //  注解警告信
public class Account implements Serializable {
    
    
    Integer accountId;
    String username;
    String password;
    Double balance;

    public Account() {
    
    
    }

    public Account(Integer accountId, String username, String password, Double balance) {
    
    
        this.accountId = accountId;
        this.username = username;
        this.password = password;
        this.balance = balance;
    }

    public Integer getAccountId() {
    
    
        return accountId;
    }

    public void setAccountId(Integer accountId) {
    
    
        this.accountId = accountId;
    }

    public String getUsername() {
    
    
        return username;
    }

    public void setUsername(String username) {
    
    
        this.username = username;
    }

    public String getPassword() {
    
    
        return password;
    }

    public void setPassword(String password) {
    
    
        this.password = password;
    }

    public Double getBalance() {
    
    
        return balance;
    }

    public void setBalance(Double balance) {
    
    
        this.balance = balance;
    }

    @Override
    public String toString() {
    
    
        return "Account{" +
                "accountId=" + accountId +
                ", username='" + username + '\'' +
                ", password='" + password + '\'' +
                ", balance=" + balance +
                '}';
    }
}

如图所示:在这里插入图片描述
创建account数据库以及表结构的SQL语句如下:

create table t_account(
	accout_id int primary key auto_increment,
	username varchar(20),
	password varchar(20),
	balance  double
)

如图所示:在这里插入图片描述
在com.txw.config包下创建jdbc.properties的代码如下:

driverClassName=com.mysql.cj.jdbc.Driver
username=root
password=123456
url=jdbc:mysql://192.168.64.128:3306/test?useUnicode=true&characterEncoding=UTF-8&uesSSL=false&serverTimezone=Asia/Shanghai

如图所示:在这里插入图片描述

2 视图层设计(view)(了解)

概念:视图层负责与用户交互可以接收用户提交的数据或展示用户操作的结果。
视图层要放到view包中,由于目前视图使用console窗口临时代替,无其他要求。
演示:AccountSystemView的代码如下:

package com.txw.view;

import com.txw.entity.Account;
import com.txw.service.AccountService;
import com.txw.service.impl.AccountServiceImpl;
import java.util.Scanner;
/**
 * 账户系统视图
 * @Author Adair
 * @QQ:[email protected]
 * @Date 2021/10/19  上午 10:15
 */
@SuppressWarnings("all")    //  注解警告信息
public class AccountSystemView {
    
    
    // 由于每个视图方法都要使用Scanner所以声明为属性,被static修饰可以被静态方法使用
    private static Scanner sc = new Scanner(System.in);
    public static void main(String[] args) throws Exception {
    
    
        System.out.println("欢迎使用本公司账户管理系统");
        System.out.println("请输入业务编号:");
        int n = sc.nextInt();
        System.out.println("1:存款");
        System.out.println("2:取款");
        System.out.println("3:转账");
        switch (n){
    
    
            case 1:
                // 调用注册方法
                register();
                break;
            case 2:
                // 调用存款方法
                deposit();
                break;
            case 3:
                // 调用取款方法
                withdraw();
                break;
            case 4:
                // 调用转账方法
                transfer();
                break;

        }
    }
    // 对应业务的视图方法

    /**
     * 调用注册方法
     */
    public static void register(){
    
    
        try {
    
    
            // 1.用户输入用户名密码
            System.out.println("请输入用户名:");
            String username = sc.next();
            System.out.println("请输入密码:");
            String password = sc.next();
            // 调用service
            AccountService as = new AccountServiceImpl();
            Account account = new Account(null,username,password,null);
            as.register(account);
        } catch (Exception e) {
    
    
            e.printStackTrace();
        }
    }
    
    /**
     * 调用存款方法
     * @throws Exception
     */
    public static void deposit() {
    
    
        try {
    
    
            // 1.用户输入用户名密码
            System.out.println("请输入用户名:");
            String username = sc.next();
            System.out.println("请输入密码:");
            String password = sc.next();
            System.out.println("输入存款金额:");
            double money = sc.nextDouble();
            // 创建Service调用业务
            AccountService acs = new AccountServiceImpl();
            // 调用存款业务方法
            acs.deposit(username,password,money);
        } catch (Exception e) {
    
    
            e.printStackTrace();
        }
    }

    /**
     * 调用取款方法
     */
    public static void withdraw(){
    
    
        // 1.用户输入用户名密码
        // 2.调用dao根据account_name与password查询一行数据
        // 3.验证用户名密码是否正确,如果不正确不允许取款
        // 4.验证余额是否充足,如果不充足,不允许取款
        // 5.扣除账户余额
        // 6.调用dao修改一行数据
    }

    /**
     * 调用转账方法
     * @throws Exception
     */
    public static void transfer() {
    
    
        try {
    
    
            Scanner sc = new Scanner(System.in);
            System.out.println("请输入用户名密码:");
            String usernameA = sc.next();
            String password = sc.next();
            //4.用户输入B账户名称
            System.out.println("请输入对方账户名称:");
            String usernameB = sc.next();
            //7.用户输入转账金额
            System.out.println("请输入转账金额:");
            Double balance = sc.nextDouble();
            //使用AccountService完成业务
            AccountService as = new AccountServiceImpl();
            as.transfer(usernameA,password,usernameB,balance);
        }catch(Exception e){
    
    
            e.printStackTrace();
        }
    }
}

如图所示:
在这里插入图片描述
包结构示意图,如图所示:在这里插入图片描述

扫描二维码关注公众号,回复: 13368571 查看本文章

2 事务控制

概念:一个完整的业务就代表一个事务,我们在一个业务中会多次调用Dao进行CRUD,这些SQL语句要么一起成功,要么一起失败。
演示:转账方法失败的代码如下:

/**
     *  转账业务
     * @param username
     * @param password
     * @param money
     * @param toUsername
     * @throws Exception
     */
    @Override
    public void transfer(String username, String password, String toUsername,Double money) throws Exception {
    
    
        try {
    
    
            AccountDao accountDao = new AccountDaoImpl();
            // 1.调用dao根据account_name与password查询一行数据
            Account accA = accountDao.selectAccountByUsernameAndPassword(username, password);
            // 2.验证用户名密码是否正确,如果不正确不允许转账
            if(accA == null){
    
    
                throw new RuntimeException("用户名/密码错误!");
            }
            // 调用dao查询用户是否存在
            Account accB = accountDao.selectAccountByUsername(toUsername);
            if(accB == null){
    
    
                throw new RuntimeException("对方账号不存在!");
            }
            if(money > accA.getBalance()){
    
    
                throw new RuntimeException("余额不足!");
            }
            // 扣除余额
            accA.setBalance( accA.getBalance() - money );
            accountDao.updateAccount(accA);
            if(1==1)
                throw new RuntimeException("业务失败");
            // 添加余额
            accB.setBalance(accB.getBalance() + money );
            accountDao.updateAccount( accB );
        }catch(Exception e){
    
    
            e.printStackTrace();
        }
    }
}

如图所示:在这里插入图片描述
1.控制事务的方法
所有的数据库操作都要基于链接(Connection)的基础上,事务的控制也不例外Connection中控制事务的方法。如图所示:
在这里插入图片描述
3.控制事务的位置
业务开始前设置为手动提交,业务结束后提交事务,出现异常回滚事务。
演示的代码如下:

public void transfer(String username, String password, String toUsername,Double money) throws Exception {
    
    
        Connection conn = null;
        try {
    
    
            // 获取链接,一条链接一个事务
            conn = JDBCUtilsPlusProMax.getConnection();
            System.out.println("AccountService--transfer--"+conn);
            //设置事务提交方式为手动提交
            conn.setAutoCommit(false);
            System.out.println("-----begin transaction----");

            AccountDao ad = new AccountDaoImpl();
            //1.调用dao根据account_name与password查询一行数据
            Account accA = ad.selectAccountByUsernameAndPassword(username, password);
            // 2.验证用户名密码是否正确,如果不正确不允许转账
            if(accA==null){
    
    
                System.out.println("用户名密码错误!");
                return;
            }
            // 3.调用dao根据account_name查询一行数据
            Account accB = ad.selectAccountByUsername(toUsername);
            // 4.如果账户不存在,不允许转账
            if(accB==null){
    
    
                System.out.println("对方账户不存在!");
                return;
            }
            // 5.验证A账户余额是否充足,如果不充足,不允许转账
            if(accA.getBalance()<money){
    
    
                System.out.println("余额不足!");
                return;
            }
            // 6.扣除A账户余额
            accA.setBalance(accA.getBalance() - money);
            // 7.调用dao修改一行数据
            ad.updateAccount(accA);
            // if(1==1)throw new RuntimeException("业务失败");
            // 8.为B账户增加余额
            accB.setBalance(accB.getBalance() + money);
            // 9.调用dao修改一行数据
            ad.updateAccount(accB);
            // 所有业务代码执行完毕后,提交事务
            conn.commit();
            System.out.println("-----commit----");
        }catch(Exception e){
    
    
            e.printStackTrace();
            conn.rollback();        // 回滚
            System.out.println("-----rollback----");
        }finally{
    
    
            JDBCUtilsPlusProMax.close(null,null,conn);
        }
    }

如图所示:
在这里插入图片描述

注意:实战开发中,无论业务层方法有多简单,都一定要添加事务控制。
3.链接共享问题
由于事务控制是在链接的基础上进行,所以必须保证控制事务的链接与发送SQL的链接是同一个链接。
例:Service与Dao的获取的链接并不是共享链接,所以控制事务的失败。如图所示:在这里插入图片描述
解决步骤

  1. 修改JDBCUtils加入判断,如果链接已创建则不创建新的链接。
    演示的代码如下:
package com.txw.util;

import java.io.InputStream;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.Properties;
/**
 * JDBC工具类
 * @Author Adair
 * @QQ:[email protected]
 * @Date 2021/10/20  上午 11:35
 */
@SuppressWarnings("all")    //  注解警告信息
public class JDBCUtils {
    
    
    private static Properties p = new Properties();
    // 1.将Connection对象提取为静态属性,不用重复创建
    private static Connection conn = null;
    static {
    
    
        InputStream is = JDBCUtilsPlus.class.getResourceAsStream("/com/txw/config/jdbc.properties");
        try {
    
    
            p.load(is);
        } catch (Exception e) {
    
    
            e.printStackTrace();
        }
    }
    public static Connection getConnection(){
    
    
        try {
    
    
            // 2.加入if判断
            // 如果是第二次调用conn不为null,则不创建
            if (conn == null){
    
    
                // 使用getProperty方法根据key获取对应的value
                // 1.加载驱动
                Class.forName(p.getProperty("driverClassName"));
                // 2.创建链接
                String dbUsername = p.getProperty("username"); 
                String dbPassword = p.getProperty("password"); 
                String url = p.getProperty("url"); 
                conn = DriverManager.getConnection(url, dbUsername, dbPassword);
            }
        }catch (Exception e){
    
    
            e.printStackTrace();
        }
        return conn;
    }
    
    /**
            * 关闭资源 ResultSet  PreparedStatement Connection
     *     需要关闭哪个资源就传入哪个对象,如果没有资源需要关闭则传入null
     *     例:JDBCUtils.close(rs,pstm,conn);关闭ResultSet、PreparedStatement、Connection
     *     JDBCUtils.close(null,pstm,conn);关闭PreparedStatement、Connection
     * @param rs
     * @param pstm
     * @param conn
     */
    public static void close(ResultSet rs, PreparedStatement pstm, Connection conn){
    
    
        try{
    
    if(rs!=null)rs.close();}catch(Exception e){
    
    e.printStackTrace();}
        try{
    
    if(pstm!=null)pstm.close();}catch(Exception e){
    
    e.printStackTrace();}
        try{
    
    if(conn!=null)conn.close();}catch(Exception e){
    
    e.printStackTrace();}
    }
}

如图所示:在这里插入图片描述
AccountDaoImpl的代码如下:

package com.txw.dao.impl;

import com.txw.dao.AccountDao;
import com.txw.entity.Account;
import com.txw.util.JDBCUtils;
import com.txw.util.JDBCUtilsPlus;
import com.txw.util.JDBCUtilsPlusProMax;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.ArrayList;
import java.util.List;
/**
 * 业务持久层实现类
 * @Author Adair
 * @QQ:[email protected]
 * @Date 2021/10/19 0019 上午 9:32
 */
@SuppressWarnings("all")    //  注解警告信息
public class AccountDaoImpl implements AccountDao {
    
    
    private Account acc = null;
    private Connection conn = null;
    private PreparedStatement pstm = null;
    private ResultSet rs = null;

    /**
     * 添加账户
     * @param account
     */
    @Override
    public void insertAccount(Account acc) throws Exception{
    
    
        conn = JDBCUtilsPlus.getConnection();
        String sql = "insert into t_account values(null,?,?,?)";
        pstm = conn.prepareStatement(sql);
        pstm.setString(1,acc.getUsername());
        pstm.setString(2,acc.getPassword());
        pstm.setDouble(3,0.0); // 新账户余额为0
        pstm.executeUpdate();
       JDBCUtilsPlus.close(null,pstm, conn);
    }

    /**
     * 根据用户名查询账户
     * @param username
     * @return
     */
    @Override
    public Account selectAccountByUsername(String username) throws Exception{
    
    
        // 加载驱动,获取链接
        conn = JDBCUtilsPlus.getConnection();
        // 准备sql 创建执行sql的工具
        String sql = "select account_id,username,password,balance from t_account where username=?;";
        PreparedStatement pstm  = conn.prepareStatement(sql);
        pstm.setString(1,username);
        // 执行sql
        ResultSet rs = pstm.executeQuery();
        Account account = null;
        // 处理结果集
        if(rs.next()){
    
    
            int accountId = rs.getInt("account_id");
            String uname = rs.getString("username");
            String paw = rs.getString("password");
            double balance = rs.getDouble("balance");
            account = new Account(accountId,uname,paw,balance);
        }
        JDBCUtilsPlus.close(rs,pstm,conn);
        return account;
    }

    /**
     * 根据用户名和密码查询账户
     * @param username
     * @param password
     * @return
     */
    @Override
    public Account selectAccountByUsernameAndPassword(String username, String password) throws Exception{
    
    
        // 加载驱动,获取链接
        conn = JDBCUtilsPlus.getConnection();
        // 准备sql 创建执行sql的工具
        String sql = "select account_id,username,password,balance from t_account where username=? and password=?;";
        pstm  = conn.prepareStatement(sql);
        pstm.setString(1,username);
        pstm.setString(2,password);
        // 执行sql
        ResultSet rs = pstm.executeQuery();
        Account account = null;
        // 处理结果集
        if(rs.next()){
    
    
            int accountId = rs.getInt("account_id");
            String uname = rs.getString("username");
            String paw = rs.getString("password");
            double balance = rs.getDouble("balance");
            account = new Account(accountId,uname,paw,balance);
        }
        JDBCUtilsPlus.close(rs,pstm,conn);
        return account;
    }

    /**
     * 修改账户
     * @param account
     */
    @Override
    public void updateAccount(Account account) throws Exception{
    
    
        conn = JDBCUtilsPlusProMax.getConnection();
        System.out.println("AccountDao--update--"+conn);
        String sql = "update t_account set username=?,password=?,balance=? where account_id = ?;";
        pstm  = conn.prepareStatement( sql );
        pstm.setString(1,acc.getUsername());
        pstm.setString(2,acc.getPassword());
        pstm.setDouble(3,acc.getBalance());
        pstm.setInt(4,acc.getAccountId());
        pstm.executeUpdate();
        JDBCUtilsPlusProMax.close(null,pstm,null);
    }

    /**
     * 查询所有数据
     * @return
     * @throws Exception
     */
    @Override
    public List<Account> selectAccounts() throws Exception {
    
    
        // 加载驱动,获取链接
        conn = JDBCUtilsPlus.getConnection();
        // 准备sql 创建执行sql的工具
        String sql = "select * from t_account";
        pstm  = conn.prepareStatement(sql);
        // 执行sql
        rs = pstm.executeQuery();
        List<Account> list = new ArrayList<>();
        Account account = null;
        // 处理结果集
        if(rs.next()){
    
    
            int accountId = rs.getInt("account_id");
            String uname = rs.getString("username");
            String paw = rs.getString("password");
            double balance = rs.getDouble("balance");
            account = new Account(accountId,uname,paw,balance);
            list.add(account);
        }
        JDBCUtilsPlus.close(rs,pstm,conn);
        return list;
    }

    /**
     * 根据id删除一行数据
     * @param id
     * @throws Exception
     */
    @Override
    public void deleteAccountById(Integer id) throws Exception {
    
    
        // 加载驱动,获取链接
        conn = JDBCUtilsPlus.getConnection();
        // 准备发送SQL
        String sql = "delete from t_account where account_id = ?";
        pstm = conn.prepareStatement(sql);
        pstm.setInt(1,id);
        // 执行SQL
        pstm.executeUpdate();
        // 关闭资源
        JDBCUtilsPlus.close(null,pstm,conn);
    }
}

实现步骤:

1.Connection提取为属性,避免因局部变量导致每次调用getConnection都会重新创建。
2.加入if判断,如果没有Connection则创建,否则不创建。
  1. 由于service需要使用链接对象提交事务,所以dao不再负责关闭链接。
    演示的代码如下:
 /**
     * 根据用户名查询账户
     * @param username
     * @return
     */
    @Override
    public Account selectAccountByUsername(String username) throws Exception{
    
    
        // 加载驱动,获取链接
        conn = JDBCUtilsPlus.getConnection();
        // 准备sql 创建执行sql的工具
        String sql = "select account_id,username,password,balance from t_account where username=?;";
        pstm  = conn.prepareStatement(sql);
        pstm.setString(1,username);
        // 执行sql
        rs = pstm.executeQuery();
        Account account = null;
        // 处理结果集
        if(rs.next()){
    
    
            int accountId = rs.getInt("account_id");
            String uname = rs.getString("username");
            String paw = rs.getString("password");
            double balance = rs.getDouble("balance");
            account = new Account(accountId,uname,paw,balance);
        }
        //不再关闭链接资源
        JDBCUtils.close(rs,pstm,null);
        return account;
    }

如图所示:在这里插入图片描述
测试多次获取链接
演示的代码如下:

package com.txw.test;

import com.txw.util.JDBCUtilsPlus;
import java.sql.Connection;
/**
 * JDBC测试类
 * @Author Adair
 * @QQ:[email protected]
 * @Date 2021/10/20  上午 10:52
 */
@SuppressWarnings("all")    //  注解警告信息
public class TestJDBCUtils {
    
    
    public static void main(String[] args) {
    
    
        // 测试工具类
        Connection conn1 = JDBCUtils.getConnection();
        System.out.println(conn1);
        // 加载驱动、获取链接
        Connection conn2 = JDBCUtils.getConnection();
        System.out.println(conn2);
        // 执行SQL
        // 处理结果
        // 关闭资源
    }
}

如图所示:在这里插入图片描述

3 多线程环境中的事务控制

多个用户就是多个线程,当系统暴露在多用户使用的情况时则出现新的问题。
Connection对象过度共享,所有线程都用一个,如果线程1将链接关闭,线程2将无法使用。
演示的代码如下:

package com.txw.test;

import com.txw.service.AccountService;
import com.txw.service.impl.AccountServiceImpl;
/**
 * 测试账户业务层
 * @Author Adair
 * @QQ:[email protected]
 * @Date 2021/10/20 上午 11:20
 */
@SuppressWarnings("all")    //  注解警告信息
public class TestAccountService {
    
    
    public static void main(String[] args) {
    
    
        AccountService accountService = new AccountServiceImpl();
        Thread t1 = new Thread(){
    
    
            @Override
            public void run() {
    
    
                try {
    
    
                    accountService.transfer("Adair","123456","Adair999",100.00);
                } catch (Exception e) {
    
    
                    e.printStackTrace();
                }
            }
        };
        Thread t2 = new Thread(){
    
    
            @Override
            public void run() {
    
    
                try {
    
    
                    Thread.sleep(1000);
                    accountService.transfer("Adair","123456","Adair666",200.00);
                } catch (Exception e) {
    
    
                    e.printStackTrace();
                }
            }
        };
        // 启动线程
        t1.start();
        t2.start();
    }
}

如图所示:
在这里插入图片描述
核心问题:Connection对象存储在JDBCUtils中,该对象可被多个线程共享使用。
解决方案:将Connection存储在线程中,为每个线程分配一个Connection对象。

  1. ThreadLocal
    概念:ThreadLocal可以为每个线程都开辟一块独立内存空间,可以为线程存储(绑定)一个对象,不会被其他线程获取。
    常用方法,如图所示:
    在这里插入图片描述
    演示的代码如下:
package com.txw.util;

/**
 * 线程测试
 * @Author Adair
 * @QQ:[email protected]
 * @Date 2021/10/20 0020 上午 10:59
 */
@SuppressWarnings("all")    //  注解警告信息
public class ThreadLocalTest {
    
    
    public static void main(String[] args) {
    
    
        // 创建ThreadLocal
        ThreadLocal<String> threadLocal = new ThreadLocal<>();
        Thread t1 = new Thread() {
    
    
            @Override
            public void run() {
    
    
                // 将数据与当前线程绑定
                threadLocal.set("t1----value");
                // 获取当前线程绑定到threadLocal中的数据
                System.out.println(threadLocal.get());   // t1----value
            }
        };
        Thread t2 = new Thread() {
    
    
            @Override
            public void run() {
    
    
                // 模拟网络延迟
                try {
    
    
                    Thread.sleep(300);
                } catch (Exception e) {
    
    
                    e.printStackTrace();
                }
                // 获取当前线程绑定到threadLocal中的数据
                System.out.println(threadLocal.get());       // null
            }
        };
        // 启动线程
        t1.start();
        t2.start();
    }
}

如图所示:
在这里插入图片描述
实现原理:ThreadLocal内部存储了一个Map,以当前线程为key,将绑定的数据作为value形成一个键值对存储到Map中。
比如:
在这里插入图片描述
获取value时只能获取当前线程所对应的value,从而实现存储独立于线程间的数据。
2.修改JDBCUtils
将Connection对象保存到ThreadLocal中,为每个线程分配一个独立的Connection对象。
JDBCUtils最终威力加强版。
演示的代码如下:

package com.txw.util;

import java.io.InputStream;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.Properties;
/**
 * JDBC工具类
 * @Author Adair
 * @QQ:[email protected]
 * @Date 2021/10/20  上午 10:50
 */
@SuppressWarnings("all")    //  注解警告信息
public class JDBCUtilsPlusProMax {
    
    
    // 使用Properties存储配置文件中的key-value
    private static Properties ps = new Properties();
    // 创建ThreadLocal存储链接对象,一个线程绑定一个链接对象
    private static ThreadLocal<Connection> tl = new ThreadLocal<>();
    // 静态代码块只执行一次
    static{
    
    
        try {
    
    
            // 指定配置文件路径
            // 路径中: 第一个/表示src
            InputStream is = JDBCUtilsPlusProMax.class.getResourceAsStream("/com/txw/config/jdbc.properties");
            // 自动从流中读取配置文件并转换为键值对
            ps.load( is );
            // 根据key获取value
            Class.forName(ps.getProperty("driverClassName"));
        }catch(Exception e){
    
    
            e.printStackTrace();
        }
    }

    /**
     * 创建链接对象并返回
     * @return
     */
    public static Connection getConnection(){
    
    
        try {
    
    
            // 从ThreadLocal中获取当前线程存储的链接对象
            Connection conn = tl.get();
            // 如果conn为null创建链接为conn赋值,否则直接返回conn链接对象
            if(conn==null) {
    
    
                // 2. 创建链接
                // 根据key获取对应的value
                String url = ps.getProperty("url");
                String username = ps.getProperty("username");
                String password = ps.getProperty("password");
                // 创建新链接,将链接对象添加到ThreadLocal中与线程绑定再一起
                conn = DriverManager.getConnection(url, username, password);
                // 绑定
                tl.set(conn);
            }
        }catch(Exception e){
    
    
            e.printStackTrace();
        }
        // 返回ThreadLocal中创建的线程对象
        return tl.get();
    }
    /**
     * 关闭资源 ResultSet  PreparedStatement Connection
     *     需要关闭哪个资源就传入哪个对象,如果没有资源需要关闭则传入null
     *     例:JDBCUtils.close(rs,pstm,conn);关闭ResultSet、PreparedStatement、Connection
     *     JDBCUtils.close(null,pstm,conn);关闭PreparedStatement、Connection
     * @param rs
     * @param pstm
     * @param conn
     */
    public static void close(ResultSet rs, PreparedStatement pstm, Connection conn){
    
    
        try{
    
    if(rs!=null)rs.close();}catch(Exception e){
    
    e.printStackTrace();}
        try{
    
    if(pstm!=null)pstm.close();}catch(Exception e){
    
    e.printStackTrace();}
        try{
    
    
            if(conn!=null){
    
    
                conn.close();
                // 删除ThreadLocal中无用的链接,下次使用则创建新的链接
                tl.remove();
            }
        }catch(Exception e){
    
    e.printStackTrace();}
    }
}

如图所示:
在这里插入图片描述
再次测试多线程环境下的业务测试。

4 使用JUnit测试框架进行测试

替换主函数的测试方案
一个要测试的方法就得有一个测试类,测试类数量非常庞大,难以管理。
解决方案:JUnit测试框架
使用思路:使用@Test注解描述普通方法,即可使得该方法拥有和main函数一样的运行能力。
使用JUnit4框架的步骤:
2. 使用框架
使用要求:

  1. 测试类必须是公开类
  2. 测试方法必须是公开的非静态的无参的无返回值的方法
最最简单的普通方法
  1. 注意:同包下尽量不要定义名为Test的类,避免混淆
    要运行的测试方法一定要添加@Test描述。

猜你喜欢

转载自blog.csdn.net/weixin_40055163/article/details/120825906