MVC架构模式 | 使用银行转账的功能实现引出MVC架构模式

不念不忘少年蓝@ 2024-03-30 11:29 185阅读 0赞

一、银行转账的功能实现

数据库表的准备

创建数据库表,主要包括三个字段:自增的id、账户名、账户余额

8967958e1a5c41a9adbf84a4edf09b8e.png

  1. 不使用MVC架构模式完成账户转账

首先写一个页面,写入转账的账户和金额;并发送post请求

  1. <%@ page contentType="text/html;charset=UTF-8" language="java" %>
  2. <html>
  3. <head>
  4. <%--<base href="http://localhost:8080/bank/">--%>
  5. <base href="${pageContext.request.scheme}://${pageContext.request.serverName}:${pageContext.request.serverPort}${pageContext.request.contextPath}/">
  6. <title>转账</title>
  7. </head>
  8. <body>
  9. <form action="transfer" method="post">
  10. 转出账户:<input type="text" name="fromActno"><br>
  11. 装入账户:<input type="text" name="toActno"><br>
  12. 转账金额:<input type="text" name="money"><br>
  13. <input type="submit" value="转账">
  14. </form>
  15. </body>
  16. </html>

根据发送的数据请求,编写Servlet,连接数据库进行转账操作

  1. package com.bjpowernode.zl.javaweb;
  2. import com.bjpowernode.zl.exception.MoneyNotException;
  3. import com.bjpowernode.zl.exception.ResultException;
  4. import javax.servlet.ServletException;
  5. import javax.servlet.annotation.WebServlet;
  6. import javax.servlet.http.HttpServlet;
  7. import javax.servlet.http.HttpServletRequest;
  8. import javax.servlet.http.HttpServletResponse;
  9. import java.io.IOException;
  10. import java.io.PrintWriter;
  11. import java.sql.*;
  12. /**
  13. * @Author:朗朗乾坤
  14. * @Package:com.bjpowernode.zl.javaweb
  15. * @Project:mvc-test
  16. * @name:TransferServlet
  17. * @Date:2022/12/23 18:53
  18. */
  19. @WebServlet("/transfer")
  20. public class TransferServlet extends HttpServlet {
  21. @Override
  22. protected void doPost(HttpServletRequest request, HttpServletResponse response)
  23. throws ServletException, IOException {
  24. // 获取前端提交的数据
  25. response.setContentType("text/html;charset=UTF-8");
  26. PrintWriter out = response.getWriter();
  27. String fromActno = request.getParameter("fromActno");
  28. String toActno = request.getParameter("toActno");
  29. double money = Double.parseDouble(request.getParameter("money"));
  30. // 连接数据库,进行转账操作
  31. // 1. 转账之前先要进行查询操作,有没有钱可以转
  32. Connection conn = null;
  33. PreparedStatement ps = null;
  34. PreparedStatement ps2 = null;
  35. PreparedStatement ps3 = null;
  36. ResultSet rs = null;
  37. try {
  38. // 注册驱动
  39. Class.forName("com.mysql.jdbc.Driver");
  40. // 获取连接
  41. String url = "jdbc:mysql://localhost:3306/mvc";
  42. String username = "root";
  43. String password = "123";
  44. conn = DriverManager.getConnection(url, username, password);
  45. // 获取预编译的数据库操作对象
  46. String sql = "select balance from t_act where actno = ?";
  47. ps = conn.prepareStatement(sql);
  48. ps.setString(1,fromActno);
  49. // 执行sql
  50. rs = ps.executeQuery();
  51. // 处理查询结果集
  52. if (rs.next()) {
  53. double balance = rs.getDouble("balance");
  54. if(balance < money){
  55. // 余额不足,抛出异常
  56. throw new MoneyNotException("对不起,余额不足!");
  57. }
  58. // 2. 余额充足,进行转账
  59. // 开启事务(手动提交)
  60. conn.setAutoCommit(false);
  61. String sql2 = "update t_act set balance = balance - ? where actno = ?";
  62. ps2 = conn.prepareStatement(sql2);
  63. ps2.setDouble(1,money);
  64. ps2.setString(2,fromActno);
  65. int count = ps2.executeUpdate();
  66. String sql3 = "update t_act set balance = balance + ? where actno = ?";
  67. ps3 = conn.prepareStatement(sql3);
  68. ps3.setDouble(1,money);
  69. ps3.setString(2,toActno);
  70. // 累计
  71. count += ps3.executeUpdate();
  72. if (count != 2){
  73. throw new ResultException("转账过程出现问题,请联系管理员");
  74. }
  75. // 转账成功
  76. // 手动提交事务
  77. conn.commit();
  78. out.print("转账成功!");
  79. }
  80. } catch (Exception e) {
  81. // 只要出现异常,进行回滚
  82. try {
  83. if (conn != null){
  84. conn.rollback();
  85. }
  86. } catch (SQLException e1) {
  87. e1.printStackTrace();
  88. }
  89. // e.printStackTrace();
  90. // 打印异常的信息
  91. out.print(e.getMessage());
  92. } finally {
  93. // 释放资源
  94. if (rs != null) {
  95. try {
  96. rs.close();
  97. } catch (SQLException e) {
  98. e.printStackTrace();
  99. }
  100. }
  101. if (ps != null) {
  102. try {
  103. ps.close();
  104. } catch (SQLException e) {
  105. e.printStackTrace();
  106. }
  107. }
  108. if (ps2 != null) {
  109. try {
  110. ps2.close();
  111. } catch (SQLException e) {
  112. e.printStackTrace();
  113. }
  114. }
  115. if (ps3 != null) {
  116. try {
  117. ps3.close();
  118. } catch (SQLException e) {
  119. e.printStackTrace();
  120. }
  121. }
  122. if (conn != null) {
  123. try {
  124. conn.close();
  125. } catch (SQLException e) {
  126. e.printStackTrace();
  127. }
  128. }
  129. }
  130. }
  131. }

两个异常处理

  1. package com.bjpowernode.zl.exception;
  2. /**
  3. * @Author:朗朗乾坤
  4. * @Package:com.bjpowernode.zl.exception
  5. * @Project:mvc-test
  6. * @name:MoneyNotException
  7. * @Date:2022/12/23 19:56
  8. */
  9. public class MoneyNotException extends Exception{
  10. public MoneyNotException() {
  11. }
  12. public MoneyNotException(String msg) {
  13. super(msg);
  14. }
  15. }
  16. package com.bjpowernode.zl.exception;
  17. /**
  18. * @Author:朗朗乾坤
  19. * @Package:com.bjpowernode.zl.exception
  20. * @Project:mvc-test
  21. * @name:ResultException
  22. * @Date:2022/12/23 20:14
  23. */
  24. public class ResultException extends Exception {
  25. public ResultException() {
  26. }
  27. public ResultException(String message) {
  28. super(message);
  29. }
  30. }

缺点:一个Servlet实现了所有的功能
①负责数据的接收
②负责核心的业务处理
③负责数据库中数据的CRUD
④负责了页面的展示,打印输出信息到浏览器上
原因:没有进行“职能分工”
①没有独立组件的概念,所以没有办法进行代码复用,代码和代码之间的耦合度太高,扩展力太差!
②操作数据库的代码和业务逻辑混杂在一起,很容易出错!

  1. MVC架构模式的理论基础

(1)系统为什么分层?
希望各司其职,专人干专事。职能分工要明确,这样可以让代码耦合度降低,扩展力增强,组件的可复用性增强。
(2)软件的架构中,一个著名的架构模式:MVC架构模式
①M(Model:数据/业务)、V(View:视图/展示)、C(Controller:控制器)
②C(是核心,是控制,是司令官)
③M(处理业务/处理数据的一个秘书)
④V(负责页面展示的一个秘书)
⑤MVC(实际上就是一个司令官,调度两个秘书)

83ad30f7f83849e4ac436036daa30cd8.png

  1. JDBC工具类的封装

为了下面我们方便写代码,还是先封装一个JDBC工具类,进行数据库连接的操作

  1. driver=com.mysql.jdbc.Driver
  2. url=jdbc:mysql://localhost:3306/mvc
  3. user=root
  4. password=123
  5. package com.bjpowernode.bank.utils;
  6. import java.sql.*;
  7. import java.util.ResourceBundle;
  8. /**
  9. * @Author:朗朗乾坤
  10. * @Package:com.bjpowernode.bank.utils
  11. * @Project:mvc-test
  12. * @name:DBUtil
  13. * @Date:2022/12/24 11:10
  14. */
  15. public class DBUtil {
  16. private static ResourceBundle bundle = ResourceBundle.getBundle("resources.jdbc");
  17. private static String driver = bundle.getString("driver");
  18. private static String url = bundle.getString("url");
  19. private static String user = bundle.getString("user");
  20. private static String password = bundle.getString("password");
  21. // 先提供给一个私有的构造方法:防止实例化对象
  22. private DBUtil(){}
  23. // 类加载时,注册驱动
  24. static {
  25. try {
  26. Class.forName(driver);
  27. } catch (ClassNotFoundException e) {
  28. e.printStackTrace();
  29. }
  30. }
  31. // 获取连接
  32. public static Connection getConnection() throws SQLException {
  33. Connection connection = DriverManager.getConnection(url, user, password);
  34. return connection;
  35. }
  36. // 关闭资源
  37. public static void close(Connection conn, Statement stmt, ResultSet rs){
  38. if (rs != null) {
  39. try {
  40. rs.close();
  41. } catch (SQLException e) {
  42. e.printStackTrace();
  43. }
  44. }
  45. if (stmt != null) {
  46. try {
  47. stmt.close();
  48. } catch (SQLException e) {
  49. e.printStackTrace();
  50. }
  51. }
  52. if (conn != null) {
  53. try {
  54. conn.close();
  55. } catch (SQLException e) {
  56. e.printStackTrace();
  57. }
  58. }
  59. }
  60. }
  1. JavaEE设计模式之DAO模式以及DAO的编写

创建一个类AccountDao负责Account数据的增删改查
①什么是DAO?Data Access Object(数据访问对象)
②DAO实际上是一种设计模式,属于JavaEE的设计模式(不是23种设计模式)
③DAO只负责数据表的CRUD,没有任何的业务逻辑在里面。
④没有任何逻辑业务,只负责表中数据的增删改查的对象,就叫做:DAO对象
⑤一般情况下:一张表就对应一个DAO对象

Account类:把查询的结果封装成一个Java对象,封装账户信息的;

注意:一般属性不建议设置为基本数据类型,建议使用包装类,防止null带来的问题

注:这种普通的对象也就做pojo对象(或者javabean对象、或者领域模型对象:domain对象)

  1. package com.bjpowernode.bank.mvc;
  2. /**
  3. * @Author:朗朗乾坤
  4. * @Package:com.bjpowernode.bank.mvc
  5. * @Project:mvc-test
  6. * @name:Account
  7. * @Date:2022/12/24 13:50
  8. */
  9. public class Account {
  10. // 一般属性不建议设置为基本数据类型,建议使用包装类,防止null带来的问题
  11. // 使用Long和Double就算取出来的数据是null也能直接赋值过去,完全没有问题
  12. private Long id;
  13. private String actno;
  14. private Double balance;
  15. public Account() {
  16. }
  17. public Account(Long id, String actno, Double balance) {
  18. this.id = id;
  19. this.actno = actno;
  20. this.balance = balance;
  21. }
  22. public Long getId() {
  23. return id;
  24. }
  25. public void setId(Long id) {
  26. this.id = id;
  27. }
  28. public String getActno() {
  29. return actno;
  30. }
  31. public void setActno(String actno) {
  32. this.actno = actno;
  33. }
  34. public Double getBalance() {
  35. return balance;
  36. }
  37. public void setBalance(Double balance) {
  38. this.balance = balance;
  39. }
  40. @Override
  41. public String toString() {
  42. return "Account{" +
  43. "id=" + id +
  44. ", actno='" + actno + '\'' +
  45. ", balance=" + balance +
  46. '}';
  47. }
  48. }

AccountDao类:只负责数据库的访问,处理数据(增删改查)

DAO中的方法名很固定,一般都是:insert、deleteByXxx、update、selectByXxx、selectAll

  1. package com.bjpowernode.bank.mvc;
  2. import com.bjpowernode.bank.utils.DBUtil;
  3. import java.sql.Connection;
  4. import java.sql.PreparedStatement;
  5. import java.sql.ResultSet;
  6. import java.sql.SQLException;
  7. import java.util.*;
  8. /**
  9. * @Author:朗朗乾坤
  10. * @Package:com.bjpowernode.bank.mvc
  11. * @Project:mvc-test
  12. * @name:AccountDao
  13. * @Date:2022/12/24 13:39
  14. */
  15. public class AccountDao { // 负责数据的增删改查
  16. // 1、插入账户信息,1表示插入成功
  17. public int insert(Account act){
  18. Connection conn = null;
  19. PreparedStatement ps = null;
  20. int count = 0;
  21. try {
  22. conn = DBUtil.getConnection();
  23. String sql = "insert into t_act(actno,balance) value(?,?)";
  24. ps = conn.prepareStatement(sql);
  25. ps.setString(1,act.getActno());
  26. ps.setDouble(2,act.getBalance());
  27. count = ps.executeUpdate();
  28. } catch (SQLException e) {
  29. e.printStackTrace();
  30. } finally {
  31. DBUtil.close(conn,ps,null);
  32. }
  33. return count;
  34. }
  35. ///2、根据主键id删除账户
  36. public int deleteById(Long id){
  37. Connection conn = null;
  38. PreparedStatement ps = null;
  39. int count = 0;
  40. try {
  41. conn = DBUtil.getConnection();
  42. String sql = "delete from t_act where id = ?";
  43. ps = conn.prepareStatement(sql);
  44. ps.setLong(1,id);
  45. count = ps.executeUpdate();
  46. } catch (SQLException e) {
  47. e.printStackTrace();
  48. } finally {
  49. DBUtil.close(conn,ps,null);
  50. }
  51. return count;
  52. }
  53. // 3、更新账户
  54. public int update(Account act){
  55. Connection conn = null;
  56. PreparedStatement ps = null;
  57. int count = 0;
  58. try {
  59. conn = DBUtil.getConnection();
  60. String sql = "update t_act set balance = ?,actno = ? where id = ?";
  61. ps = conn.prepareStatement(sql);
  62. ps.setDouble(1,act.getBalance());
  63. ps.setString(2,act.getActno());
  64. ps.setLong(3,act.getId());
  65. count = ps.executeUpdate();
  66. } catch (SQLException e) {
  67. e.printStackTrace();
  68. } finally {
  69. DBUtil.close(conn,ps,null);
  70. }
  71. return count;
  72. }
  73. // 4、根据账号查询账户
  74. public Account selectByActno(String actno){
  75. Connection conn = null;
  76. PreparedStatement ps = null;
  77. ResultSet rs = null;
  78. Account act = null;
  79. try {
  80. conn = DBUtil.getConnection();
  81. String sql = "select id,balance from t_act where actno = ?";
  82. ps = conn.prepareStatement(sql);
  83. ps.setString(1,actno);
  84. rs = ps.executeQuery();
  85. if (rs.next()) {
  86. Long id = rs.getLong("id");
  87. Double balance = rs.getDouble("balance");
  88. // 将结果集封装成java对象
  89. act = new Account();
  90. act.setId(id);
  91. act.setActno(actno);
  92. act.setBalance(balance);
  93. }
  94. } catch (SQLException e) {
  95. e.printStackTrace();
  96. } finally {
  97. DBUtil.close(conn,ps,rs);
  98. }
  99. return act;
  100. }
  101. // 5、获取所有的账户
  102. public List<Account> selectAll(){
  103. Connection conn = null;
  104. PreparedStatement ps = null;
  105. ResultSet rs = null;
  106. List<Account> list = new ArrayList<>();
  107. try {
  108. conn = DBUtil.getConnection();
  109. String sql = "select id,actno,balance from t_act";
  110. ps = conn.prepareStatement(sql);
  111. rs = ps.executeQuery();
  112. while (rs.next()) {
  113. // 取数据
  114. Long id = rs.getLong("id");
  115. String actno = rs.getString("actno");
  116. Double balance = rs.getDouble("balance");
  117. // 将结果集封装成java对象
  118. Account act = new Account();
  119. act.setId(id);
  120. act.setActno(actno);
  121. act.setBalance(balance);
  122. // 放到集合当中
  123. list.add(act);
  124. }
  125. } catch (SQLException e) {
  126. e.printStackTrace();
  127. } finally {
  128. DBUtil.close(conn,ps,rs);
  129. }
  130. return list;
  131. }
  132. }
  1. 业务层抽取以及业务方法的实现

前面我们已经写了两个类:Account类主要负责封装数据的;AccountDao类主要处理数据的;这里就要再抽取出来一个类AccountService类用来处理逻辑业务的!
①service翻译为:业务,在AccountService类中编写纯业务代码
②业务类一般起名:XxxService、XxxBiz…

  1. package com.bjpowernode.bank.mvc;
  2. import com.bjpowernode.bank.exception.AppException;
  3. import com.bjpowernode.bank.exception.MoneyNotException;
  4. /**
  5. * @Author:朗朗乾坤
  6. * @Package:com.bjpowernode.bank.mvc
  7. * @Project:mvc-test
  8. * @name:AccountService
  9. * @Date:2022/12/24 15:43
  10. * 专门编写业务的
  11. */
  12. public class AccountService { // 处理业务
  13. // 每一个业务都有可能连接数据库,所以定义在方法的外面
  14. private AccountDao accountDao = new AccountDao();
  15. // 提供一个能够实现转账的业务方法(一个业务对应一个方法)
  16. /**
  17. * 完成转账的业务逻辑
  18. * @param fromActno 转出账号
  19. * @param toActno 转入账号
  20. * @param money 转账金额
  21. */
  22. public void transfer(String fromActno,String toActno,double money) throws MoneyNotException, AppException {
  23. // 先查询余额是否充足,返回的是一个Account对象
  24. Account fromAct = accountDao.selectByActno(fromActno);
  25. if (fromAct.getBalance() < money){
  26. throw new MoneyNotException("对不起,余额不足");
  27. }
  28. // 程序走到这里,说明余额充足
  29. Account toAct = accountDao.selectByActno(toActno);
  30. // 修改金额(只是修改内存中java对象的余额)
  31. fromAct.setBalance(fromAct.getBalance()-money);
  32. toAct.setBalance(toAct.getBalance()+money);
  33. // 更新数据库中的余额
  34. int count = accountDao.update(fromAct);
  35. count += accountDao.update(toAct);
  36. // 判断
  37. if (count != 2) {
  38. throw new AppException("转账异常!");
  39. }
  40. }
  41. }

AccountServlet类:相当于司令官,进行整个业务的调度

  1. package com.bjpowernode.bank.mvc;
  2. import com.bjpowernode.bank.exception.AppException;
  3. import com.bjpowernode.bank.exception.MoneyNotException;
  4. import javax.servlet.ServletException;
  5. import javax.servlet.annotation.WebServlet;
  6. import javax.servlet.http.HttpServlet;
  7. import javax.servlet.http.HttpServletRequest;
  8. import javax.servlet.http.HttpServletResponse;
  9. import java.io.IOException;
  10. /**
  11. * @Author:朗朗乾坤
  12. * @Package:com.bjpowernode.bank.mvc
  13. * @Project:mvc-test
  14. * @name:AccountServlet
  15. * @Date:2022/12/24 13:36
  16. */
  17. @WebServlet("/transfer")
  18. public class AccountServlet extends HttpServlet { // AccountServlet作为Controller
  19. @Override
  20. protected void doPost(HttpServletRequest request, HttpServletResponse response)
  21. throws ServletException, IOException {
  22. // 接收数据
  23. String fromActno = request.getParameter("fromActno");
  24. String toActno = request.getParameter("toActno");
  25. double money = Double.parseDouble(request.getParameter("money"));
  26. // 调用业务方法处理业务(调度Model处理业务)
  27. AccountService accountService = new AccountService();
  28. try {
  29. accountService.transfer(fromActno,toActno,money); // 进行转账
  30. // 转账成功
  31. // 展示处理结果(调度View做页面展示)
  32. response.sendRedirect(request.getContextPath()+"/success.jsp");
  33. } catch (MoneyNotException e) {
  34. // 余额不足
  35. // 展示处理结果(调度View做页面展示)
  36. response.sendRedirect(request.getContextPath()+"/money-not-enough.jsp");
  37. } catch (AppException e) {
  38. // 失败
  39. // 展示处理结果(调度View做页面展示)
  40. response.sendRedirect(request.getContextPath()+"/error.jsp");
  41. }
  42. }
  43. }
  1. MVC架构模式与三层架构的关系

通过前面的一步步分层,实际就达到了以下三层架构的模型
(横向)

384c10c6c91647f9b58a5278cdccdedb.png

(纵向)

f2265145fc7343a19d2456bb8668c311.png

所以,对于原来的图,就可以进一步细化,展示出:MVC架构模式与三层架构的关系

f5559db0fb334c36a74c034cd5187bea.png

补充:SSM框架的理解
①Spring:项目大管家,负责整个项目所有对象的创建以及维护对象和对象之间的关系。
②SpringMVC:将MVC结构模式体现的非常完美;在这个框架的基础上开发一定是用了MVC架构模式;实际上SringMVC架构已经把MVC架构的环境已经搭建出来了。
③MyBatis:属于持久化层的框架。

  1. 解决事务问题

上述我们已经可以实现所有的功能,但是还没有添加事务,一旦出现异常现象会出现问题。
注: 事务一定是在业务逻辑层(service层)进行控制。

第一种方式:不可以,不是同一个Connection对象,无法控制事务;但是是在同一个线程里面的

  1. package com.bjpowernode.bank.mvc;
  2. import com.bjpowernode.bank.exception.AppException;
  3. import com.bjpowernode.bank.exception.MoneyNotException;
  4. import com.bjpowernode.bank.utils.DBUtil;
  5. import java.sql.Connection;
  6. import java.sql.SQLException;
  7. /**
  8. * @Author:朗朗乾坤
  9. * @Package:com.bjpowernode.bank.mvc
  10. * @Project:mvc-test
  11. * @name:AccountService
  12. * @Date:2022/12/24 15:43
  13. * 专门编写业务的
  14. */
  15. public class AccountService { // 处理业务
  16. // 每一个业务都有可能连接数据库,所以定义在方法的外面
  17. private AccountDao accountDao = new AccountDao();
  18. // 提供一个能够实现转账的业务方法(一个业务对应一个方法)
  19. /**
  20. * 完成转账的业务逻辑
  21. * @param fromActno 转出账号
  22. * @param toActno 转入账号
  23. * @param money 转账金额
  24. */
  25. public void transfer(String fromActno,String toActno,double money) throws MoneyNotException, AppException {
  26. try (Connection connection = DBUtil.getConnection()){//资源自动管理,写在这里面,就不用写finally关闭资源
  27. // 开启事务(需要使用Connection对象)
  28. connection.setAutoCommit(false);
  29. Account fromAct = accountDao.selectByActno(fromActno);
  30. if (fromAct.getBalance() < money){
  31. throw new MoneyNotException("对不起,余额不足");
  32. }
  33. // 程序走到这里,说明余额充足
  34. Account toAct = accountDao.selectByActno(toActno);
  35. // 修改金额(只是修改内存中java对象的余额)
  36. fromAct.setBalance(fromAct.getBalance()-money);
  37. toAct.setBalance(toAct.getBalance()+money);
  38. // 更新数据库中的余额
  39. int count = accountDao.update(fromAct);
  40. // 模拟异常
  41. String s = null;
  42. s.toString();
  43. count += accountDao.update(toAct);
  44. // 判断
  45. if (count != 2) {
  46. throw new AppException("转账异常!");
  47. }
  48. // 提交事务
  49. connection.commit();
  50. } catch (SQLException e) {
  51. // 这里先不会滚也可以,遇到异常就不会提交了
  52. throw new AppException("转账异常!");
  53. }
  54. }
  55. }

第二种方式:把这个Connection对象传参传过去,这样就能共享同一个Connection对象;可以,但是代码比较丑陋!

  1. package com.bjpowernode.bank.mvc;
  2. import com.bjpowernode.bank.exception.AppException;
  3. import com.bjpowernode.bank.exception.MoneyNotException;
  4. import com.bjpowernode.bank.utils.DBUtil;
  5. import java.sql.Connection;
  6. import java.sql.SQLException;
  7. /**
  8. * @Author:朗朗乾坤
  9. * @Package:com.bjpowernode.bank.mvc
  10. * @Project:mvc-test
  11. * @name:AccountService
  12. * @Date:2022/12/24 15:43
  13. * 专门编写业务的
  14. */
  15. public class AccountService { // 处理业务
  16. // 每一个业务都有可能连接数据库,所以定义在方法的外面
  17. private AccountDao accountDao = new AccountDao();
  18. // 提供一个能够实现转账的业务方法(一个业务对应一个方法)
  19. /**
  20. * 完成转账的业务逻辑
  21. * @param fromActno 转出账号
  22. * @param toActno 转入账号
  23. * @param money 转账金额
  24. */
  25. public void transfer(String fromActno,String toActno,double money) throws MoneyNotException, AppException {
  26. try (Connection connection = DBUtil.getConnection()){//资源自动管理,写在这里面,就不用写finally关闭资源
  27. // 开启事务(需要使用Connection对象)
  28. connection.setAutoCommit(false);
  29. Account fromAct = accountDao.selectByActno(fromActno,connection);
  30. if (fromAct.getBalance() < money){
  31. throw new MoneyNotException("对不起,余额不足");
  32. }
  33. // 程序走到这里,说明余额充足
  34. Account toAct = accountDao.selectByActno(toActno,connection);
  35. // 修改金额(只是修改内存中java对象的余额)
  36. fromAct.setBalance(fromAct.getBalance()-money);
  37. toAct.setBalance(toAct.getBalance()+money);
  38. // 更新数据库中的余额
  39. int count = accountDao.update(fromAct,connection);
  40. // 模拟异常
  41. String s = null;
  42. s.toString();
  43. count += accountDao.update(toAct,connection);
  44. // 判断
  45. if (count != 2) {
  46. throw new AppException("转账异常!");
  47. }
  48. // 提交事务
  49. connection.commit();
  50. } catch (SQLException e) {
  51. // 这里先不会滚也可以,遇到异常就不会提交了
  52. throw new AppException("转账异常!");
  53. }
  54. }
  55. }

AccountDao代码也要进行修改。

  1. package com.bjpowernode.bank.mvc;
  2. import com.bjpowernode.bank.utils.DBUtil;
  3. import java.sql.Connection;
  4. import java.sql.PreparedStatement;
  5. import java.sql.ResultSet;
  6. import java.sql.SQLException;
  7. import java.util.*;
  8. /**
  9. * @Author:朗朗乾坤
  10. * @Package:com.bjpowernode.bank.mvc
  11. * @Project:mvc-test
  12. * @name:AccountDao
  13. * @Date:2022/12/24 13:39
  14. * 专门处理数据的
  15. */
  16. public class AccountDao { // 负责数据的增删改查
  17. // 1、插入账户信息,1表示插入成功
  18. public int insert(Account act,Connection conn){
  19. PreparedStatement ps = null;
  20. int count = 0;
  21. try {
  22. String sql = "insert into t_act(actno,balance) value(?,?)";
  23. ps = conn.prepareStatement(sql);
  24. ps.setString(1,act.getActno());
  25. ps.setDouble(2,act.getBalance());
  26. count = ps.executeUpdate();
  27. } catch (SQLException e) {
  28. e.printStackTrace();
  29. } finally {
  30. DBUtil.close(null,ps,null);
  31. }
  32. return count;
  33. }
  34. ///2、根据主键id删除账户
  35. public int deleteById(Long id, Connection conn){
  36. PreparedStatement ps = null;
  37. int count = 0;
  38. try {
  39. String sql = "delete from t_act where id = ?";
  40. ps = conn.prepareStatement(sql);
  41. ps.setLong(1,id);
  42. count = ps.executeUpdate();
  43. } catch (SQLException e) {
  44. e.printStackTrace();
  45. } finally {
  46. DBUtil.close(null,ps,null);
  47. }
  48. return count;
  49. }
  50. // 3、更新账户
  51. public int update(Account act, Connection conn){
  52. /*Connection conn = null;*/
  53. PreparedStatement ps = null;
  54. int count = 0;
  55. try {
  56. /*conn = DBUtil.getConnection();*/
  57. String sql = "update t_act set balance = ?,actno = ? where id = ?";
  58. ps = conn.prepareStatement(sql);
  59. ps.setDouble(1,act.getBalance());
  60. ps.setString(2,act.getActno());
  61. ps.setLong(3,act.getId());
  62. count = ps.executeUpdate();
  63. } catch (SQLException e) {
  64. e.printStackTrace();
  65. } finally {
  66. /*DBUtil.close(conn,ps,null);*/
  67. DBUtil.close(null,ps,null);
  68. }
  69. return count;
  70. }
  71. // 4、根据账号查询账户
  72. public Account selectByActno(String actno, Connection conn){
  73. PreparedStatement ps = null;
  74. ResultSet rs = null;
  75. Account act = null;
  76. try {
  77. String sql = "select id,balance from t_act where actno = ?";
  78. ps = conn.prepareStatement(sql);
  79. ps.setString(1,actno);
  80. rs = ps.executeQuery();
  81. if (rs.next()) {
  82. Long id = rs.getLong("id");
  83. Double balance = rs.getDouble("balance");
  84. // 将结果集封装成java对象
  85. act = new Account();
  86. act.setId(id);
  87. act.setActno(actno);
  88. act.setBalance(balance);
  89. }
  90. } catch (SQLException e) {
  91. e.printStackTrace();
  92. } finally {
  93. DBUtil.close(null,ps,rs);
  94. }
  95. return act;
  96. }
  97. // 5、获取所有的账户
  98. public List<Account> selectAll(Connection conn){
  99. PreparedStatement ps = null;
  100. ResultSet rs = null;
  101. List<Account> list = new ArrayList<>();
  102. try {
  103. String sql = "select id,actno,balance from t_act";
  104. ps = conn.prepareStatement(sql);
  105. rs = ps.executeQuery();
  106. while (rs.next()) {
  107. // 取数据
  108. Long id = rs.getLong("id");
  109. String actno = rs.getString("actno");
  110. Double balance = rs.getDouble("balance");
  111. // 将结果集封装成java对象
  112. Account act = new Account();
  113. act.setId(id);
  114. act.setActno(actno);
  115. act.setBalance(balance);
  116. // 放到集合当中
  117. list.add(act);
  118. }
  119. } catch (SQLException e) {
  120. e.printStackTrace();
  121. } finally {
  122. DBUtil.close(null,ps,rs);
  123. }
  124. return list;
  125. }
  126. }

在AccountService的transfer方法中调用AccountDao中的任意一个方法;实际上访问的还是同一个线程对象:Thread.currentThread;所以就可以搞一个Map集合:key存线程对象,value存Connection对象!

  1. 手撕ThreadLocal源码

根据我们上面的理解,我们需要一个大Map集合;怎么设置这个大Map?不妨从一个简单的例子,一步步进行改进优化!

定义一个类Connection,作为参数传参

  1. package com.bjpowernode.threadlocal;
  2. public class Connection {
  3. }

定义UserService类,再定义个save方法;在save方法中调用UserDao类的insert方法

  1. package com.bjpowernode.threadlocal;
  2. public class UserService {
  3. private UserDao userDao = new UserDao();
  4. public void save(Connection conn){
  5. // 获取当前线程
  6. Thread thread = Thread.currentThread();
  7. System.out.println(thread);
  8. // 在UserService的save方法当中去调用UserDao类的insert方法
  9. userDao.insert(conn);
  10. }
  11. }

定义UserDao类

  1. package com.bjpowernode.threadlocal;
  2. public class UserDao {
  3. public void insert(Connection conn){
  4. // 获取当前线程
  5. Thread thread = Thread.currentThread();
  6. System.out.println(thread);
  7. System.out.println("UserDao insert");
  8. }
  9. }

测试类Test

  1. package com.bjpowernode.threadlocal;
  2. public class Test {
  3. public static void main(String[] args) {
  4. // 获取Connection对象
  5. Connection conn = new Connection();
  6. // 获取当前线程
  7. Thread thread = Thread.currentThread();
  8. System.out.println(thread);
  9. // 调用service
  10. UserService userService = new UserService();
  11. userService.save(conn);
  12. }
  13. }

通过测试发现,所有获得的是同一个线程对象,并且通过传参Connection,也可以正常调用

0c5a4713ceda4c3fbb7aac3ba1877330.png

既然所有的线程对象相同,我们不妨通过一个Map集合的方式Map<线程对象,Connection对象>,做到不传参Connection,也做到所有类的Connection对象是同一个对象!

编写一个MyThreadLocal类:里面是一个Map集合,所有需要和当前线程绑定的数据要放到这个容器里

  1. package com.bjpowernode.threadlocal;
  2. import java.util.HashMap;
  3. import java.util.Map;
  4. public class MyThreadLocal<T> {
  5. // 所有需要和当前线程绑定的数据要放到这个容器里
  6. private Map<Thread,T> map = new HashMap<>();
  7. // 存
  8. public void set(T t){
  9. map.put(Thread.currentThread(),t);
  10. }
  11. // 取
  12. public T get(){
  13. return map.get(Thread.currentThread());
  14. }
  15. // 删除
  16. public void remove(){
  17. map.remove(Thread.currentThread());
  18. }
  19. }

DBUtil工具类:用来获取Connection

  1. package com.bjpowernode.utils;
  2. import com.bjpowernode.threadlocal.Connection;
  3. import com.bjpowernode.threadlocal.MyThreadLocal;
  4. public class DBUtil { // 封装一个工具类
  5. // 静态变量:类加载时执行,并且只执行一次
  6. // 全局的大Map集合
  7. private static MyThreadLocal<Connection> local = new MyThreadLocal<>();
  8. // 每一次调用这个方法获取Connection对象
  9. public static Connection getConnection(){
  10. Connection connection = local.get();
  11. if (connection == null) {
  12. // 空的就new一次
  13. connection = new Connection();
  14. // 将new的Connection对象绑定到大Map集合
  15. local.set(connection);
  16. }
  17. return connection;
  18. }
  19. }

原来的Test类中new Connection对象,就不许需要了;方法中为了保持所有的Connection一样传参Connection也不需要了,直接使用工具类就可以获取全部相同的Connection对象!

Test类

  1. package com.bjpowernode.threadlocal;
  2. public class Test {
  3. public static void main(String[] args) {
  4. // 获取当前线程
  5. Thread thread = Thread.currentThread();
  6. System.out.println(thread);
  7. // 调用service
  8. UserService userService = new UserService();
  9. userService.save();
  10. }
  11. }

UserService类

  1. package com.bjpowernode.threadlocal;
  2. import com.bjpowernode.utils.DBUtil;
  3. public class UserService {
  4. private UserDao userDao = new UserDao();
  5. public void save(){
  6. // 获取当前线程
  7. Thread thread = Thread.currentThread();
  8. System.out.println(thread);
  9. // 直接获取Connextion对象
  10. Connection connection = DBUtil.getConnection();
  11. System.out.println(connection);
  12. // 在UserService的save方法当中去调用UserDao类的insert方法
  13. userDao.insert();
  14. }
  15. }

UserDao类

  1. package com.bjpowernode.threadlocal;
  2. import com.bjpowernode.utils.DBUtil;
  3. public class UserDao {
  4. public void insert(){
  5. // 获取当前线程
  6. Thread thread = Thread.currentThread();
  7. System.out.println(thread);
  8. // 获取Connection对象
  9. Connection connection = DBUtil.getConnection();
  10. System.out.println(connection);
  11. System.out.println("UserDao insert");
  12. }
  13. }

通过测试发现,这两个直接获取到的Connection对象,确实是同一个Connection对象

eccc7deae11f43de9710b8c78990bb51.png

第三种方法:项目中直接引入ThreadLocal(java.lang.ThreadLocal已经封装好了,不需要手写!在连接数据的DBUtil工具类中进行代码的修改

补充:Tomcat服务器内置了一个线程池,线程池中有很多线程对象:这些线程对象t1,t2,t3都是提前创建好的,也就是说t1,t2,t3存在重复使用的现象!

  1. package com.bjpowernode.bank.utils;
  2. import java.sql.*;
  3. import java.util.ResourceBundle;
  4. public class DBUtil {
  5. private static ResourceBundle bundle = ResourceBundle.getBundle("resources.jdbc");
  6. private static String driver = bundle.getString("driver");
  7. private static String url = bundle.getString("url");
  8. private static String user = bundle.getString("user");
  9. private static String password = bundle.getString("password");
  10. // 先提供给一个私有的构造方法:防止实例化对象
  11. private DBUtil(){}
  12. // 类加载时,注册驱动
  13. static {
  14. try {
  15. Class.forName(driver);
  16. } catch (ClassNotFoundException e) {
  17. e.printStackTrace();
  18. }
  19. }
  20. // 创建ThreadLocal(本质上是一个大Map)
  21. private static ThreadLocal<Connection> local = new ThreadLocal<>();
  22. // 获取连接
  23. public static Connection getConnection() throws SQLException {
  24. // 获取connection对象
  25. Connection connection = local.get();
  26. if (connection == null) {
  27. // 如果为空,就创建出来
  28. connection = DriverManager.getConnection(url, user, password);
  29. // 放入集合当中
  30. local.set(connection);
  31. }
  32. return connection;
  33. }
  34. // 关闭资源
  35. public static void close(Connection conn, Statement stmt, ResultSet rs){
  36. if (rs != null) {
  37. try {
  38. rs.close();
  39. } catch (SQLException e) {
  40. e.printStackTrace();
  41. }
  42. }
  43. if (stmt != null) {
  44. try {
  45. stmt.close();
  46. } catch (SQLException e) {
  47. e.printStackTrace();
  48. }
  49. }
  50. if (conn != null) {
  51. try {
  52. conn.close();
  53. // 思考:conn关闭之后,线程要从大Map移除?
  54. // 根本原因:Tomcat服务器时支持多线程的,也就是说一个人用过了t1线程,t1线程还有可能被其他用户使用
  55. local.remove();
  56. } catch (SQLException e) {
  57. e.printStackTrace();
  58. }
  59. }
  60. }
  61. }

修改以后,其他类的方法传参时就不需要传Connection对象了,因为它们获得的都是同一个Connection对象,可以正常关闭!

AccountDao类:关于数据的增删改查操作,不需要传参Connection,还是使用工具类直接获取

  1. package com.bjpowernode.bank.mvc;
  2. import com.bjpowernode.bank.utils.DBUtil;
  3. import java.sql.Connection;
  4. import java.sql.PreparedStatement;
  5. import java.sql.ResultSet;
  6. import java.sql.SQLException;
  7. import java.util.*;
  8. /**
  9. * @Author:朗朗乾坤
  10. * @Package:com.bjpowernode.bank.mvc
  11. * @Project:mvc-test
  12. * @name:AccountDao
  13. * @Date:2022/12/24 13:39
  14. * 专门处理数据的
  15. */
  16. public class AccountDao { // 负责数据的增删改查
  17. // 1、插入账户信息,1表示插入成功
  18. public int insert(Account act){
  19. PreparedStatement ps = null;
  20. int count = 0;
  21. try {
  22. Connection conn = DBUtil.getConnection();
  23. String sql = "insert into t_act(actno,balance) value(?,?)";
  24. ps = conn.prepareStatement(sql);
  25. ps.setString(1,act.getActno());
  26. ps.setDouble(2,act.getBalance());
  27. count = ps.executeUpdate();
  28. } catch (SQLException e) {
  29. e.printStackTrace();
  30. } finally {
  31. DBUtil.close(null,ps,null);
  32. }
  33. return count;
  34. }
  35. ///2、根据主键id删除账户
  36. public int deleteById(Long id){
  37. PreparedStatement ps = null;
  38. int count = 0;
  39. try {
  40. Connection conn = DBUtil.getConnection();
  41. String sql = "delete from t_act where id = ?";
  42. ps = conn.prepareStatement(sql);
  43. ps.setLong(1,id);
  44. count = ps.executeUpdate();
  45. } catch (SQLException e) {
  46. e.printStackTrace();
  47. } finally {
  48. DBUtil.close(null,ps,null);
  49. }
  50. return count;
  51. }
  52. // 3、更新账户
  53. public int update(Account act){
  54. PreparedStatement ps = null;
  55. int count = 0;
  56. try {
  57. Connection conn = DBUtil.getConnection();
  58. String sql = "update t_act set balance = ?,actno = ? where id = ?";
  59. ps = conn.prepareStatement(sql);
  60. ps.setDouble(1,act.getBalance());
  61. ps.setString(2,act.getActno());
  62. ps.setLong(3,act.getId());
  63. count = ps.executeUpdate();
  64. } catch (SQLException e) {
  65. e.printStackTrace();
  66. } finally {
  67. DBUtil.close(null,ps,null);
  68. }
  69. return count;
  70. }
  71. // 4、根据账号查询账户
  72. public Account selectByActno(String actno){
  73. PreparedStatement ps = null;
  74. ResultSet rs = null;
  75. Account act = null;
  76. try {
  77. Connection conn = DBUtil.getConnection();
  78. String sql = "select id,balance from t_act where actno = ?";
  79. ps = conn.prepareStatement(sql);
  80. ps.setString(1,actno);
  81. rs = ps.executeQuery();
  82. if (rs.next()) {
  83. Long id = rs.getLong("id");
  84. Double balance = rs.getDouble("balance");
  85. // 将结果集封装成java对象
  86. act = new Account();
  87. act.setId(id);
  88. act.setActno(actno);
  89. act.setBalance(balance);
  90. }
  91. } catch (SQLException e) {
  92. e.printStackTrace();
  93. } finally {
  94. DBUtil.close(null,ps,rs);
  95. }
  96. return act;
  97. }
  98. // 5、获取所有的账户
  99. public List<Account> selectAll(){
  100. PreparedStatement ps = null;
  101. ResultSet rs = null;
  102. List<Account> list = new ArrayList<>();
  103. try {
  104. Connection conn = DBUtil.getConnection();
  105. String sql = "select id,actno,balance from t_act";
  106. ps = conn.prepareStatement(sql);
  107. rs = ps.executeQuery();
  108. while (rs.next()) {
  109. // 取数据
  110. Long id = rs.getLong("id");
  111. String actno = rs.getString("actno");
  112. Double balance = rs.getDouble("balance");
  113. // 将结果集封装成java对象
  114. Account act = new Account();
  115. act.setId(id);
  116. act.setActno(actno);
  117. act.setBalance(balance);
  118. // 放到集合当中
  119. list.add(act);
  120. }
  121. } catch (SQLException e) {
  122. e.printStackTrace();
  123. } finally {
  124. DBUtil.close(null,ps,rs);
  125. }
  126. return list;
  127. }
  128. }

AccountService类:逻辑业务代码调用增删改查的方法也不需要传参数

  1. package com.bjpowernode.bank.mvc;
  2. import com.bjpowernode.bank.exception.AppException;
  3. import com.bjpowernode.bank.exception.MoneyNotException;
  4. import com.bjpowernode.bank.utils.DBUtil;
  5. import java.sql.Connection;
  6. import java.sql.SQLException;
  7. /**
  8. * @Author:朗朗乾坤
  9. * @Package:com.bjpowernode.bank.mvc
  10. * @Project:mvc-test
  11. * @name:AccountService
  12. * @Date:2022/12/24 15:43
  13. * 专门编写业务的
  14. */
  15. public class AccountService { // 处理业务
  16. // 每一个业务都有可能连接数据库,所以定义在方法的外面
  17. private AccountDao accountDao = new AccountDao();
  18. // 提供一个能够实现转账的业务方法(一个业务对应一个方法)
  19. public void transfer(String fromActno,String toActno,double money) throws MoneyNotException, AppException {
  20. try (Connection connection = DBUtil.getConnection()){//资源自动管理,写在这里面,就不用写finally关闭资源
  21. // 开启事务(需要使用Connection对象)
  22. connection.setAutoCommit(false);
  23. Account fromAct = accountDao.selectByActno(fromActno);
  24. if (fromAct.getBalance() < money){
  25. throw new MoneyNotException("对不起,余额不足");
  26. }
  27. // 程序走到这里,说明余额充足
  28. Account toAct = accountDao.selectByActno(toActno);
  29. // 修改金额(只是修改内存中java对象的余额)
  30. fromAct.setBalance(fromAct.getBalance()-money);
  31. toAct.setBalance(toAct.getBalance()+money);
  32. // 更新数据库中的余额
  33. int count = accountDao.update(fromAct);
  34. // 模拟异常
  35. /* String s = null;
  36. s.toString();*/
  37. count += accountDao.update(toAct);
  38. // 判断
  39. if (count != 2) {
  40. throw new AppException("转账异常!");
  41. }
  42. // 提交事务
  43. connection.commit();
  44. } catch (SQLException e) {
  45. // 这里先不会滚也可以,遇到异常就不会提交了
  46. throw new AppException("转账异常!");
  47. }
  48. }
  49. }

这样就能做到事务的控制!

不能功能放到不同的包下

按照三层架构分析:web下的UserServlet是表示层、service包下的UserService是业务层、dao包下的UserDao是持久化层
按照MVC架构模式分析:pojo下的Account类、dao包下的UserDao、service包下的UserService属于M、所有的.jsp属于V、web下的UserServlet属于C

a64e965e58d94efabf928751c18578b4.png

并把AccountService和AccountDao类写成接口的形式(面向接口编程),然后写对应的实现类去实现这个接口即可!

发表评论

表情:
评论列表 (有 0 条评论,185人围观)

还没有评论,来说两句吧...

相关阅读

    相关 MVC模式和三层架构

    MVC模式和三层架构 MVC 模式和三层架构是一些理论的知识,将来我们使用了它们进行代码开发会让我们代码维护性和扩展性更好。 MVC模式 MVC 是一种分层开发的

    相关 MVC架构模式详细说明

    一、简介:   架构模式是一个通用的、可重用的解决方案,用于在给定上下文中的软件体系结构中经常出现的问题。架构模式与软件设计模式类似,但具有更广泛的范围。   模型-视图-

    相关 MVC架构模式

    MVC架构模式 MVC是三个单词的缩写,分别为:模型(Model),视图(View)和控制(Controller)。 MVC模式的目的就是是向Web系统的职能分工具体如