老杜 JavaWeb 讲解(二十一)——通过银行账户转账业务讲解MVC架构

发布时间 2023-08-17 20:51:00作者: 猪无名

老杜-通过银行账户转账业务讲解MVC架构

老杜-银行账户转账(mvc001)

这个项目将层层迭代,最终成为MVC架构的项目。

老杜第一次写代码并没有使用JDBC的封装类,但大差不差,这里即使用了之前的DBUtil.java,代码依然很杂乱。

建立数据库

数据库名:mvc

字符集:utf8mb4

排序规则:utf8mb4_unicode_ci

建表:t_act

项目目录

具体代码

AppException.java

package com.zwm.bank.exceptions;

public class AppException extends Exception {
    public AppException() {
        super();
    }

    public AppException(String message) {
            super(message);
        }
    }

MonerNotEnoughException.java

package com.zwm.bank.exceptions;

public class MonerNotEnoughException  extends Exception {

    public MonerNotEnoughException() {
    }

    public MonerNotEnoughException(String message) {
        super(message);
    }
}

AccountTransferServlet.java

package com.zwm.bank.web.servlet;

import com.zwm.bank.exceptions.AppException;
import com.zwm.bank.exceptions.MonerNotEnoughException;
import com.zwm.bank.web.utils.DBUtil;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import java.io.IOException;
import java.io.PrintWriter;
import java.math.BigDecimal;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

@WebServlet("/transfer")
public class AccountTransferServlet 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"));

        Connection conn = null;
        PreparedStatement ps = null;
        PreparedStatement ps2 = null;
        PreparedStatement ps3 = null;
        ResultSet rs = null;

        try {
            // 转账之前判断账户余额是否充足
            //获取连接
            conn = DBUtil.getConnection();
            //获取预编译的数据库对象
            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 MonerNotEnoughException("抱歉,余额不足");
                }

                //当运行到这里说明,余额一定充足
                //开始转账,fromActno账户上减去money ,toActno账户上加上money
                //开启事务,取消自动提交,改为手动提交,业务完成之后再提交。
                conn.setAutoCommit(false);
                //1.先更新from账户的余额
                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();


                //2.再更新to账户的余额
                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 AppException("App出错了,请联系管理员。");
                 }

                 //手动提交
                conn.commit();
                 //项目能运行到这里,代表转账成功。
                out.print("转账成功");
            }
        } catch (Exception e) {
            //保险起见,回滚事务。
            try {
                if(conn != null){
                    conn.rollback();
                }
            } catch (SQLException ex) {
                throw new RuntimeException(ex);
            }
            out.print(e.getMessage());
        }finally {
            DBUtil.closeForResultSet(rs);
            DBUtil.closeForStatement(ps);
            DBUtil.closeForStatement(ps2);
            DBUtil.closeForStatement(ps3);
            DBUtil.closeForConnection(conn);
        }
    }

}

DBUtil.java

package com.zwm.bank.web.utils;

import java.sql.*;
import java.util.ResourceBundle;

public class DBUtil {
    //静态变量:在类加载时,执行
    //顺序:自上而下

    //属性资源文件绑定,根据属性配置文件的key获取value。
    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");

    static {
        //注册驱动只需要注册一次,放在静态代码块中.DBUtil类加载的时候执行。
        try {
            //com.mysql.jdbc.Driver是连接数据库的驱动,不能写死,因为以后可能要连接Oracle数据库。
            //如果直接写死,后续更改可能要修改java代码,显然违背OCP原则(对扩展开放,对修改关闭)。
            //Class.forName("com.mysql.jdbc.Driver");
            Class.forName(driver);
        } catch (ClassNotFoundException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * 获取数据库连接对象
     * @return connection连接对象
     * @throws SQLException
     */
    public static Connection getConnection()  throws SQLException {
        //注册驱动只需要注册一次, 因此放在静态代码块中。
        //获取连接
        Connection connection = DriverManager.getConnection(url, user, password);

        return connection;
    }

    /**
     *释放资源
     * @param connection     连接对象
     * @param statement      数据库操作对象
     * @param resultSet      结果集对象
     */
    public static void close(Connection connection, Statement statement, ResultSet resultSet){
        if (resultSet != null) {
            try {
                resultSet.close();
            } catch (SQLException e) {
                throw new RuntimeException(e);
            }
        }
        if (statement != null) {
            try {
                statement.close();
            } catch (SQLException e) {
                throw new RuntimeException(e);
            }
        }
        if (connection != null) {
            try {
                connection.close();
            } catch (SQLException e) {
                throw new RuntimeException(e);
            }
        }

    }

    public static void closeForStatement(Statement statement){
        if (statement != null) {
            try {
                statement.close();
                System.out.println("statement closed");
            } catch (SQLException e) {
                throw new RuntimeException(e);
            }
        }
    }
    public static void closeForConnection(Connection connection){

        if (connection != null) {
            try {
                connection.close();
                System.out.println("Connection closed");
            } catch (SQLException e) {
                throw new RuntimeException(e);
            }
        }

    }


    public static void closeForResultSet( ResultSet resultSet){
        if (resultSet != null) {
            try {
                resultSet.close();
                System.out.println("resultSet closed");
            } catch (SQLException e) {
                throw new RuntimeException(e);
            }
        }
    }


}

jdbc.properties

driver=com.mysql.cj.jdbc.Driver
url=jdbc:mysql://localhost:3306/数据库名称
user=用户名
password=密码

index.jsp

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
  <base href="${pageContext.request.scheme}://${pageContext.request.serverName}:${pageContext.request.serverPort}${pageContext.request.contextPath}/">
  <title>银行账户转账</title>
  <style>
    body {
      font-family: Arial, sans-serif;
      background-color: #f5f5f5;
    }

    .container {
      width: 400px;
      margin: 0 auto;
      padding: 20px;
      background-color: #fff;
      box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
      border-radius: 5px;
    }

    .form-group {
      margin-bottom: 15px;
    }

    .form-group label {
      display: block;
      font-weight: bold;
      margin-bottom: 5px;
    }

    .form-group input[type="text"] {
      width: 100%;
      padding: 8px;
      border: 1px solid #ccc;
      border-radius: 4px;
    }

    .form-submit {
      text-align: center;
    }

    .form-submit input[type="submit"] {
      padding: 10px 20px;
      font-size: 16px;
      background-color: #4CAF50;
      color: #fff;
      border: none;
      border-radius: 4px;
      cursor: pointer;
    }
  </style>
</head>
<body>
<div class="container">
  <h1>银行账户转账</h1>
  <form action="transfer" method="post">
    <div class="form-group">
      <label>转出账户:</label>
      <input type="text" name="fromActno">
    </div>
    <div class="form-group">
      <label>转入账户:</label>
      <input type="text" name="toActno">
    </div>
    <div class="form-group">
      <label>转账金额:</label>
      <input type="text" name="money" pattern="[0-9]+" title="请输入数字">
    </div>
    <div class="form-submit">
      <input type="submit" value="转账">
    </div>
  </form>
</div>
</body>
</html>

项目存在问题

分析代码:
1.负责数据的接收。
2.负责了核心业务的处理。
3.负责数据库表中数据的CRUD操作。
4.负责页面的展示。
5.负责异常处理。
缺点:
1.代码的复用性太差。
原因:没有进行组件分工,没有独立组件的概念。
2.耦合度太高,代码很难扩展。代码耦合度太高,无法实现复用。扩展力太差。
3.操作数据库的代码和业务逻辑代码混合在一起,编写业务代码的时候很容易出错。
4.代码还要负责页面的数据展示。

如何解决上述问题:使用MVC架构进行分层。

对应现实:
公司里面一般都有很多员工。每个员工各司其职,为什么?
进行明确的职能分工,就算某个人出现问题,也不会对公司造成重大影响。

MVC介绍

MVC(Model-View-Controller)是一种常用的软件架构模式,用于将应用程序的逻辑、数据和用户界面分离开来,以提高代码的可维护性和可扩展性。它将应用程序分为三个主要组件:模型(Model)、视图(View)和控制器(Controller)。

  1. 模型(Model):模型负责管理应用程序的数据和业务逻辑。它包括数据结构、数据库操作、文件读写等与数据相关的功能。模型不关心数据是如何显示或如何被用户操作的,只负责对数据进行处理和管理。
  2. 视图(View):视图是应用程序的用户界面,负责数据的显示和呈现给用户。它可以是一个网页、一个窗口、一个控件等用户可以直接看到和操作的界面元素。视图从模型中获取数据,并将其呈现给用户。视图不包含任何业务逻辑,只负责数据的展示。
  3. 控制器(Controller):控制器是模型和视图之间的桥梁,负责接收用户的输入、处理用户请求,并更新模型和视图。它根据用户的操作调用相应的模型方法,获取数据并更新模型,然后通知视图进行更新,以反映最新的数据状态。控制器还可以处理用户的输入验证、权限控制等与应用程序逻辑相关的功能。

MVC架构模式的优点包括:

  • 分离关注点:将数据、界面和逻辑分离,使得各个组件的职责清晰,易于开发和维护。
  • 提高代码复用性:模型和视图可以独立于彼此存在,可以更轻松地进行修改、替换和重用。
  • 支持并行开发:不同的开发人员可以同时工作在模型、视图和控制器上,加快开发效率。
  • 支持多种界面表现形式:由于视图与模型解耦,可以方便地为应用程序添加多个不同的用户界面。

老杜-银行账户转账(mvc002)

项目目录:

具体代码:

AppException.java

package com.zwm.bank.exceptions;

public class AppException extends Exception {
    public AppException() {
        super();
    }

    public AppException(String message) {
            super(message);
        }
    }

MonerNotEnoughException.java

package com.zwm.bank.exceptions;

public class MonerNotEnoughException  extends Exception {

    public MonerNotEnoughException() {
    }

    public MonerNotEnoughException(String message) {
        super(message);
    }
}

Account.java

package com.zwm.bank.mvc;

/**
 * @author 猪无名
 * @date 2023/8/14 14 26
 * @version 1.0
 * @since 1.0
 * discription:账户实体类 ,封装账户信息。一般一张表对应一个实体类。
 */
public class Account {
    //封装类里面的属性一般不使用基本数据类型,建议使用包装类,因为传过来的数值可能为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.java

package com.zwm.bank.mvc;

import com.zwm.bank.utils.DBUtil;

import java.sql.*;
import java.util.ArrayList;
import java.util.List;

public class AccountDao {
    /**
     * 插入账户信息
     * @param act
     * @return 1表示插入成功
     */
    public int insert(Account act){
        Connection connection = null;
        PreparedStatement preparedStatement = null;
        int count = 0;
        try {
             connection = DBUtil.getConnection();
             String sql = "insert into t_act(actno,balance) values(?,?)";
              preparedStatement = connection.prepareStatement(sql);
              preparedStatement.setString(1,act.getActno());
              preparedStatement.setDouble(2,act.getBalance());
              count = preparedStatement.executeUpdate();
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }finally {
            DBUtil.close(connection,preparedStatement,null);
        }
        return count;
    }

    /**
     * 根据id(主键)删除账户信息
     * @param id
     * @return
     */
    public int deleteById(Long id){
        Connection connection = null;
        PreparedStatement preparedStatement = null;
        int count = 0;
        try {
            connection  = DBUtil.getConnection();
            String sql = "delete from t_act where id = ?";
            preparedStatement = connection.prepareStatement(sql);
            preparedStatement.setLong(1,id);
            count = preparedStatement.executeUpdate();

        } catch (SQLException e) {
            throw new RuntimeException(e);
        }finally {
            DBUtil.close(connection,preparedStatement,null);
        }
        return count;
    }

    /**
     * 更新账户信息
     * @param act
     * @return
     */
    public int update(Account act){//负责更新
        Connection connection = null;
        PreparedStatement preparedStatement = null;
        int count = 0;
        try {
            connection  = DBUtil.getConnection();
            String sql = "update t_act set actno = ? ,balance = ? where id = ?";
            preparedStatement = connection.prepareStatement(sql);
            preparedStatement.setString(1,act.getActno());
            preparedStatement.setDouble(2,act.getBalance());
            preparedStatement.setLong(3,act.getId());
            count = preparedStatement.executeUpdate();
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }finally {
            DBUtil.close(connection,preparedStatement,null);
        }
        return count;
    }

    /**
     * 根据actno(账号)查询账户信息
     * @param actno
     * @return Account
     */
    public Account selectByActno(String actno){
        Connection connection = null;
        PreparedStatement preparedStatement = null;
        ResultSet resultSet = null;
        Account act = null;
        try {
            connection  = DBUtil.getConnection();
            String sql = "select id,balance from t_act where actno = ?";
            preparedStatement = connection.prepareStatement(sql);
            preparedStatement.setString(1,actno);
            resultSet = preparedStatement.executeQuery();
            if(resultSet.next()){
                long id = resultSet.getLong("id");
                Double balance = resultSet.getDouble("balance");
                //将结果集封装为一个对象。
                act = new Account();
                act.setId(id);
                act.setActno(actno);
                act.setBalance(balance);
            }
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }finally {
            DBUtil.close(connection,preparedStatement,resultSet);
        }
        return act;
    }

    /**
     * 查询所有账户信息
     * @return List
     */
    public List<Account> selectAll(){
        Connection connection = null;
        PreparedStatement preparedStatement = null;
        ResultSet resultSet = null;
        List<Account> list = new ArrayList<>();
        try {
            connection  = DBUtil.getConnection();
            String sql = "select id,actno,balance from t_act ";
            preparedStatement = connection.prepareStatement(sql);
            resultSet = preparedStatement.executeQuery();
            while(resultSet.next()){
                //取出数据
                long id = resultSet.getLong("id");
                String actno = resultSet.getString("actno");
                Double balance = resultSet.getDouble("balance");
                //封装对象
                Account act = new Account(id,actno,balance);
                //加到List集合
                list.add(act);
            }
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }finally {
            DBUtil.close(connection,preparedStatement,resultSet);
        }
        return list;
    }

}

AccountService.java

package com.zwm.bank.mvc;

import com.zwm.bank.exceptions.AppException;
import com.zwm.bank.exceptions.MonerNotEnoughException;

/**
 * @version 1.0
 * @since 1.0
 * discription:专门处理account业务的一个类。在这个类里面只专注业务,不写别的。命名为***Service,***Biz... ...
 */
public class AccountService {
    private AccountDao accountDao = new AccountDao();
    //这里的方法起名要体现业务。事务也应该在这进行控制,一般一个业务对应一个事务。
    public void transfer(String fromActno, String toActno, double money) throws MonerNotEnoughException, AppException {
        //1.根据账号,查询余额是否充足。
        Account fromAct = accountDao.selectByActno(fromActno);
        if(fromAct.getBalance()<money){
                throw  new MonerNotEnoughException("抱歉,余额不足。");
        }
        //2.程序到这一步,说明余额充足。
        Account toAct = accountDao.selectByActno(toActno);

        //3.修改余额(只修改内存中的java对象的余额)
        fromAct.setBalance(fromAct.getBalance() - money);
        toAct.setBalance(toAct.getBalance() + money);
        //4.更新数据库中的数据
        int count =  accountDao.update(fromAct);
        count += accountDao.update(toAct);

        if(count!=2){
            throw  new AppException("账户转账异常!!!");
        }
    }
}

AccountServlet.java

package com.zwm.bank.mvc;

import com.zwm.bank.exceptions.AppException;
import com.zwm.bank.exceptions.MonerNotEnoughException;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import java.io.IOException;

@WebServlet("/transfer")
public class AccountServlet extends HttpServlet {
    AccountService accountService = new AccountService();
    //之前在这个方法内做了很多的事情,现在思考简化一下。
    //让它充当一个司令官(Controller),他负责调度其他组件(Model,View)来完成任务。
    @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"));
        //调用业务方法处理业务
        try {
            accountService.transfer(fromActno,toActno,money);
            //程序运行到这里,代表转账成功。
            response.sendRedirect(request.getContextPath()+"/success.jsp");
        }catch (MonerNotEnoughException e){
            //执行到这里失败,代表余额不足。
            response.sendRedirect(request.getContextPath()+"/moneyNotEnough.jsp");
        }catch (AppException e) {
            //程序运行到这里,代表转账失败。
            response.sendRedirect(request.getContextPath()+"/fail.jsp");
        }
        //展示处理结果
    }
}

DBUtil.java

package com.zwm.bank.utils;

import java.sql.*;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.ResourceBundle;

/**
 * discription:JDBC工具类
 * @version 1.0
 * @since  1.0
 */
public class DBUtil {
    private static final 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();
        }
    }

    /**这里没有使用数据库连接池,直接创建连接对象。
     * @return 连接对象
     * @throws SQLException
     */
    public static Connection getConnection() throws SQLException {
        Connection connection = DriverManager.getConnection(url, user, password);
        return connection;
    }

    /**
     * 关闭资源
     * @param connection 连接对象
     * @param statement  执行对象
     * @param resultSet   结果集
     */
    public static void close(Connection connection, Statement statement, ResultSet resultSet) {
        if (resultSet != null) {
            try {
                resultSet.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
        if (statement != null) {
            try {
                statement.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
        if (connection != null) {
            try {
                connection.close();
            } catch (SQLException e) {
                throw new RuntimeException(e);
            }
        }
    }



}

jdbc.properties

driver=com.mysql.cj.jdbc.Driver
url=jdbc:mysql://localhost:3306/mvc
user=root
password=root

index.jsp

<%--
  Created by IntelliJ IDEA.
  User: 11933
  Date: 2023/8/10
  Time: 12:07
  To change this template use File | Settings | File Templates.
--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
  <base href="${pageContext.request.scheme}://${pageContext.request.serverName}:${pageContext.request.serverPort}${pageContext.request.contextPath}/">
  <title>银行账户转账</title>
  <style>
    body {
      font-family: Arial, sans-serif;
      background-color: #f5f5f5;
    }

    .container {
      width: 400px;
      margin: 0 auto;
      padding: 20px;
      background-color: #fff;
      box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
      border-radius: 5px;
    }

    .form-group {
      margin-bottom: 15px;
    }

    .form-group label {
      display: block;
      font-weight: bold;
      margin-bottom: 5px;
    }

    .form-group input[type="text"] {
      width: 100%;
      padding: 8px;
      border: 1px solid #ccc;
      border-radius: 4px;
    }

    .form-submit {
      text-align: center;
    }

    .form-submit input[type="submit"] {
      padding: 10px 20px;
      font-size: 16px;
      background-color: #4CAF50;
      color: #fff;
      border: none;
      border-radius: 4px;
      cursor: pointer;
    }
  </style>
</head>
<body>
<div class="container">
  <h1>银行账户转账</h1>
  <form action="transfer" method="post">
    <div class="form-group">
      <label>转出账户:</label>
      <input type="text" name="fromActno">
    </div>
    <div class="form-group">
      <label>转入账户:</label>
      <input type="text" name="toActno">
    </div>
    <div class="form-group">
      <label>转账金额:</label>
      <input type="text" name="money" pattern="[0-9]+" title="请输入数字">
    </div>
    <div class="form-submit">
      <input type="submit" value="转账">
    </div>
  </form>
</div>
</body>
</html>

moneyNotEnough.jsp

<%--
  Created by IntelliJ IDEA.
  User: 11933
  Date: 2023/8/14
  Time: 17:32
  To change this template use File | Settings | File Templates.
--%>
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
  <title>转账失败</title>
  <style>
    body {
      font-family: Arial, sans-serif;
      background-color: #f2f2f2;
      text-align: center;
      padding-top: 50px;
    }

    .container {
      background-color: #fff;
      border-radius: 5px;
      box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
      max-width: 400px;
      margin: 0 auto;
      padding: 30px;
    }

    h1 {
      font-size: 24px;
      color: #333;
    }

    p {
      font-size: 18px;
      color: #666;
      margin-bottom: 20px;
    }

    .error-icon {
      width: 80px;
      height: 80px;
      background-image: url('<%= request.getContextPath() %>/images/error-icon.png');
      background-size: cover;
      margin: 0 auto 20px;
    }

    .btn {
      display: inline-block;
      background-color: #F44336;
      color: #fff;
      padding: 10px 20px;
      border-radius: 4px;
      text-decoration: none;
      transition: background-color 0.3s;
    }

    .btn:hover {
      background-color: #d32f2f;
    }
  </style>
</head>
<body>
<div class="container">
  <div class="error-icon"></div>
  <h1>转账失败</h1>
  <p>余额不足。</p>
  <a class="btn" href="index.jsp">返回</a>
</div>
</body>
</html>

success.jsp

<%--
  Created by IntelliJ IDEA.
  User: 11933
  Date: 2023/8/14
  Time: 17:16
  To change this template use File | Settings | File Templates.
--%>
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
  <title>转账成功</title>
  <style>
    body {
      font-family: Arial, sans-serif;
      background-color: #f2f2f2;
      text-align: center;
      padding-top: 50px;
    }
    .container {
      background-color: #fff;
      border-radius: 5px;
      box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
      max-width: 400px;
      margin: 0 auto;
      padding: 30px;
    }
    h1 {
      font-size: 24px;
      color: #333;
    }
    p {
      font-size: 18px;
      color: #666;
      margin-bottom: 20px;
    }
    .success-icon {
      width: 80px;
      height: 80px;
      background-image: url('<%= request.getContextPath() %>/images/success-icon.png');
      background-size: cover;
      margin: 0 auto 20px;
    }
    .btn {
      display: inline-block;
      background-color: #4CAF50;
      color: #fff;
      padding: 10px 20px;
      border-radius: 4px;
      text-decoration: none;
      transition: background-color 0.3s;
    }
    .btn:hover {
      background-color: #45a049;
    }
  </style>
</head>
<body>
<div class="container">
  <div class="success-icon"></div>
  <h1>转账成功</h1>
  <p>您的转账已成功完成。</p>
</div>
</body>
</html>

fail.jsp

<%--
  Created by IntelliJ IDEA.
  User: 11933
  Date: 2023/8/14
  Time: 17:32
  To change this template use File | Settings | File Templates.
--%>
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
    <title>转账失败</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            background-color: #f2f2f2;
            text-align: center;
            padding-top: 50px;
        }

        .container {
            background-color: #fff;
            border-radius: 5px;
            box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
            max-width: 400px;
            margin: 0 auto;
            padding: 30px;
        }

        h1 {
            font-size: 24px;
            color: #333;
        }

        p {
            font-size: 18px;
            color: #666;
            margin-bottom: 20px;
        }

        .error-icon {
            width: 80px;
            height: 80px;
            background-image: url('<%= request.getContextPath() %>/images/error-icon.png');
            background-size: cover;
            margin: 0 auto 20px;
        }

        .btn {
            display: inline-block;
            background-color: #F44336;
            color: #fff;
            padding: 10px 20px;
            border-radius: 4px;
            text-decoration: none;
            transition: background-color 0.3s;
        }

        .btn:hover {
            background-color: #d32f2f;
        }
    </style>
</head>
<body>
<div class="container">
    <div class="error-icon"></div>
    <h1>转账失败</h1>
    <p>很抱歉,您的转账未能成功。</p>
    <a class="btn" href="index.jsp">返回</a>
</div>
</body>
</html>

MVC代码逻辑:

对应三层架构:

老杜-银行账户转账(mvc003)

解决mvc002事务不同步的问题。

修改代码:

AccountDao.java

package bank.mvc;



import bank.utils.DBUtil;

import java.sql.*;
import java.util.ArrayList;
import java.util.List;

public class AccountDao {
    /**
     * 插入账户信息
     * @param act
     * @return 1表示插入成功
     */
    public int insert(Account act,Connection connection){
        PreparedStatement preparedStatement = null;
        int count = 0;
        try {
             String sql = "insert into t_act(actno,balance) values(?,?)";
              preparedStatement = connection.prepareStatement(sql);
              preparedStatement.setString(1,act.getActno());
              preparedStatement.setDouble(2,act.getBalance());
              count = preparedStatement.executeUpdate();
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }finally {
            DBUtil.close(null,preparedStatement,null);
        }
        return count;
    }

    /**
     * 根据id(主键)删除账户信息
     * @param id
     * @return
     */
    public int deleteById(Long id,Connection connection){
        PreparedStatement preparedStatement = null;
        int count = 0;
        try {
            String sql = "delete from t_act where id = ?";
            preparedStatement = connection.prepareStatement(sql);
            preparedStatement.setLong(1,id);
            count = preparedStatement.executeUpdate();

        } catch (SQLException e) {
            throw new RuntimeException(e);
        }finally {
            DBUtil.close(null,preparedStatement,null);
        }
        return count;
    }

    /**
     * 更新账户信息
     * @param act
     * @return
     */
    public int update(Account act,Connection connection){//负责更新
        PreparedStatement preparedStatement = null;
        int count = 0;
        try {
            String sql = "update t_act set actno = ? ,balance = ? where id = ?";
            preparedStatement = connection.prepareStatement(sql);
            preparedStatement.setString(1,act.getActno());
            preparedStatement.setDouble(2,act.getBalance());
            preparedStatement.setLong(3,act.getId());
            count = preparedStatement.executeUpdate();
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }finally {
            DBUtil.close(null,preparedStatement,null);
        }
        return count;
    }

    /**
     * 根据actno(账号)查询账户信息
     * @param actno
     * @return Account
     */
    public Account selectByActno(String actno,Connection connection){
        PreparedStatement preparedStatement = null;
        ResultSet resultSet = null;
        Account act = null;
        try {
            String sql = "select id,balance from t_act where actno = ?";
            preparedStatement = connection.prepareStatement(sql);
            preparedStatement.setString(1,actno);
            resultSet = preparedStatement.executeQuery();
            if(resultSet.next()){
                long id = resultSet.getLong("id");
                Double balance = resultSet.getDouble("balance");
                //将结果集封装为一个对象。
                act = new Account();
                act.setId(id);
                act.setActno(actno);
                act.setBalance(balance);
            }
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }finally {
            DBUtil.close(null,preparedStatement,resultSet);
        }
        return act;
    }

    /**
     * 查询所有账户信息
     * @return List
     */
    public List<Account> selectAll(Connection connection){
        PreparedStatement preparedStatement = null;
        ResultSet resultSet = null;
        List<Account> list = new ArrayList<>();
        try {
            String sql = "select id,actno,balance from t_act ";
            preparedStatement = connection.prepareStatement(sql);
            resultSet = preparedStatement.executeQuery();
            while(resultSet.next()){
                //取出数据
                long id = resultSet.getLong("id");
                String actno = resultSet.getString("actno");
                Double balance = resultSet.getDouble("balance");
                //封装对象
                Account act = new Account(id,actno,balance);
                //加到List集合
                list.add(act);
            }
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }finally {
            DBUtil.close(null,preparedStatement,resultSet);
        }
        return list;
    }
}

AccountService.java

package bank.mvc;

import bank.exceptions.AppException;
import bank.exceptions.MonerNotEnoughException;
import bank.utils.DBUtil;
import java.sql.Connection;
import java.sql.SQLException;

/**
 * @version 1.0
 * @since 1.0
 * discription:专门处理account业务的一个类。在这个类里面只专注业务,不写别的。命名为***Service,***Biz... ...
 */
public class AccountService {
    private AccountDao accountDao = new AccountDao();
    //这里的方法起名要体现业务。事务也应该在这进行控制,一般一个业务对应一个事务。
    public void transfer(String fromActno, String toActno, double money)
            throws MonerNotEnoughException, AppException {
       //开启事务(需要获取Connection对象,这个操作很不正常,因为需要在业务层写JDBC的代码。但先这样写。)
        try ( Connection connection = DBUtil.getConnection()){
            //禁止自动提交
           connection.setAutoCommit(false);

            //1.根据账号,查询余额是否充足。
            Account fromAct = accountDao.selectByActno(fromActno,connection);
            if(fromAct.getBalance()<money){
                throw  new MonerNotEnoughException("抱歉,余额不足。");
            }
            //2.程序到这一步,说明余额充足。
            Account toAct = accountDao.selectByActno(toActno,connection);
            //3.修改余额(只修改内存中的java对象的余额)
            fromAct.setBalance(fromAct.getBalance() - money);
            toAct.setBalance(toAct.getBalance() + money);
            //4.更新数据库中的数据
            int count =  accountDao.update(fromAct,connection);
            count += accountDao.update(toAct,connection);

            if(count!=2){
                throw  new AppException("账户转账异常!!!");
            }
            
           //提交事务
            connection.commit();
        } catch (SQLException e) {
            throw  new AppException("账户转账异常!!!");
        }
    }
}

遗留问题:

通过传参可以实现Connection对象是同一个,但代码很丑陋,需要进一步修改。

老杜-银行账户转账(mvc004-ThreadLocal)

在一个类的方法里面调用另外一个类的方法,这种操作属于同一线程中的操作。

将Connection对象和线程对象绑定,放在一个Map集合里面,这样就可以实现Connection对象是同一个。

代码简单模拟ThreadLocal

package com.zwm.ThreadLocal;

public class Connection {
}
package com.zwm.ThreadLocal;

public class DBUtil {
    //静态变量,类加载的时候执行,只执行一次。
    private static MyThreadLocal<Connection> local = new MyThreadLocal<>();  //全局Map集合

    public static Connection getConnection() {
        //第一次调用get方法,一定是空的。所以在第一次的时候要进行绑定。
        Connection connection = local.get();
        //当第一次调用为空时,进行Connection对象的绑定。
        if (connection == null) {
            connection = new Connection();
            local.set(connection);
        }
        return connection;
    }
}

package com.zwm.ThreadLocal;

import java.util.HashMap;
import java.util.Map;

public class MyThreadLocal<T> {
    /**
     * 所有需要和当前线程绑定的数据要放到这个容器当中。
     */
    private Map<Thread,T> map = new HashMap<>();
    /**
     * 向ThreadLocal中绑定
     */
    public void set(T obj){
        map.put(Thread.currentThread(),obj);
    }
    /**
     * 从ThreadLocal中获取
     */
    public T get(){
        return map.get(Thread.currentThread());
    }
    /**
     * 删除ThreadLocal中的数据
     */
    public void remove(){
        map.remove(Thread.currentThread());
    }
}
package com.zwm.ThreadLocal;

public class Test {
    public static void main(String[] args) {
        UserService userService = new UserService();
        Thread thread = Thread.currentThread();
        System.out.println(thread);
        userService.save();
    }
}

package com.zwm.ThreadLocal;

public class UserDao {
    public void insert(){
        Thread thread = Thread.currentThread();
        System.out.println(thread);

        Connection connection = DBUtil.getConnection();
        System.out.println(connection);

        System.out.println("User DAO insert");
    }
}

package com.zwm.ThreadLocal;

public class UserService {
    private UserDao userDao = new UserDao();
    public void save(){
        Thread thread = Thread.currentThread();
        System.out.println(thread);
        Connection connection = DBUtil.getConnection();
        System.out.println(connection);
        userDao.insert();
    }
}

java.lang包下的ThreadLocal

ThreadLocal是Java中的一个线程局部变量,它为每个线程提供了一个独立的变量副本。在多线程环境中,每个线程可以独立地操作自己的ThreadLocal变量,而不会对其他线程产生影响。

ThreadLocal通常用于解决多线程并发访问共享变量时的线程安全问题。通过将变量声明为ThreadLocal类型,可以确保每个线程都拥有自己的变量副本,避免了线程间相互干扰的问题。

使用ThreadLocal时,每个线程通过调用ThreadLocal的get()方法获取自己的变量副本,并通过set()方法设置自己的变量值。这样,即使多个线程同时访问同一个ThreadLocal变量,它们之间的数据也是独立的,彼此不会相互干扰。

值得注意的是,当线程结束后,ThreadLocal变量的副本会被自动清除,从而避免了内存泄漏的问题。

ThreadLocal提供了一种简单而有效的方式来实现线程间的数据隔离,使得每个线程都可以独立地操作自己的变量,从而提高了多线程程序的性能和可靠性。

去除线程安全这些问题,可以将它看作一个Map集合,里面存储的key为当前线程,value为保存的对象。

具体修改部分代码:

DBUtil.java

package bank.utils;

import java.sql.*;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.ResourceBundle;

public class DBUtil {
    private static final 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();
        }
    }

    private static ThreadLocal<Connection> threadLocal = new ThreadLocal<>();
    /**这里没有使用数据库连接池,而是直接创建连接对象。
     * @return 连接对象
     * @throws SQLException
     */
    public static Connection getConnection() throws SQLException {
        Connection connection = threadLocal.get();
        if(connection == null){
            connection = DriverManager.getConnection(url, user, password);
            threadLocal.set(connection);
        }
        return connection;
    }

    /**
     * 关闭资源
     * @param connection 连接对象
     * @param statement  执行对象
     * @param resultSet   结果集
     */
    public static void close(Connection connection, Statement statement, ResultSet resultSet) {
        if (resultSet != null) {
            try {
                resultSet.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
        if (statement != null) {
            try {
                statement.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
        if (connection != null) {
            try {
                connection.close();
                //Tomcat服务器是支持线程池的,线程池的线程是复用的,所以需要把连接对象从线程池中移除。
                threadLocal.remove();
            } catch (SQLException e) {
                throw new RuntimeException(e);
            }
        }
    }

}

AccountService.java

package bank.mvc;

import bank.exceptions.AppException;
import bank.exceptions.MonerNotEnoughException;
import bank.utils.DBUtil;

import java.sql.Connection;
import java.sql.SQLException;

/**
 * @version 1.0
 * @since 1.0
 * discription:专门处理account业务的一个类。在这个类里面只专注业务,不写别的。命名为***Service,***Biz... ...
 */
public class AccountService {
    private AccountDao accountDao = new AccountDao();
    //这里的方法起名要体现业务。事务也应该在这进行控制,一般一个业务对应一个事务。
    public void transfer(String fromActno, String toActno, double money)
            throws MonerNotEnoughException, AppException {
       //开启事务(需要获取Connection对象,这个操作很不正常,因为需要在业务层写JDBC的代码。但先这样写。)
        try (Connection  connection = DBUtil.getConnection()){
            //禁止自动提交
            connection.setAutoCommit(false);

            //1.根据账号,查询余额是否充足。
            Account fromAct = accountDao.selectByActno(fromActno);
            if(fromAct.getBalance()<money){
                throw  new MonerNotEnoughException("抱歉,余额不足。");
            }
            //2.程序到这一步,说明余额充足。
            Account toAct = accountDao.selectByActno(toActno);

            //3.修改余额(只修改内存中的java对象的余额)
            fromAct.setBalance(fromAct.getBalance() - money);
            toAct.setBalance(toAct.getBalance() + money);
            //4.更新数据库中的数据
            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("账户转账异常!!!");
        }

    }

}

AccountDao.java

package bank.mvc;

import bank.utils.DBUtil;
import java.sql.*;
import java.util.ArrayList;
import java.util.List;

/**
 * @version 1.0
 * @since 1.0
 * discription:负责Account数据的增删改查
 */
public class AccountDao {
    /**
     * 插入账户信息
     * @param act
     * @return 1表示插入成功
     */
    public int insert(Account act){
        PreparedStatement preparedStatement = null;
        int count = 0;
        try {
             String sql = "insert into t_act(actno,balance) values(?,?)";
              preparedStatement = DBUtil.getConnection().prepareStatement(sql);
              preparedStatement.setString(1,act.getActno());
              preparedStatement.setDouble(2,act.getBalance());
              count = preparedStatement.executeUpdate();
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }finally {
            DBUtil.close(null,preparedStatement,null);
        }
        return count;
    }

    /**
     * 根据id(主键)删除账户信息
     * @param id
     * @return
     */
    public int deleteById(Long id){
        PreparedStatement preparedStatement = null;
        int count = 0;
        try {
            String sql = "delete from t_act where id = ?";
            preparedStatement = DBUtil.getConnection().prepareStatement(sql);
            preparedStatement.setLong(1,id);
            count = preparedStatement.executeUpdate();

        } catch (SQLException e) {
            throw new RuntimeException(e);
        }finally {
            DBUtil.close(null,preparedStatement,null);
        }
        return count;
    }

    /**
     * 更新账户信息
     * @param act
     * @return
     */
    public int update(Account act){//负责更新
        PreparedStatement preparedStatement = null;
        int count = 0;
        try {
            String sql = "update t_act set actno = ? ,balance = ? where id = ?";
            preparedStatement = DBUtil.getConnection().prepareStatement(sql);
            preparedStatement.setString(1,act.getActno());
            preparedStatement.setDouble(2,act.getBalance());
            preparedStatement.setLong(3,act.getId());
            count = preparedStatement.executeUpdate();
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }finally {
            DBUtil.close(null,preparedStatement,null);
        }
        return count;
    }

    /**
     * 根据actno(账号)查询账户信息
     * @param actno
     * @return Account
     */
    public Account selectByActno(String actno){
        PreparedStatement preparedStatement = null;
        ResultSet resultSet = null;
        Account act = null;
        try {
            String sql = "select id,balance from t_act where actno = ?";
            preparedStatement = DBUtil.getConnection().prepareStatement(sql);
            preparedStatement.setString(1,actno);
            resultSet = preparedStatement.executeQuery();
            if(resultSet.next()){
                long id = resultSet.getLong("id");
                Double balance = resultSet.getDouble("balance");
                //将结果集封装为一个对象。
                act = new Account();
                act.setId(id);
                act.setActno(actno);
                act.setBalance(balance);
            }
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }finally {
            DBUtil.close(null,preparedStatement,resultSet);
        }
        return act;
    }

    /**
     * 查询所有账户信息
     * @return List
     */
    public List<Account> selectAll(){
        PreparedStatement preparedStatement = null;
        ResultSet resultSet = null;
        List<Account> list = new ArrayList<>();
        try {
            String sql = "select id,actno,balance from t_act ";
            preparedStatement = DBUtil.getConnection().prepareStatement(sql);
            resultSet = preparedStatement.executeQuery();
            while(resultSet.next()){
                //取出数据
                long id = resultSet.getLong("id");
                String actno = resultSet.getString("actno");
                Double balance = resultSet.getDouble("balance");
                //封装对象
                Account act = new Account(id,actno,balance);
                //加到List集合
                list.add(act);
            }
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }finally {
            DBUtil.close(null,preparedStatement,resultSet);
        }
        return list;
    }
}

老杜-银行账户转账(mvc005+last-包的结构+接口)

在实际开发中,合理的包结构可以提高代码的可维护性和可读性,并且便于团队协作开发。

三层架构层与层之间交互是通过接口的。降低耦合度,提高扩展力。

项目结构:

具体代码:

AccountDao.java

package bank.dao;

import bank.pojo.Account;

import java.util.List;

public interface AccountDao {
    int insert(Account act);
    int deleteById(Long id);
    int update(Account act);
    Account selectByActno(String actno);
    List<Account> selectAll();
}

AccountDaoImpl.java

package bank.dao.impl;

import bank.dao.AccountDao;
import bank.pojo.Account;
import bank.utils.DBUtil;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;

public class AccountDaoImpl implements AccountDao {
    /**
     * 插入账户信息
     * @param act
     * @return 1表示插入成功
     */
    public int insert(Account act){
        PreparedStatement preparedStatement = null;
        int count = 0;
        try {
            String sql = "insert into t_act(actno,balance) values(?,?)";
            preparedStatement = DBUtil.getConnection().prepareStatement(sql);
            preparedStatement.setString(1,act.getActno());
            preparedStatement.setDouble(2,act.getBalance());
            count = preparedStatement.executeUpdate();
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }finally {
            DBUtil.close(null,preparedStatement,null);
        }
        return count;
    }

    /**
     * 根据id(主键)删除账户信息
     * @param id
     * @return
     */
    public int deleteById(Long id){
        PreparedStatement preparedStatement = null;
        int count = 0;
        try {
            String sql = "delete from t_act where id = ?";
            preparedStatement = DBUtil.getConnection().prepareStatement(sql);
            preparedStatement.setLong(1,id);
            count = preparedStatement.executeUpdate();

        } catch (SQLException e) {
            throw new RuntimeException(e);
        }finally {
            DBUtil.close(null,preparedStatement,null);
        }
        return count;
    }

    /**
     * 更新账户信息
     * @param act
     * @return
     */
    public int update(Account act){//负责更新
        PreparedStatement preparedStatement = null;
        int count = 0;
        try {
            String sql = "update t_act set actno = ? ,balance = ? where id = ?";
            preparedStatement = DBUtil.getConnection().prepareStatement(sql);
            preparedStatement.setString(1,act.getActno());
            preparedStatement.setDouble(2,act.getBalance());
            preparedStatement.setLong(3,act.getId());
            count = preparedStatement.executeUpdate();
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }finally {
            DBUtil.close(null,preparedStatement,null);
        }
        return count;
    }

    /**
     * 根据actno(账号)查询账户信息
     * @param actno
     * @return Account
     */
    public Account selectByActno(String actno){
        PreparedStatement preparedStatement = null;
        ResultSet resultSet = null;
        Account act = null;
        try {
            String sql = "select id,balance from t_act where actno = ?";
            preparedStatement = DBUtil.getConnection().prepareStatement(sql);
            preparedStatement.setString(1,actno);
            resultSet = preparedStatement.executeQuery();
            if(resultSet.next()){
                long id = resultSet.getLong("id");
                Double balance = resultSet.getDouble("balance");
                //将结果集封装为一个对象。
                act = new Account();
                act.setId(id);
                act.setActno(actno);
                act.setBalance(balance);
            }
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }finally {
            DBUtil.close(null,preparedStatement,resultSet);
        }
        return act;
    }

    /**
     * 查询所有账户信息
     * @return List
     */
    public List<Account> selectAll(){
        PreparedStatement preparedStatement = null;
        ResultSet resultSet = null;
        List<Account> list = new ArrayList<>();
        try {
            String sql = "select id,actno,balance from t_act ";
            preparedStatement = DBUtil.getConnection().prepareStatement(sql);
            resultSet = preparedStatement.executeQuery();
            while(resultSet.next()){
                //取出数据
                long id = resultSet.getLong("id");
                String actno = resultSet.getString("actno");
                Double balance = resultSet.getDouble("balance");
                //封装对象
                Account act = new Account(id,actno,balance);
                //加到List集合
                list.add(act);
            }
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }finally {
            DBUtil.close(null,preparedStatement,resultSet);
        }
        return list;
    }
}

AccountService.java

package bank.service;

import bank.exceptions.AppException;
import bank.exceptions.MonerNotEnoughException;

public interface AccountService {
    void transfer(String fromActno, String toActno, double money)
            throws MonerNotEnoughException, AppException;
}

AccountServiceImpl.java

package bank.service.impl;

import bank.dao.AccountDao;
import bank.dao.impl.AccountDaoImpl;
import bank.exceptions.AppException;
import bank.exceptions.MonerNotEnoughException;
import bank.pojo.Account;
import bank.service.AccountService;
import bank.utils.DBUtil;

import java.sql.Connection;
import java.sql.SQLException;

public class AccountServiceImpl implements AccountService {
    private AccountDao accountDao = new AccountDaoImpl();//多态
    //这里的方法起名要体现业务。事务也应该在这进行控制,一般一个业务对应一个事务。
    public void transfer(String fromActno, String toActno, double money)
            throws MonerNotEnoughException, AppException {
        //开启事务(需要获取Connection对象,这个操作很不正常,因为需要在业务层写JDBC的代码。但先这样写。)
        try (Connection  connection = DBUtil.getConnection()){
            //禁止自动提交
            connection.setAutoCommit(false);

            //1.根据账号,查询余额是否充足。
            Account fromAct = accountDao.selectByActno(fromActno);
            if(fromAct.getBalance()<money){
                throw  new MonerNotEnoughException("抱歉,余额不足。");
            }
            //2.程序到这一步,说明余额充足。
            Account toAct = accountDao.selectByActno(toActno);

            //3.修改余额(只修改内存中的java对象的余额)
            fromAct.setBalance(fromAct.getBalance() - money);
            toAct.setBalance(toAct.getBalance() + money);
            //4.更新数据库中的数据
            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("账户转账异常!!!");
        }
    }
}

AccountServlet.java

package bank.web;

import bank.exceptions.AppException;
import bank.exceptions.MonerNotEnoughException;
import bank.service.AccountService;
import bank.service.impl.AccountServiceImpl;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import java.io.IOException;

@WebServlet("/transfer")
public class AccountServlet extends HttpServlet {
    AccountService accountService = new AccountServiceImpl();
    //之前在这个方法内做了很多的事情,现在思考简化一下。
    //让它充当一个司令官(Controller),他负责调度其他组件(Model,View)来完成任务。
    @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"));
        //调用业务方法处理业务
        try {
            accountService.transfer(fromActno,toActno,money);
            //程序运行到这里,代表转账成功。
            response.sendRedirect(request.getContextPath()+"/success.jsp");
        }catch (MonerNotEnoughException e){
            //执行到这里失败,代表余额不足。
            response.sendRedirect(request.getContextPath()+"/moneyNotEnough.jsp");
        }catch (AppException e) {
            //程序运行到这里,代表转账失败。
            response.sendRedirect(request.getContextPath()+"/fail.jsp");
        }
        //展示处理结果
    }
}

其余代码没有变化。

目前项目依然存在缺陷:

①:在service方法里面控制事务。(可以使用动态代理机制解决这个问题)

②:AccountService类中

private AccountDao accountDao = new AccountDaoImpl();

虽然我们使用了接口来衔接三层架构,但是在AccountService类中,依然使用了AccountDaoImpl这个类名。耦合度依然很高。

(Spring的Ioc容器解决这个问题)

知识点补充:

①:Double.parseDouble()

Double.parseDouble()方法是Java中的一个静态方法,用于将字符串转换为double类型的数值。它接受一个表示数值的字符串作为参数,并返回对应的double值。

使用Double.parseDouble()方法的基本语法如下:

double result = Double.parseDouble(str);

其中,str是要转换的字符串,result是转换后的double数值。

使用该方法时需要注意以下几点:

  • 字符串必须表示合法的数字,否则将抛出NumberFormatException异常。
  • 允许字符串中包含前导空格和尾随空格。
  • 允许字符串中使用正负号('+'或'-')表示正负数。
  • 允许字符串中使用科学计数法表示数值,例如"1.23e+10"。
  • 如果字符串为空或为null,将抛出NullPointerException异常。

示例:

String str = "3.14";
double result = Double.parseDouble(str);
System.out.println(result); // 输出: 3.14

如果要将其他类型的字符串转换为double类型,可以先将其转换为合法的数值格式,然后再使用Double.parseDouble()进行转换。

②:pattern

要给<input type="text" name="money">文本框添加限制条件,使其只能输入数字,可以使用HTML5中的pattern属性结合正则表达式来实现。以下是修改后的代码:

<div class="form-group">
  <label>转账金额:</label>
  <input type="text" name="money" pattern="[0-9]+" title="请输入数字">
</div>

在上述代码中,我添加了pattern="[0-9]+"属性到<input>标签中,表示只允许输入数字。[0-9]+是一个正则表达式,表示1个或多个数字。

另外,我还添加了title="请输入数字"属性,用于显示在鼠标悬停时的提示信息,提醒用户只能输入数字。

通过以上修改,当用户在转账金额文本框中输入非数字字符时,将无法提交表单,并且会显示提示信息。

请注意,虽然在前端通过正则表达式限制用户输入的内容,但为了确保数据的安全性和正确性,后端服务器也应该对接收到的数据进行验证和处理。

③:DAO

DAO(Data Access Object)是一种设计模式(JavaEE设计模式),用于在应用程序和数据库之间提供数据访问的接口。在Java Web开发中,DAO层通常用于处理与数据库交互的操作,没有任何的业务逻辑。

一般一张表对应一个dao。

④:POJO/JavaBean/domain/领域模型对象(叫法不同,代表一个东西)

POJO(Plain Old Java Object)指的是一个普通的Java对象,它不依赖于任何特定的框架或库,并且遵循JavaBean的标准。POJO对象通常用于封装数据,无业务逻辑,并且易于编写和理解。

一个典型的POJO对象包含以下特征:

  1. 私有字段:使用private关键字声明的成员变量。
  2. 公共访问方法:每个字段都有对应的getter和setter方法。
  3. 无参构造方法:用于创建对象的默认构造方法。
  4. 有参构造方法:用于初始化对象并设置字段值的构造方法。
  5. 可选的重写方法:例如toString、equals和hashCode等。

下面是一个示例POJO对象的代码:

public class Person {
    private String name;
    private int age;
    
    public Person() {
        // 无参构造方法
    }
    
    public Person(String name, int age) {
        // 有参构造方法
        this.name = name;
        this.age = age;
    }
    
    // getter和setter方法
    public String getName() {
        return name;
    }
    
    public void setName(String name) {
        this.name = name;
    }
    
    public int getAge() {
        return age;
    }
    
    public void setAge(int age) {
        this.age = age;
    }
    
    // 可选重写方法
    @Override
    public String toString() {
        return "Person{name='" + name + "', age=" + age + "}";
    }
}

上述代码定义了一个简单的Person类作为POJO对象,包含了name和age两个私有字段,以及对应的getter和setter方法。这个类没有任何与框架或库相关的依赖,可以直接在Java应用程序中使用。

⑤:SSM(下一步要学习的)

SSM 是指 Spring + Spring MVC + MyBatis,是一种常用的 Java Web 应用开发框架。

Spring 是一个轻量级的 Java 开发框架,提供了依赖注入(DI)和面向切面编程(AOP)等功能,简化了企业级应用的开发。

Spring MVC 是 Spring 框架的一部分,是一个基于 MVC(Model-View-Controller)设计模式的 Web 开发框架,用于构建灵活、高性能的 Web 应用程序。

MyBatis 是一个优秀的持久层框架,它通过 XML 或注解的方式将数据库操作和 Java 对象映射起来,方便快捷地实现数据持久化操作。

在 SSM 中,Spring 负责整个应用的框架搭建和管理,提供了一系列的容器和扩展点;Spring MVC 主要负责处理客户端请求和返回响应,完成 Web 层面的处理;MyBatis 则负责与数据库的交互,执行 SQL 语句,将查询结果映射为 Java 对象。

SSM 框架结合了 Spring 的依赖注入和面向切面编程的特性,使得应用开发更加简化和灵活;同时,采用了 MVC 设计模式,提供了良好的分层架构,促进了项目的结构清晰和代码的可维护性;MyBatis 则通过简单易用的方式,将数据库操作与 Java 对象的映射完成,提高了开发效率。