一、银行转账的功能实现
数据库表的准备
创建数据库表,主要包括三个字段:自增的id、账户名、账户余额
- 不使用MVC架构模式完成账户转账
首先写一个页面,写入转账的账户和金额;并发送post请求
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<%--<base href="http://localhost:8080/bank/">--%>
<base href="${pageContext.request.scheme}://${pageContext.request.serverName}:${pageContext.request.serverPort}${pageContext.request.contextPath}/">
<title>转账</title>
</head>
<body>
<form action="transfer" method="post">
转出账户:<input type="text" name="fromActno"><br>
装入账户:<input type="text" name="toActno"><br>
转账金额:<input type="text" name="money"><br>
<input type="submit" value="转账">
</form>
</body>
</html>
根据发送的数据请求,编写Servlet,连接数据库进行转账操作
package com.bjpowernode.zl.javaweb;
import com.bjpowernode.zl.exception.MoneyNotException;
import com.bjpowernode.zl.exception.ResultException;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.sql.*;
/**
* @Author:朗朗乾坤
* @Package:com.bjpowernode.zl.javaweb
* @Project:mvc-test
* @name:TransferServlet
* @Date:2022/12/23 18:53
*/
@WebServlet("/transfer")
public class TransferServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// 获取前端提交的数据
response.setContentType("text/html;charset=UTF-8");
PrintWriter out = response.getWriter();
String fromActno = request.getParameter("fromActno");
String toActno = request.getParameter("toActno");
double money = Double.parseDouble(request.getParameter("money"));
// 连接数据库,进行转账操作
// 1. 转账之前先要进行查询操作,有没有钱可以转
Connection conn = null;
PreparedStatement ps = null;
PreparedStatement ps2 = null;
PreparedStatement ps3 = null;
ResultSet rs = null;
try {
// 注册驱动
Class.forName("com.mysql.jdbc.Driver");
// 获取连接
String url = "jdbc:mysql://localhost:3306/mvc";
String username = "root";
String password = "123";
conn = DriverManager.getConnection(url, username, password);
// 获取预编译的数据库操作对象
String sql = "select balance from t_act where actno = ?";
ps = conn.prepareStatement(sql);
ps.setString(1,fromActno);
// 执行sql
rs = ps.executeQuery();
// 处理查询结果集
if (rs.next()) {
double balance = rs.getDouble("balance");
if(balance < money){
// 余额不足,抛出异常
throw new MoneyNotException("对不起,余额不足!");
}
// 2. 余额充足,进行转账
// 开启事务(手动提交)
conn.setAutoCommit(false);
String sql2 = "update t_act set balance = balance - ? where actno = ?";
ps2 = conn.prepareStatement(sql2);
ps2.setDouble(1,money);
ps2.setString(2,fromActno);
int count = ps2.executeUpdate();
String sql3 = "update t_act set balance = balance + ? where actno = ?";
ps3 = conn.prepareStatement(sql3);
ps3.setDouble(1,money);
ps3.setString(2,toActno);
// 累计
count += ps3.executeUpdate();
if (count != 2){
throw new ResultException("转账过程出现问题,请联系管理员");
}
// 转账成功
// 手动提交事务
conn.commit();
out.print("转账成功!");
}
} catch (Exception e) {
// 只要出现异常,进行回滚
try {
if (conn != null){
conn.rollback();
}
} catch (SQLException e1) {
e1.printStackTrace();
}
// e.printStackTrace();
// 打印异常的信息
out.print(e.getMessage());
} finally {
// 释放资源
if (rs != null) {
try {
rs.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (ps != null) {
try {
ps.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (ps2 != null) {
try {
ps2.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (ps3 != null) {
try {
ps3.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (conn != null) {
try {
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
}
两个异常处理
package com.bjpowernode.zl.exception;
/**
* @Author:朗朗乾坤
* @Package:com.bjpowernode.zl.exception
* @Project:mvc-test
* @name:MoneyNotException
* @Date:2022/12/23 19:56
*/
public class MoneyNotException extends Exception{
public MoneyNotException() {
}
public MoneyNotException(String msg) {
super(msg);
}
}
package com.bjpowernode.zl.exception;
/**
* @Author:朗朗乾坤
* @Package:com.bjpowernode.zl.exception
* @Project:mvc-test
* @name:ResultException
* @Date:2022/12/23 20:14
*/
public class ResultException extends Exception {
public ResultException() {
}
public ResultException(String message) {
super(message);
}
}
缺点:一个Servlet实现了所有的功能
①负责数据的接收
②负责核心的业务处理
③负责数据库中数据的CRUD
④负责了页面的展示,打印输出信息到浏览器上
原因:没有进行“职能分工”
①没有独立组件的概念,所以没有办法进行代码复用,代码和代码之间的耦合度太高,扩展力太差!
②操作数据库的代码和业务逻辑混杂在一起,很容易出错!
- MVC架构模式的理论基础
(1)系统为什么分层?
希望各司其职,专人干专事。职能分工要明确,这样可以让代码耦合度降低,扩展力增强,组件的可复用性增强。
(2)软件的架构中,一个著名的架构模式:MVC架构模式
①M(Model:数据/业务)、V(View:视图/展示)、C(Controller:控制器)
②C(是核心,是控制,是司令官)
③M(处理业务/处理数据的一个秘书)
④V(负责页面展示的一个秘书)
⑤MVC(实际上就是一个司令官,调度两个秘书)
JDBC工具类的封装
为了下面我们方便写代码,还是先封装一个JDBC工具类,进行数据库连接的操作
driver=com.mysql.jdbc.Driver
url=jdbc:mysql://localhost:3306/mvc
user=root
password=123
package com.bjpowernode.bank.utils;
import java.sql.*;
import java.util.ResourceBundle;
/**
* @Author:朗朗乾坤
* @Package:com.bjpowernode.bank.utils
* @Project:mvc-test
* @name:DBUtil
* @Date:2022/12/24 11:10
*/
public class DBUtil {
private static ResourceBundle bundle = ResourceBundle.getBundle("resources.jdbc");
private static String driver = bundle.getString("driver");
private static String url = bundle.getString("url");
private static String user = bundle.getString("user");
private static String password = bundle.getString("password");
// 先提供给一个私有的构造方法:防止实例化对象
private DBUtil(){}
// 类加载时,注册驱动
static {
try {
Class.forName(driver);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
// 获取连接
public static Connection getConnection() throws SQLException {
Connection connection = DriverManager.getConnection(url, user, password);
return connection;
}
// 关闭资源
public static void close(Connection conn, Statement stmt, ResultSet rs){
if (rs != null) {
try {
rs.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (stmt != null) {
try {
stmt.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (conn != null) {
try {
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
JavaEE设计模式之DAO模式以及DAO的编写
创建一个类AccountDao负责Account数据的增删改查
①什么是DAO?Data Access Object(数据访问对象)
②DAO实际上是一种设计模式,属于JavaEE的设计模式(不是23种设计模式)
③DAO只负责数据表的CRUD,没有任何的业务逻辑在里面。
④没有任何逻辑业务,只负责表中数据的增删改查的对象,就叫做:DAO对象
⑤一般情况下:一张表就对应一个DAO对象
Account类:把查询的结果封装成一个Java对象,封装账户信息的;
注意:一般属性不建议设置为基本数据类型,建议使用包装类,防止null带来的问题
注:这种普通的对象也就做pojo对象(或者javabean对象、或者领域模型对象:domain对象)
package com.bjpowernode.bank.mvc;
/**
* @Author:朗朗乾坤
* @Package:com.bjpowernode.bank.mvc
* @Project:mvc-test
* @name:Account
* @Date:2022/12/24 13:50
*/
public class Account {
// 一般属性不建议设置为基本数据类型,建议使用包装类,防止null带来的问题
// 使用Long和Double就算取出来的数据是null也能直接赋值过去,完全没有问题
private Long id;
private String actno;
private Double balance;
public Account() {
}
public Account(Long id, String actno, Double balance) {
this.id = id;
this.actno = actno;
this.balance = balance;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getActno() {
return actno;
}
public void setActno(String actno) {
this.actno = actno;
}
public Double getBalance() {
return balance;
}
public void setBalance(Double balance) {
this.balance = balance;
}
@Override
public String toString() {
return "Account{" +
"id=" + id +
", actno='" + actno + '\'' +
", balance=" + balance +
'}';
}
}
AccountDao类:只负责数据库的访问,处理数据(增删改查)
DAO中的方法名很固定,一般都是:insert、deleteByXxx、update、selectByXxx、selectAll
package com.bjpowernode.bank.mvc;
import com.bjpowernode.bank.utils.DBUtil;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.*;
/**
* @Author:朗朗乾坤
* @Package:com.bjpowernode.bank.mvc
* @Project:mvc-test
* @name:AccountDao
* @Date:2022/12/24 13:39
*/
public class AccountDao { // 负责数据的增删改查
// 1、插入账户信息,1表示插入成功
public int insert(Account act){
Connection conn = null;
PreparedStatement ps = null;
int count = 0;
try {
conn = DBUtil.getConnection();
String sql = "insert into t_act(actno,balance) value(?,?)";
ps = conn.prepareStatement(sql);
ps.setString(1,act.getActno());
ps.setDouble(2,act.getBalance());
count = ps.executeUpdate();
} catch (SQLException e) {
e.printStackTrace();
} finally {
DBUtil.close(conn,ps,null);
}
return count;
}
///2、根据主键id删除账户
public int deleteById(Long id){
Connection conn = null;
PreparedStatement ps = null;
int count = 0;
try {
conn = DBUtil.getConnection();
String sql = "delete from t_act where id = ?";
ps = conn.prepareStatement(sql);
ps.setLong(1,id);
count = ps.executeUpdate();
} catch (SQLException e) {
e.printStackTrace();
} finally {
DBUtil.close(conn,ps,null);
}
return count;
}
// 3、更新账户
public int update(Account act){
Connection conn = null;
PreparedStatement ps = null;
int count = 0;
try {
conn = DBUtil.getConnection();
String sql = "update t_act set balance = ?,actno = ? where id = ?";
ps = conn.prepareStatement(sql);
ps.setDouble(1,act.getBalance());
ps.setString(2,act.getActno());
ps.setLong(3,act.getId());
count = ps.executeUpdate();
} catch (SQLException e) {
e.printStackTrace();
} finally {
DBUtil.close(conn,ps,null);
}
return count;
}
// 4、根据账号查询账户
public Account selectByActno(String actno){
Connection conn = null;
PreparedStatement ps = null;
ResultSet rs = null;
Account act = null;
try {
conn = DBUtil.getConnection();
String sql = "select id,balance from t_act where actno = ?";
ps = conn.prepareStatement(sql);
ps.setString(1,actno);
rs = ps.executeQuery();
if (rs.next()) {
Long id = rs.getLong("id");
Double balance = rs.getDouble("balance");
// 将结果集封装成java对象
act = new Account();
act.setId(id);
act.setActno(actno);
act.setBalance(balance);
}
} catch (SQLException e) {
e.printStackTrace();
} finally {
DBUtil.close(conn,ps,rs);
}
return act;
}
// 5、获取所有的账户
public List<Account> selectAll(){
Connection conn = null;
PreparedStatement ps = null;
ResultSet rs = null;
List<Account> list = new ArrayList<>();
try {
conn = DBUtil.getConnection();
String sql = "select id,actno,balance from t_act";
ps = conn.prepareStatement(sql);
rs = ps.executeQuery();
while (rs.next()) {
// 取数据
Long id = rs.getLong("id");
String actno = rs.getString("actno");
Double balance = rs.getDouble("balance");
// 将结果集封装成java对象
Account act = new Account();
act.setId(id);
act.setActno(actno);
act.setBalance(balance);
// 放到集合当中
list.add(act);
}
} catch (SQLException e) {
e.printStackTrace();
} finally {
DBUtil.close(conn,ps,rs);
}
return list;
}
}
- 业务层抽取以及业务方法的实现
前面我们已经写了两个类:Account类主要负责封装数据的;AccountDao类主要处理数据的;这里就要再抽取出来一个类AccountService类用来处理逻辑业务的!
①service翻译为:业务,在AccountService类中编写纯业务代码
②业务类一般起名:XxxService、XxxBiz...
package com.bjpowernode.bank.mvc;
import com.bjpowernode.bank.exception.AppException;
import com.bjpowernode.bank.exception.MoneyNotException;
/**
* @Author:朗朗乾坤
* @Package:com.bjpowernode.bank.mvc
* @Project:mvc-test
* @name:AccountService
* @Date:2022/12/24 15:43
* 专门编写业务的
*/
public class AccountService { // 处理业务
// 每一个业务都有可能连接数据库,所以定义在方法的外面
private AccountDao accountDao = new AccountDao();
// 提供一个能够实现转账的业务方法(一个业务对应一个方法)
/**
* 完成转账的业务逻辑
* @param fromActno 转出账号
* @param toActno 转入账号
* @param money 转账金额
*/
public void transfer(String fromActno,String toActno,double money) throws MoneyNotException, AppException {
// 先查询余额是否充足,返回的是一个Account对象
Account fromAct = accountDao.selectByActno(fromActno);
if (fromAct.getBalance() < money){
throw new MoneyNotException("对不起,余额不足");
}
// 程序走到这里,说明余额充足
Account toAct = accountDao.selectByActno(toActno);
// 修改金额(只是修改内存中java对象的余额)
fromAct.setBalance(fromAct.getBalance()-money);
toAct.setBalance(toAct.getBalance()+money);
// 更新数据库中的余额
int count = accountDao.update(fromAct);
count += accountDao.update(toAct);
// 判断
if (count != 2) {
throw new AppException("转账异常!");
}
}
}
AccountServlet类:相当于司令官,进行整个业务的调度
package com.bjpowernode.bank.mvc;
import com.bjpowernode.bank.exception.AppException;
import com.bjpowernode.bank.exception.MoneyNotException;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* @Author:朗朗乾坤
* @Package:com.bjpowernode.bank.mvc
* @Project:mvc-test
* @name:AccountServlet
* @Date:2022/12/24 13:36
*/
@WebServlet("/transfer")
public class AccountServlet extends HttpServlet { // AccountServlet作为Controller
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// 接收数据
String fromActno = request.getParameter("fromActno");
String toActno = request.getParameter("toActno");
double money = Double.parseDouble(request.getParameter("money"));
// 调用业务方法处理业务(调度Model处理业务)
AccountService accountService = new AccountService();
try {
accountService.transfer(fromActno,toActno,money); // 进行转账
// 转账成功
// 展示处理结果(调度View做页面展示)
response.sendRedirect(request.getContextPath()+"/success.jsp");
} catch (MoneyNotException e) {
// 余额不足
// 展示处理结果(调度View做页面展示)
response.sendRedirect(request.getContextPath()+"/money-not-enough.jsp");
} catch (AppException e) {
// 失败
// 展示处理结果(调度View做页面展示)
response.sendRedirect(request.getContextPath()+"/error.jsp");
}
}
}
MVC架构模式与三层架构的关系
通过前面的一步步分层,实际就达到了以下三层架构的模型
(横向)
(纵向)
所以,对于原来的图,就可以进一步细化,展示出:MVC架构模式与三层架构的关系
补充:SSM框架的理解
①Spring:项目大管家,负责整个项目所有对象的创建以及维护对象和对象之间的关系。
②SpringMVC:将MVC结构模式体现的非常完美;在这个框架的基础上开发一定是用了MVC架构模式;实际上SringMVC架构已经把MVC架构的环境已经搭建出来了。
③MyBatis:属于持久化层的框架。
解决事务问题
上述我们已经可以实现所有的功能,但是还没有添加事务,一旦出现异常现象会出现问题。
注: 事务一定是在业务逻辑层(service层)进行控制。
第一种方式:不可以,不是同一个Connection对象,无法控制事务;但是是在同一个线程里面的
package com.bjpowernode.bank.mvc;
import com.bjpowernode.bank.exception.AppException;
import com.bjpowernode.bank.exception.MoneyNotException;
import com.bjpowernode.bank.utils.DBUtil;
import java.sql.Connection;
import java.sql.SQLException;
/**
* @Author:朗朗乾坤
* @Package:com.bjpowernode.bank.mvc
* @Project:mvc-test
* @name:AccountService
* @Date:2022/12/24 15:43
* 专门编写业务的
*/
public class AccountService { // 处理业务
// 每一个业务都有可能连接数据库,所以定义在方法的外面
private AccountDao accountDao = new AccountDao();
// 提供一个能够实现转账的业务方法(一个业务对应一个方法)
/**
* 完成转账的业务逻辑
* @param fromActno 转出账号
* @param toActno 转入账号
* @param money 转账金额
*/
public void transfer(String fromActno,String toActno,double money) throws MoneyNotException, AppException {
try (Connection connection = DBUtil.getConnection()){//资源自动管理,写在这里面,就不用写finally关闭资源
// 开启事务(需要使用Connection对象)
connection.setAutoCommit(false);
Account fromAct = accountDao.selectByActno(fromActno);
if (fromAct.getBalance() < money){
throw new MoneyNotException("对不起,余额不足");
}
// 程序走到这里,说明余额充足
Account toAct = accountDao.selectByActno(toActno);
// 修改金额(只是修改内存中java对象的余额)
fromAct.setBalance(fromAct.getBalance()-money);
toAct.setBalance(toAct.getBalance()+money);
// 更新数据库中的余额
int count = accountDao.update(fromAct);
// 模拟异常
String s = null;
s.toString();
count += accountDao.update(toAct);
// 判断
if (count != 2) {
throw new AppException("转账异常!");
}
// 提交事务
connection.commit();
} catch (SQLException e) {
// 这里先不会滚也可以,遇到异常就不会提交了
throw new AppException("转账异常!");
}
}
}
第二种方式:把这个Connection对象传参传过去,这样就能共享同一个Connection对象;可以,但是代码比较丑陋!
package com.bjpowernode.bank.mvc;
import com.bjpowernode.bank.exception.AppException;
import com.bjpowernode.bank.exception.MoneyNotException;
import com.bjpowernode.bank.utils.DBUtil;
import java.sql.Connection;
import java.sql.SQLException;
/**
* @Author:朗朗乾坤
* @Package:com.bjpowernode.bank.mvc
* @Project:mvc-test
* @name:AccountService
* @Date:2022/12/24 15:43
* 专门编写业务的
*/
public class AccountService { // 处理业务
// 每一个业务都有可能连接数据库,所以定义在方法的外面
private AccountDao accountDao = new AccountDao();
// 提供一个能够实现转账的业务方法(一个业务对应一个方法)
/**
* 完成转账的业务逻辑
* @param fromActno 转出账号
* @param toActno 转入账号
* @param money 转账金额
*/
public void transfer(String fromActno,String toActno,double money) throws MoneyNotException, AppException {
try (Connection connection = DBUtil.getConnection()){//资源自动管理,写在这里面,就不用写finally关闭资源
// 开启事务(需要使用Connection对象)
connection.setAutoCommit(false);
Account fromAct = accountDao.selectByActno(fromActno,connection);
if (fromAct.getBalance() < money){
throw new MoneyNotException("对不起,余额不足");
}
// 程序走到这里,说明余额充足
Account toAct = accountDao.selectByActno(toActno,connection);
// 修改金额(只是修改内存中java对象的余额)
fromAct.setBalance(fromAct.getBalance()-money);
toAct.setBalance(toAct.getBalance()+money);
// 更新数据库中的余额
int count = accountDao.update(fromAct,connection);
// 模拟异常
String s = null;
s.toString();
count += accountDao.update(toAct,connection);
// 判断
if (count != 2) {
throw new AppException("转账异常!");
}
// 提交事务
connection.commit();
} catch (SQLException e) {
// 这里先不会滚也可以,遇到异常就不会提交了
throw new AppException("转账异常!");
}
}
}
AccountDao代码也要进行修改。
package com.bjpowernode.bank.mvc;
import com.bjpowernode.bank.utils.DBUtil;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.*;
/**
* @Author:朗朗乾坤
* @Package:com.bjpowernode.bank.mvc
* @Project:mvc-test
* @name:AccountDao
* @Date:2022/12/24 13:39
* 专门处理数据的
*/
public class AccountDao { // 负责数据的增删改查
// 1、插入账户信息,1表示插入成功
public int insert(Account act,Connection conn){
PreparedStatement ps = null;
int count = 0;
try {
String sql = "insert into t_act(actno,balance) value(?,?)";
ps = conn.prepareStatement(sql);
ps.setString(1,act.getActno());
ps.setDouble(2,act.getBalance());
count = ps.executeUpdate();
} catch (SQLException e) {
e.printStackTrace();
} finally {
DBUtil.close(null,ps,null);
}
return count;
}
///2、根据主键id删除账户
public int deleteById(Long id, Connection conn){
PreparedStatement ps = null;
int count = 0;
try {
String sql = "delete from t_act where id = ?";
ps = conn.prepareStatement(sql);
ps.setLong(1,id);
count = ps.executeUpdate();
} catch (SQLException e) {
e.printStackTrace();
} finally {
DBUtil.close(null,ps,null);
}
return count;
}
// 3、更新账户
public int update(Account act, Connection conn){
/*Connection conn = null;*/
PreparedStatement ps = null;
int count = 0;
try {
/*conn = DBUtil.getConnection();*/
String sql = "update t_act set balance = ?,actno = ? where id = ?";
ps = conn.prepareStatement(sql);
ps.setDouble(1,act.getBalance());
ps.setString(2,act.getActno());
ps.setLong(3,act.getId());
count = ps.executeUpdate();
} catch (SQLException e) {
e.printStackTrace();
} finally {
/*DBUtil.close(conn,ps,null);*/
DBUtil.close(null,ps,null);
}
return count;
}
// 4、根据账号查询账户
public Account selectByActno(String actno, Connection conn){
PreparedStatement ps = null;
ResultSet rs = null;
Account act = null;
try {
String sql = "select id,balance from t_act where actno = ?";
ps = conn.prepareStatement(sql);
ps.setString(1,actno);
rs = ps.executeQuery();
if (rs.next()) {
Long id = rs.getLong("id");
Double balance = rs.getDouble("balance");
// 将结果集封装成java对象
act = new Account();
act.setId(id);
act.setActno(actno);
act.setBalance(balance);
}
} catch (SQLException e) {
e.printStackTrace();
} finally {
DBUtil.close(null,ps,rs);
}
return act;
}
// 5、获取所有的账户
public List<Account> selectAll(Connection conn){
PreparedStatement ps = null;
ResultSet rs = null;
List<Account> list = new ArrayList<>();
try {
String sql = "select id,actno,balance from t_act";
ps = conn.prepareStatement(sql);
rs = ps.executeQuery();
while (rs.next()) {
// 取数据
Long id = rs.getLong("id");
String actno = rs.getString("actno");
Double balance = rs.getDouble("balance");
// 将结果集封装成java对象
Account act = new Account();
act.setId(id);
act.setActno(actno);
act.setBalance(balance);
// 放到集合当中
list.add(act);
}
} catch (SQLException e) {
e.printStackTrace();
} finally {
DBUtil.close(null,ps,rs);
}
return list;
}
}
在AccountService的transfer方法中调用AccountDao中的任意一个方法;实际上访问的还是同一个线程对象:Thread.currentThread;所以就可以搞一个Map集合<Thread,Object>:key存线程对象,value存Connection对象!
- 手撕ThreadLocal源码
根据我们上面的理解,我们需要一个大Map集合;怎么设置这个大Map?不妨从一个简单的例子,一步步进行改进优化!
定义一个类Connection,作为参数传参
package com.bjpowernode.threadlocal;
public class Connection {
}
定义UserService类,再定义个save方法;在save方法中调用UserDao类的insert方法
package com.bjpowernode.threadlocal;
public class UserService {
private UserDao userDao = new UserDao();
public void save(Connection conn){
// 获取当前线程
Thread thread = Thread.currentThread();
System.out.println(thread);
// 在UserService的save方法当中去调用UserDao类的insert方法
userDao.insert(conn);
}
}
定义UserDao类
package com.bjpowernode.threadlocal;
public class UserDao {
public void insert(Connection conn){
// 获取当前线程
Thread thread = Thread.currentThread();
System.out.println(thread);
System.out.println("UserDao insert");
}
}
测试类Test
package com.bjpowernode.threadlocal;
public class Test {
public static void main(String[] args) {
// 获取Connection对象
Connection conn = new Connection();
// 获取当前线程
Thread thread = Thread.currentThread();
System.out.println(thread);
// 调用service
UserService userService = new UserService();
userService.save(conn);
}
}
通过测试发现,所有获得的是同一个线程对象,并且通过传参Connection,也可以正常调用
既然所有的线程对象相同,我们不妨通过一个Map集合的方式Map<线程对象,Connection对象>,做到不传参Connection,也做到所有类的Connection对象是同一个对象!
编写一个MyThreadLocal类:里面是一个Map集合,所有需要和当前线程绑定的数据要放到这个容器里
package com.bjpowernode.threadlocal;
import java.util.HashMap;
import java.util.Map;
public class MyThreadLocal<T> {
// 所有需要和当前线程绑定的数据要放到这个容器里
private Map<Thread,T> map = new HashMap<>();
// 存
public void set(T t){
map.put(Thread.currentThread(),t);
}
// 取
public T get(){
return map.get(Thread.currentThread());
}
// 删除
public void remove(){
map.remove(Thread.currentThread());
}
}
DBUtil工具类:用来获取Connection
package com.bjpowernode.utils;
import com.bjpowernode.threadlocal.Connection;
import com.bjpowernode.threadlocal.MyThreadLocal;
public class DBUtil { // 封装一个工具类
// 静态变量:类加载时执行,并且只执行一次
// 全局的大Map集合
private static MyThreadLocal<Connection> local = new MyThreadLocal<>();
// 每一次调用这个方法获取Connection对象
public static Connection getConnection(){
Connection connection = local.get();
if (connection == null) {
// 空的就new一次
connection = new Connection();
// 将new的Connection对象绑定到大Map集合
local.set(connection);
}
return connection;
}
}
原来的Test类中new Connection对象,就不许需要了;方法中为了保持所有的Connection一样传参Connection也不需要了,直接使用工具类就可以获取全部相同的Connection对象!
Test类
package com.bjpowernode.threadlocal;
public class Test {
public static void main(String[] args) {
// 获取当前线程
Thread thread = Thread.currentThread();
System.out.println(thread);
// 调用service
UserService userService = new UserService();
userService.save();
}
}
UserService类
package com.bjpowernode.threadlocal;
import com.bjpowernode.utils.DBUtil;
public class UserService {
private UserDao userDao = new UserDao();
public void save(){
// 获取当前线程
Thread thread = Thread.currentThread();
System.out.println(thread);
// 直接获取Connextion对象
Connection connection = DBUtil.getConnection();
System.out.println(connection);
// 在UserService的save方法当中去调用UserDao类的insert方法
userDao.insert();
}
}
UserDao类
package com.bjpowernode.threadlocal;
import com.bjpowernode.utils.DBUtil;
public class UserDao {
public void insert(){
// 获取当前线程
Thread thread = Thread.currentThread();
System.out.println(thread);
// 获取Connection对象
Connection connection = DBUtil.getConnection();
System.out.println(connection);
System.out.println("UserDao insert");
}
}
通过测试发现,这两个直接获取到的Connection对象,确实是同一个Connection对象
第三种方法:项目中直接引入ThreadLocal(java.lang.ThreadLocal已经封装好了,不需要手写!在连接数据的DBUtil工具类中进行代码的修改
补充:Tomcat服务器内置了一个线程池,线程池中有很多线程对象:这些线程对象t1,t2,t3都是提前创建好的,也就是说t1,t2,t3存在重复使用的现象!
package com.bjpowernode.bank.utils;
import java.sql.*;
import java.util.ResourceBundle;
public class DBUtil {
private static ResourceBundle bundle = ResourceBundle.getBundle("resources.jdbc");
private static String driver = bundle.getString("driver");
private static String url = bundle.getString("url");
private static String user = bundle.getString("user");
private static String password = bundle.getString("password");
// 先提供给一个私有的构造方法:防止实例化对象
private DBUtil(){}
// 类加载时,注册驱动
static {
try {
Class.forName(driver);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
// 创建ThreadLocal(本质上是一个大Map)
private static ThreadLocal<Connection> local = new ThreadLocal<>();
// 获取连接
public static Connection getConnection() throws SQLException {
// 获取connection对象
Connection connection = local.get();
if (connection == null) {
// 如果为空,就创建出来
connection = DriverManager.getConnection(url, user, password);
// 放入集合当中
local.set(connection);
}
return connection;
}
// 关闭资源
public static void close(Connection conn, Statement stmt, ResultSet rs){
if (rs != null) {
try {
rs.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (stmt != null) {
try {
stmt.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (conn != null) {
try {
conn.close();
// 思考:conn关闭之后,线程要从大Map移除?
// 根本原因:Tomcat服务器时支持多线程的,也就是说一个人用过了t1线程,t1线程还有可能被其他用户使用
local.remove();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
修改以后,其他类的方法传参时就不需要传Connection对象了,因为它们获得的都是同一个Connection对象,可以正常关闭!
AccountDao类:关于数据的增删改查操作,不需要传参Connection,还是使用工具类直接获取
package com.bjpowernode.bank.mvc;
import com.bjpowernode.bank.utils.DBUtil;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.*;
/**
* @Author:朗朗乾坤
* @Package:com.bjpowernode.bank.mvc
* @Project:mvc-test
* @name:AccountDao
* @Date:2022/12/24 13:39
* 专门处理数据的
*/
public class AccountDao { // 负责数据的增删改查
// 1、插入账户信息,1表示插入成功
public int insert(Account act){
PreparedStatement ps = null;
int count = 0;
try {
Connection conn = DBUtil.getConnection();
String sql = "insert into t_act(actno,balance) value(?,?)";
ps = conn.prepareStatement(sql);
ps.setString(1,act.getActno());
ps.setDouble(2,act.getBalance());
count = ps.executeUpdate();
} catch (SQLException e) {
e.printStackTrace();
} finally {
DBUtil.close(null,ps,null);
}
return count;
}
///2、根据主键id删除账户
public int deleteById(Long id){
PreparedStatement ps = null;
int count = 0;
try {
Connection conn = DBUtil.getConnection();
String sql = "delete from t_act where id = ?";
ps = conn.prepareStatement(sql);
ps.setLong(1,id);
count = ps.executeUpdate();
} catch (SQLException e) {
e.printStackTrace();
} finally {
DBUtil.close(null,ps,null);
}
return count;
}
// 3、更新账户
public int update(Account act){
PreparedStatement ps = null;
int count = 0;
try {
Connection conn = DBUtil.getConnection();
String sql = "update t_act set balance = ?,actno = ? where id = ?";
ps = conn.prepareStatement(sql);
ps.setDouble(1,act.getBalance());
ps.setString(2,act.getActno());
ps.setLong(3,act.getId());
count = ps.executeUpdate();
} catch (SQLException e) {
e.printStackTrace();
} finally {
DBUtil.close(null,ps,null);
}
return count;
}
// 4、根据账号查询账户
public Account selectByActno(String actno){
PreparedStatement ps = null;
ResultSet rs = null;
Account act = null;
try {
Connection conn = DBUtil.getConnection();
String sql = "select id,balance from t_act where actno = ?";
ps = conn.prepareStatement(sql);
ps.setString(1,actno);
rs = ps.executeQuery();
if (rs.next()) {
Long id = rs.getLong("id");
Double balance = rs.getDouble("balance");
// 将结果集封装成java对象
act = new Account();
act.setId(id);
act.setActno(actno);
act.setBalance(balance);
}
} catch (SQLException e) {
e.printStackTrace();
} finally {
DBUtil.close(null,ps,rs);
}
return act;
}
// 5、获取所有的账户
public List<Account> selectAll(){
PreparedStatement ps = null;
ResultSet rs = null;
List<Account> list = new ArrayList<>();
try {
Connection conn = DBUtil.getConnection();
String sql = "select id,actno,balance from t_act";
ps = conn.prepareStatement(sql);
rs = ps.executeQuery();
while (rs.next()) {
// 取数据
Long id = rs.getLong("id");
String actno = rs.getString("actno");
Double balance = rs.getDouble("balance");
// 将结果集封装成java对象
Account act = new Account();
act.setId(id);
act.setActno(actno);
act.setBalance(balance);
// 放到集合当中
list.add(act);
}
} catch (SQLException e) {
e.printStackTrace();
} finally {
DBUtil.close(null,ps,rs);
}
return list;
}
}
AccountService类:逻辑业务代码调用增删改查的方法也不需要传参数
package com.bjpowernode.bank.mvc;
import com.bjpowernode.bank.exception.AppException;
import com.bjpowernode.bank.exception.MoneyNotException;
import com.bjpowernode.bank.utils.DBUtil;
import java.sql.Connection;
import java.sql.SQLException;
/**
* @Author:朗朗乾坤
* @Package:com.bjpowernode.bank.mvc
* @Project:mvc-test
* @name:AccountService
* @Date:2022/12/24 15:43
* 专门编写业务的
*/
public class AccountService { // 处理业务
// 每一个业务都有可能连接数据库,所以定义在方法的外面
private AccountDao accountDao = new AccountDao();
// 提供一个能够实现转账的业务方法(一个业务对应一个方法)
public void transfer(String fromActno,String toActno,double money) throws MoneyNotException, AppException {
try (Connection connection = DBUtil.getConnection()){//资源自动管理,写在这里面,就不用写finally关闭资源
// 开启事务(需要使用Connection对象)
connection.setAutoCommit(false);
Account fromAct = accountDao.selectByActno(fromActno);
if (fromAct.getBalance() < money){
throw new MoneyNotException("对不起,余额不足");
}
// 程序走到这里,说明余额充足
Account toAct = accountDao.selectByActno(toActno);
// 修改金额(只是修改内存中java对象的余额)
fromAct.setBalance(fromAct.getBalance()-money);
toAct.setBalance(toAct.getBalance()+money);
// 更新数据库中的余额
int count = accountDao.update(fromAct);
// 模拟异常
/* String s = null;
s.toString();*/
count += accountDao.update(toAct);
// 判断
if (count != 2) {
throw new AppException("转账异常!");
}
// 提交事务
connection.commit();
} catch (SQLException e) {
// 这里先不会滚也可以,遇到异常就不会提交了
throw new AppException("转账异常!");
}
}
}
这样就能做到事务的控制!
不能功能放到不同的包下
按照三层架构分析:web下的UserServlet是表示层、service包下的UserService是业务层、dao包下的UserDao是持久化层
按照MVC架构模式分析:pojo下的Account类、dao包下的UserDao、service包下的UserService属于M、所有的.jsp属于V、web下的UserServlet属于C
并把AccountService和AccountDao类写成接口的形式(面向接口编程),然后写对应的实现类去实现这个接口即可!