【通俗易懂】一篇文章教会你MVC架构模式

MVC架构模式(以银行账户转账为例)

本文使用Java最原始的代码演示,以银行账户转账为例详细介绍了MVC架构模式

介绍前的准备

创建数据库

drop table if exists t_act;
create table t_act(
    actno varchar(32) primary key,
    balance numeric
);

insert into t_act (actno,balance) values ('act-001',50000);
insert into t_act (actno,balance) values ('act-002',10000);

使用JSP进行页面展示

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%
	String basepath = request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort() + request.getContextPath() + "/";
%>
<html>
  <head>
    <base href="<%=basepath%>">
    <title>账户转账</title>
  </head>
  <body>
  <form action="servlet/transfer" method="post">
    转出账户:<input type="text" name="fromActno" value="act-001"><br>
    转入账户:<input type="text" name="toActno" value="act-002"><br>
    转账金额:<input type="text" name="money" value="10000"><br>
    <input type="submit" value="转账">
  </form>
  </body>
</html>

web.xml的配置

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
         version="4.0">

    <welcome-file-list>
        <welcome-file>index.jsp</welcome-file>
    </welcome-file-list>
    <servlet>
        <servlet-name>transfer</servlet-name>
        <servlet-class>servlet.TransferServlet</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>transfer</servlet-name>
        <url-pattern>/servlet/transfer</url-pattern>
    </servlet-mapping>

</web-app>

创建DBUtil工具类

package utils;

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

/**
 * 数据库工具类,便于JDBC的代码编写。
 */
public class DBUtil {

    private DBUtil() {}

    //类加载时绑定属性资源文件(必须放上面,先执行)
    private static ResourceBundle bundle = ResourceBundle.getBundle("resource/jdbc");

    //注册驱动
    static {
        try {
            Class.forName(bundle.getString("driver"));
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }

    /**
     * 获取数据库连接对象
     * @return 新的连接对象
     * @throws SQLException
     */
    public static Connection getConnection() throws SQLException {
        String url = bundle.getString("url");
        String user = bundle.getString("user");
        String passward = bundle.getString("password");
        Connection conn = DriverManager.getConnection(url,user,passward);
        return conn;
    }

    /**
     * 释放资源
     * @param conn 连接对象
     * @param stmt 数据库操作对象
     * @param rs 查询结果集
     */
    public static void close(Connection conn, Statement stmt, ResultSet rs){
        if (rs != null) {
            try {
                rs.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
        if (stmt != null) {
            try {
                stmt.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
        if (conn != null) {
            try {
                conn.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 开启事务
     * @param conn
     * @throws SQLException
     */
    public static void startTransaction(Connection conn) throws SQLException {
        if (conn != null){
            conn.setAutoCommit(false);
        }
    }

    /**
     * 提交事务
     * @param conn
     * @throws SQLException
     */
    public static void commitTransaction(Connection conn) throws SQLException {
        if (conn != null){
            conn.commit();
        }
    }

    /**
     * 回滚事务
     * @param conn
     */
    public static void rollbackTransaction(Connection conn) {
        if (conn != null){
            try {
                conn.rollback();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 结束事务
     * @param conn
     */
    public static void endTransaction(Connection conn) {
        if (conn != null){
            try {
                conn.setAutoCommit(true);
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
    }
}

设置配置文件(resource/jdbc.properties)

driver=com.mysql.jdbc.Driver
url=jdbc:mysql://localhost:3306/【你的数据库】
user=【你的用户名】
password=【你的密码】

不使用MVC架构模式的版本

package servlet;

import utils.DBUtil;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

public class TransferServlet extends HttpServlet {
    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"));

        //连接数据库
        Connection conn = null;
        PreparedStatement ps = null;
        ResultSet rs = null;
        int transferResult = 0;
        try {
            conn = DBUtil.getConnection();

            //开启事务
            DBUtil.startTransaction(conn);

            //核心业务
            //查询转出账户的余额
            String sql = "select balance from t_act where actno = ?";
            ps = conn.prepareStatement(sql);
            ps.setString(1, fromActno);
            rs = ps.executeQuery();
            if (rs.next()) {
                //读取转出账户在转账之前的余额
                double fromActBeforeBalance = rs.getDouble("balance");

                //判断余额是否充足
                if (fromActBeforeBalance >= money) {
                    //将转出账户的修改(余额减去)
                    double fromActAfterBalance = fromActBeforeBalance - money;
                    String updateSql = "update t_act set balance = ? where actno = ?";
                    ps = conn.prepareStatement(updateSql);
                    ps.setDouble(1, fromActAfterBalance);
                    ps.setString(2, fromActno);
                    transferResult += ps.executeUpdate();

                    //将转入账户的余额修改(余额加上)
                    ps = conn.prepareStatement(sql);
                    ps.setString(1, toActno);
                    rs = ps.executeQuery();
                    if (rs.next()) {
                        //转入账户在转账之前的余额
                        double toActBeforeBalance = rs.getDouble("balance");
                        //转入账户在转账之后的余额
                        double toActAfterBalance = toActBeforeBalance + money;
                        ps = conn.prepareStatement(updateSql);
                        ps.setDouble(1, toActAfterBalance);
                        ps.setString(2, toActno);
                        transferResult += ps.executeUpdate();
                    }
                } else {
                    //余额不足,转账失败
                    transferResult = -1;
                }
            }

            //提交事务
            DBUtil.commitTransaction(conn);

        } catch (SQLException e) {
            //回滚事务
            DBUtil.rollbackTransaction(conn);
            e.printStackTrace();
        } finally {
            //结束事务
            DBUtil.endTransaction(conn);
            DBUtil.close(conn, ps, rs);
        }

        response.setContentType("text/html;charset=UTF-8");
        PrintWriter out = response.getWriter();
        out.print("<html>");
        out.print("    <head>");
        out.print("      <title>转账结果</title>");
        out.print("    </head>");
        out.print("    <body>");
        if (transferResult == -1) {
            //转账失败,由于余额不足
            out.print("<h1 align='center'><font color='red'>余额不足,转账失败</font></h1>");
        } else if (transferResult == 2) {
            //转账成功
            out.print("<h1 align='center'><font color='green'>转账成功</font></h1>");
        } else {
            //转账失败,未知原因
            out.print("<h1 align='center'><font color='red'>转账失败,未知原因</font></h1>");
        }
        out.print("    </body>");
        out.print("</html>");

    }
}
  • 分析以上Servlet负责了哪些事情?
    • 数据的输入(数据的接收)
    • 连接数据库,操作数据库中的数据(JDBC)
    • 业务逻辑的处理
    • 数据的输出(页面的展示)
  • 分析这样编程存在哪些缺点(不使用MVC架构模式)
    • 违背了“高内聚,低耦合”的原则,组件的独立性差,导致复用性差,开发效率很低,耦合度高,扩展力差
    • 整个web系统没有明确的分工(web系统的职能分工没有明确)

使用MVC架构模式的第一个版本

  • 使用MVC架构模式开发应用,达到web系统的职能分工,降低耦合,提高扩展力,提高组件独立性和提高组件可重用性
    • 有专门的类完成数据的输入(Servlet)
    • 有专门的类处理业务逻辑(Service)
    • 有专门的类处理CRUD(Create,Retrieve,Update,Delete)(Dao:Date Access Object)
    • 有专门的类完成数据的输出(Jsp)
  • Servlet类当作总司令:(指挥官,核心控制器 Controller(控制器))
    • 调度Model(模型)完成业务逻辑的处理(调度Service处理业务,但是Service在执行过程中必须处理数据,service也有秘书,叫做Dao)
    • 调度View(视图)完成页面的展示(调度Jsp类完成页面的展示,除了Jsp外还有其他的页面展示技术)
  • 分层方式可以分为:横向层次和纵向层次。
    • 横向层次:MVC
    • 纵向层次:
      • Servlet (表示层 Struts1 Struts2)
      • Service (业务层 Spring)
      • Dao (持久层 Mybatis Hibernate)

bean(创建Account实体类)

package bean;

public class Account {
    private String actno;
    private Double balance;

    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;
    }
}

Servlet

package servlet;

import service.AccountService;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public class TransferServlet extends HttpServlet {//Servlet是单例的

    private AccountService actService = new AccountService(); //成员变量也是单例的

    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        //数据的输入
        String fromActno = request.getParameter("fromActno");
        String toActno = request.getParameter("toActno");
        double money = Double.parseDouble(request.getParameter("money"));

        //调用Model完成业务处理
        int transferResult = actService.transfer(fromActno,toActno,money);

        //调用View完成页面展示
        if (transferResult == -1){
            response.sendRedirect(request.getContextPath() + "/transfer_error_1.jsp");
        }else if (transferResult == 2){
            response.sendRedirect(request.getContextPath() + "/transfer_success.jsp");
        }else {
            response.sendRedirect(request.getContextPath() + "/transfer_error_2.jsp");
        }
    }
}

Service

  • Model层:业务层(业务模型)
    • 在业务层中只能纯业务逻辑,不能编写JDBC代码
    • 在service层中编写的方法名一般和业务逻辑有关
package service;

import bean.Account;
import dao.AccountDao;

public class AccountService {

    //虽然是在多线程环境下运行的单实例,但是这些对象的内存不会被修改,所以没有线程安全问题
    private AccountDao actDao = new AccountDao(); //成员变量也是单例的

    /**
     * 转账方法
     * @param fromActno
     * @param toActno
     * @param money
     * @return
     */
    public int transfer(String fromActno,String toActno,double money){
//        AccountDao actDao = new AccountDao();

        //根据账号查询转出账户信息
        Account fromAct = actDao.selectByActno(fromActno);

        //根据账号查询转入账户信息
        Account toAct = actDao.selectByActno(toActno);

        //判断转出账户余额是否充足
        if (fromAct.getBalance() < money){
            return -1;
        }

        //余额充足,开始转账
        //下面两行代码表示将JVM中的java对象的balance属性修改,但是数据库中的数据并未修改
        fromAct.setBalance(fromAct.getBalance() - money);
        toAct.setBalance(toAct.getBalance() + money);

        //更新数据库中的数据
        int count = actDao.update(fromAct);
        count += actDao.update(toAct);

        return count;
    }
}

DAO

  • DAO(Date Access Object,数据访问对象,和业务逻辑没有任何关系,不能有业务代码,只完成CRUD操作)
  • dao层的方法名一般都是:insertXXX/deleteXXX/updateXXX/selectXXX
package dao;

import bean.Account;
import utils.DBUtil;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

public class AccountDao {
    /**
     * 通过账号查询账户信息
     * @param actno 账号
     * @return 账户信息
     */
    public Account selectByActno(String actno){
        Connection conn = null;
        PreparedStatement ps = null;
        ResultSet rs = null;
        Account act = null;
        try {
            conn = DBUtil.getConnection();
            String sql = "select balance from t_act where actno=?";
            ps = conn.prepareStatement(sql);
            ps.setString(1,actno);
            rs = ps.executeQuery();
            if (rs.next()){
                act = new Account();
                act.setActno(actno);
                act.setBalance(rs.getDouble("balance"));
            }
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            DBUtil.close(conn,ps,rs);
        }
        return act;
    }

    /**
     * 更新账户信息
     * @param newAct
     * @return
     */
    public int update(Account newAct){
        Connection conn = null;
        PreparedStatement ps = null;
        int count = 0;
        try {
            conn = DBUtil.getConnection();
            String sql = "update t_act set balance=? where actno=?";
            ps = conn.prepareStatement(sql);
            ps.setDouble(1,newAct.getBalance());
            ps.setString(2,newAct.getActno());
            count = ps.executeUpdate();
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            DBUtil.close(conn,ps,null);
        }
        return count;
    }
}

View

transfer_error_1.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>转账结果</title>
</head>
<body>
<h1 align="center"><font color="red">转账失败,余额不足</font></h1>
</body>
</html>
transfer_error_2.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>转账结果</title>
</head>
<body>
<h1 align="center"><font color="red">转账失败,未知原因</font></h1>
</body>
</html>
transfer_success.jsp
<html>
<head>
    <title>转账结果</title>
</head>
<body>
<h1 align="center"><font color="green">转账成功</font></h1>
</body>
</html>

使用MVC架构模式的第二个版本

  • 版本一中耦合度还是太高,开发中应做到面向接口编程
  • Servlet --(Service Interface)–> Service --(Dao Interface)-> Dao
  • 作以改进(注意包名

Servlet

package servlet;

import service.AccountService;
import service.impl.AccountServiceImpl;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public class TransferServlet extends HttpServlet {

    //下面的程序已经面向接口调用方法了,但还是没有完全解耦合(直接new对象了)
    private AccountService actService = new AccountServiceImpl();

    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        //数据的输入
        String fromActno = request.getParameter("fromActno");
        String toActno = request.getParameter("toActno");
        double money = Double.parseDouble(request.getParameter("money"));

        //调用Model完成业务处理
        int transferResult = actService.transfer(fromActno,toActno,money);

        //调用View完成页面展示
        if (transferResult == -1){
            response.sendRedirect(request.getContextPath() + "/transfer_error_1.jsp");
        }else if (transferResult == 2){
            response.sendRedirect(request.getContextPath() + "/transfer_success.jsp");
        }else {
            response.sendRedirect(request.getContextPath() + "/transfer_error_2.jsp");
        }
    }
}

Service

接口
package service;

public interface AccountService {
    /**
     *
     * @param fromActno
     * @param toActno
     * @param money
     * @return
     */
    int transfer(String fromActno,String toActno,double money);
}

实现类
package service.impl;

import bean.Account;
import dao.AccountDao;
import dao.impl.AccountDaoImpl;
import service.AccountService;

public class AccountServiceImpl implements AccountService {

    //下面的程序已经面向接口调用方法了,但还是没有完全解耦合(直接new对象了)
    private AccountDao actDao = new AccountDaoImpl();

    @Override
    public int transfer(String fromActno, String toActno, double money) {
        //根据账号查询转出账户信息
        Account fromAct = actDao.selectByActno(fromActno);

        //根据账号查询转入账户信息
        Account toAct = actDao.selectByActno(toActno);

        //判断转出账户余额是否充足
        if (fromAct.getBalance() < money){
            return -1;
        }

        //余额充足,开始转账
        //下面两行代码表示将JVM中的java对象的balance属性修改,但是数据库中的数据并未修改
        fromAct.setBalance(fromAct.getBalance() - money);
        toAct.setBalance(toAct.getBalance() + money);

        //更新数据库中的数据
        int count = actDao.update(fromAct);
        count += actDao.update(toAct);

        return count;
    }
}

DAO

接口
package dao;

import bean.Account;

public interface AccountDao {
    /**
     *
     * @param actno
     * @return
     */
    Account selectByActno(String actno);

    /**
     *
     * @param newAct
     * @return
     */
    int update(Account newAct);
}

实现类
package dao.impl;

import bean.Account;
import dao.AccountDao;
import utils.DBUtil;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

public class AccountDaoImpl implements AccountDao {
    @Override
    public Account selectByActno(String actno) {
        Connection conn = null;
        PreparedStatement ps = null;
        ResultSet rs = null;
        Account act = null;
        try {
            conn = DBUtil.getConnection();
            String sql = "select balance from t_act where actno=?";
            ps = conn.prepareStatement(sql);
            ps.setString(1,actno);
            rs = ps.executeQuery();
            if (rs.next()){
                act = new Account();
                act.setActno(actno);
                act.setBalance(rs.getDouble("balance"));
            }
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            DBUtil.close(conn,ps,rs);
        }
        return act;
    }

    @Override
    public int update(Account newAct) {
        Connection conn = null;
        PreparedStatement ps = null;
        int count = 0;
        try {
            conn = DBUtil.getConnection();
            String sql = "update t_act set balance=? where actno=?";
            ps = conn.prepareStatement(sql);
            ps.setDouble(1,newAct.getBalance());
            ps.setString(2,newAct.getActno());
            count = ps.executeUpdate();
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            DBUtil.close(conn,ps,null);
        }
        return count;
    }
}

对于事务的控制

以上两个版本均未加入对于事务的控制,现作以改进

分析:事务应该在哪个类中控制?

  • 控制事务不能在dao层控制,因为事务和业务紧密相关的,在dao中没有业务逻辑。通常是N条DML语句联合起来才能完成一个完整的事务。
  • service层与业务紧密相关,当我们一个业务完全完成之后再提交数据,所以控制事务应该放再service层中,一般一个方法对应一个完整的事务。方法开始执行开启事务,方法执行结束结束事务。业务完全成功则提交,业务中间发生异常则回滚事务
  • 也可以在Servlet中控制事务,因为Servlet比service还要高一层。但是这样事务的范围太大了,没有这个必要。(最好的控制事务放在service当中

在service中加入对事务的控制

尝试在service中加入对事务的控制
package service.impl;

import bean.Account;
import dao.AccountDao;
import dao.impl.AccountDaoImpl;
import service.AccountService;
import utils.DBUtil;

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

public class AccountServiceImpl implements AccountService {

    private AccountDao actDao = new AccountDaoImpl();

    @Override
    public int transfer(String fromActno, String toActno, double money) {

        Connection conn = null;
        int count = 0;
        try {
            conn = DBUtil.getConnection();

            //开启事务
            DBUtil.startTransaction(conn);

            //根据账号查询转出账户信息
            Account fromAct = actDao.selectByActno(fromActno);

            //根据账号查询转入账户信息
            Account toAct = actDao.selectByActno(toActno);

            //判断转出账户余额是否充足
            if (fromAct.getBalance() < money){
                return -1;
            }

            //余额充足,开始转账
            //下面两行代码表示将JVM中的java对象的balance属性修改,但是数据库中的数据并未修改
            fromAct.setBalance(fromAct.getBalance() - money);
            toAct.setBalance(toAct.getBalance() + money);

            //更新数据库中的数据
            count = actDao.update(fromAct);
            count += actDao.update(toAct);

            //提交事务
            DBUtil.commitTransaction(conn);
        } catch (SQLException e) {
            e.printStackTrace();

            //回滚事务
            DBUtil.rollbackTransaction(conn);
        }finally {
            //结束事务
            DBUtil.endTransaction(conn);
        }



        return count;
    }
}

关闭Connection
  • 必须在service方法结束的时候,事务结束的时候再将连接对象关闭,不能在dao方法中关闭连接对象
  • 于是修改dao的实现类与service的实现类,得到以下代码:
package dao.impl;

import bean.Account;
import dao.AccountDao;
import utils.DBUtil;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

public class AccountDaoImpl implements AccountDao {
    @Override
    public Account selectByActno(String actno) {
        Connection conn = null;
        PreparedStatement ps = null;
        ResultSet rs = null;
        Account act = null;
        try {
            conn = DBUtil.getConnection();
            String sql = "select balance from t_act where actno=?";
            ps = conn.prepareStatement(sql);
            ps.setString(1,actno);
            rs = ps.executeQuery();
            if (rs.next()){
                act = new Account();
                act.setActno(actno);
                act.setBalance(rs.getDouble("balance"));
            }
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            DBUtil.close(null,ps,rs);
        }
        return act;
    }

    @Override
    public int update(Account newAct) {
        Connection conn = null;
        PreparedStatement ps = null;
        int count = 0;
        try {
            conn = DBUtil.getConnection();
            String sql = "update t_act set balance=? where actno=?";
            ps = conn.prepareStatement(sql);
            ps.setDouble(1,newAct.getBalance());
            ps.setString(2,newAct.getActno());
            count = ps.executeUpdate();
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            DBUtil.close(null,ps,null);
        }
        return count;
    }
}

package service.impl;

import bean.Account;
import dao.AccountDao;
import dao.impl.AccountDaoImpl;
import service.AccountService;
import utils.DBUtil;

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

public class AccountServiceImpl implements AccountService {

    //下面的程序已经面向接口调用方法了,但还是没有完全解耦合(直接new对象了)
    private AccountDao actDao = new AccountDaoImpl();

    @Override
    public int transfer(String fromActno, String toActno, double money) {

        Connection conn = null;
        int count = 0;
        try {
            conn = DBUtil.getConnection();

            //开启事务
            DBUtil.startTransaction(conn);

            //根据账号查询转出账户信息
            Account fromAct = actDao.selectByActno(fromActno);

            //根据账号查询转入账户信息
            Account toAct = actDao.selectByActno(toActno);

            //判断转出账户余额是否充足
            if (fromAct.getBalance() < money){
                return -1;
            }

            //余额充足,开始转账
            //下面两行代码表示将JVM中的java对象的balance属性修改,但是数据库中的数据并未修改
            fromAct.setBalance(fromAct.getBalance() - money);
            toAct.setBalance(toAct.getBalance() + money);

            //更新数据库中的数据
            count = actDao.update(fromAct);
            count += actDao.update(toAct);

            //提交事务
            DBUtil.commitTransaction(conn);
        } catch (SQLException e) {
            e.printStackTrace();

            //回滚事务
            DBUtil.rollbackTransaction(conn);
        }finally {
            //结束事务
            DBUtil.endTransaction(conn);
            
            //必须在整个业务流程完成之后,事务提交之后,才能关闭连接对象
            DBUtil.close(conn,null,null);
        }
        
        return count;
    }
}

方法传参
  • 以上代码看似加入了对事务的控制,实际上并不能起到作用,因为每执行一次dao中的方法,Connection都会创建一次,导致创建了四个Connection对象,而对于事务的控制时又创建了一个Connection,一共连接了五次数据库,创建了五个Connection对象,仅仅其中一个Connection有事务,其余四个都没有。
  • 因为在service层中控制事务,必须保证service方法执行的连接对象和dao方法中的连接对象是同一个
  • 为了解决以上问题,我们可以使用方法传参将Connection对象传入dao方法,使其Connection对象为同一个对象
  • 改造service实现类和dao接口及dao实现类,代码如下:
package service.impl;

import bean.Account;
import dao.AccountDao;
import dao.impl.AccountDaoImpl;
import service.AccountService;
import utils.DBUtil;

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

public class AccountServiceImpl implements AccountService {

    //下面的程序已经面向接口调用方法了,但还是没有完全解耦合(直接new对象了)
    private AccountDao actDao = new AccountDaoImpl();

    @Override
    public int transfer(String fromActno, String toActno, double money) {

        Connection conn = null;
        int count = 0;
        try {
            conn = DBUtil.getConnection();

            //开启事务
            DBUtil.startTransaction(conn);

            //根据账号查询转出账户信息
            Account fromAct = actDao.selectByActno(fromActno,conn);

            //根据账号查询转入账户信息
            Account toAct = actDao.selectByActno(toActno,conn);

            //判断转出账户余额是否充足
            if (fromAct.getBalance() < money){
                return -1;
            }

            //余额充足,开始转账
            //下面两行代码表示将JVM中的java对象的balance属性修改,但是数据库中的数据并未修改
            fromAct.setBalance(fromAct.getBalance() - money);
            toAct.setBalance(toAct.getBalance() + money);

            //更新数据库中的数据
            count = actDao.update(fromAct,conn);
            count += actDao.update(toAct,conn);

            //提交事务
            DBUtil.commitTransaction(conn);
        } catch (SQLException e) {
            e.printStackTrace();

            //回滚事务
            DBUtil.rollbackTransaction(conn);
        }finally {
            //结束事务
            DBUtil.endTransaction(conn);

            //必须在整个业务流程完成之后,事务提交之后,才能关闭连接对象
            DBUtil.close(conn,null,null);
        }

        return count;
    }
}

package dao;

import bean.Account;

import java.sql.Connection;

public interface AccountDao {
    /**
     *
     * @param actno
     * @return
     */
    Account selectByActno(String actno, Connection conn);

    /**
     *
     * @param newAct
     * @return
     */
    int update(Account newAct,Connection conn);
}

package dao.impl;

import bean.Account;
import dao.AccountDao;
import utils.DBUtil;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

public class AccountDaoImpl implements AccountDao {
    @Override
    public Account selectByActno(String actno,Connection conn) {
        PreparedStatement ps = null;
        ResultSet rs = null;
        Account act = null;
        try {
            String sql = "select balance from t_act where actno=?";
            ps = conn.prepareStatement(sql);
            ps.setString(1,actno);
            rs = ps.executeQuery();
            if (rs.next()){
                act = new Account();
                act.setActno(actno);
                act.setBalance(rs.getDouble("balance"));
            }
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            DBUtil.close(null,ps,rs);
        }
        return act;
    }

    @Override
    public int update(Account newAct,Connection conn) {
        PreparedStatement ps = null;
        int count = 0;
        try {
            String sql = "update t_act set balance=? where actno=?";
            ps = conn.prepareStatement(sql);
            ps.setDouble(1,newAct.getBalance());
            ps.setString(2,newAct.getActno());
            count = ps.executeUpdate();
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            DBUtil.close(null,ps,null);
        }
        return count;
    }
}

  • 目前项目中使用了方法在调用的时候传参的方式,保证service和dao中的连接对象是同一个并且保证了一个线程一个连接对象不能让多个线程共享同一个连接对象,这样事务就混乱了,所以不能使用单例模式)。但事务仍然无法起作用,因为service中需要使用catch捕获,捕获异常之后才能回滚,保证数据的安全,所以dao中的异常应该上抛给service。

  • 于是对dao的实现类进行改造:

package dao.impl;

import bean.Account;
import dao.AccountDao;
import utils.DBUtil;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

public class AccountDaoImpl implements AccountDao {
    @Override
    public Account selectByActno(String actno,Connection conn) {
        PreparedStatement ps = null;
        ResultSet rs = null;
        Account act = null;
        try {
            String sql = "select balance from t_act where actno=?";
            ps = conn.prepareStatement(sql);
            ps.setString(1,actno);
            rs = ps.executeQuery();
            if (rs.next()){
                act = new Account();
                act.setActno(actno);
                act.setBalance(rs.getDouble("balance"));
            }
        } catch (SQLException e) {
            e.printStackTrace();
            throw new RuntimeException("查询账户异常");
        } finally {
            DBUtil.close(null,ps,rs);
        }
        return act;
    }

    @Override
    public int update(Account newAct,Connection conn) {
        PreparedStatement ps = null;
        int count = 0;
        try {
            String sql = "update t_act set balance=? where actno=?";
            ps = conn.prepareStatement(sql);
            ps.setDouble(1,newAct.getBalance());
            ps.setString(2,newAct.getActno());
            count = ps.executeUpdate();
        } catch (SQLException e) {
            e.printStackTrace();
            //手动抛出异常
            throw new RuntimeException("更新账户异常");
        } finally {
            DBUtil.close(null,ps,null);
        }
        return count;
    }
}

  • 为了保证service方法的连接对象和dao中的连接对象是同一个,我们使用了方法传参。但这种方式会导致在dao层中的每一个方法上都有一个Connection参数,代码没有复用,最好将dao层每一个方法上的Connection参数隐藏起来并做到一个线程一个连接(开发原则)。那么我们要如何隐藏
  • 在介绍如何隐藏之前,我们先来复习一个知识点。
ThreadLocal
我们先用一个简单的程序模拟一下上面的代码
模拟的Tomcat
package tomcat;

import servlet.Servlet;

public class Tomcat {
    public static void main(String[] args) {
        //单例的对象(所有线程共享一个Servlet对象)
        Servlet servlet = new Servlet();

        //主线程调用servlet对象的doGet方法
        servlet.doGet();
    }
}

模拟的Servlet
package servlet;

import service.UserService;

public class Servlet {
    private UserService userService = new UserService();

    public void doGet(){
        System.out.println(Thread.currentThread().getName());
        userService.delete();
    }
}

模拟的Service
package service;

import dao.UserDao;

public class UserService {
    private UserDao userDao = new UserDao();
    public void delete(){
        System.out.println(Thread.currentThread().getName());
        userDao.delete();
    }
}

模拟的DAO
package dao;

public class UserDao {
    public void delete(){
        System.out.println(Thread.currentThread().getName());
    }
}

  • 先运行一下:

在这里插入图片描述

  • 得出结论:整个程序仅有一个线程
对以上程序的Tomcat进行改造,使其在多线程情况下运行
模拟的Tomcat
package tomcat;

import servlet.Servlet;

public class Tomcat {
    //主线程
    public static void main(String[] args) {
        //单例的对象(所有线程共享一个Servlet对象)
        Servlet servlet = new Servlet();

        //创建线程对象
        Thread t1 = new MyThread(servlet);
        //设置线程的名称
        t1.setName("t1");
        //启动线程(栈内存分配完毕之后,会调用run方法,run方法是在栈的底部)
        t1.start();

        //创建线程对象
        Thread t2 = new MyThread(servlet);
        //设置线程的名称
        t2.setName("t2");
        //启动线程(栈内存分配完毕之后,会调用run方法,run方法是在栈的底部)
        t2.start();
    }
}

//自定义线程
class MyThread extends Thread{

    private Servlet servlet;

    public MyThread(Servlet servlet) {
        this.servlet = servlet;
    }

    public void run(){
        servlet.doGet();
    }
}
  • 在t2线程创建时添加断点,先将t1线程创建并运行,然后放开断点,经过观察程序运行结果(t1 t1 t1 t2 t2 t2),得出结论:Servlet调用Service,Service调用DAO始终是同一个线程
  • 得出这个结论之后想必大家对于该怎么保证 将dao层每一个方法上的Connection参数隐藏起来并做到一个线程一个连接 有了初步的思路,但我们先不要着急去修改银行转账的代码,先将这里例子做完,来保证大家的理解更进一步。
  • 那么究竟要怎么保证呢?
    • 很简单,使用map集合将Connection与Thread绑定即可
【重点】创建ThreadLocal类
package util;

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

/**
 * 在同一个线程中传递一个java对象必须使用ThreadLocal
 * @param <T>
 */
public class ThreadLocal<T> {

    /**
     * Map集合key存储当前线程对象,value存储和当前线程对象绑定数据
     */
    private Map<Thread,T> threadLocalMap = new HashMap<>();

    /**
     * 向当前线程中绑定一个数据
     * @param value
     */
    public void set(T value){
        threadLocalMap.put(Thread.currentThread(),value);
    }

    /**
     * 从当前线程中读取绑定的数据
     * @return
     */
    public T get(){
        return threadLocalMap.get(Thread.currentThread());
    }
    
    /**
     * 解除绑定关系
     */
    public void remove(){
        threadLocalMap.remove(Thread.currentThread());
    }
}

加入Connection和DBUtil并修改Tomcat,Servlet,Service,Dao
模拟的Connection
package util;

public class Connection {
}

模拟的DBUtil
package util;

public class DBUtil {
    
    private static ThreadLocal<Connection> threadLocal = new ThreadLocal<>();
    public static Connection getConnection(){
        //Connection conn = new Connection();
        //return conn;
        
        Connection conn = threadLocal.get();
        if (conn == null){
            conn = new Connection();
            threadLocal.set(conn);
        }
        return conn;
    }
}

修改后的Tomcat
package tomcat;

import servlet.Servlet;

public class Tomcat {
    //主线程
    public static void main(String[] args) {
        //单例的对象(所有线程共享一个Servlet对象)
        Servlet servlet = new Servlet();

        //创建线程对象
        Thread t1 = new MyThread(servlet);
        //设置线程的名称
        t1.setName("t1");
        //启动线程(栈内存分配完毕之后,会调用run方法,run方法是在栈的底部)
        t1.start();

        /*
        //创建线程对象
        Thread t2 = new MyThread(servlet);
        //设置线程的名称
        t2.setName("t2");
        //启动线程(栈内存分配完毕之后,会调用run方法,run方法是在栈的底部)
        t2.start();
         */
    }
}

//自定义线程
class MyThread extends Thread{

    private Servlet servlet;

    public MyThread(Servlet servlet) {
        this.servlet = servlet;
    }

    public void run(){
        servlet.doGet();
    }
}
修改后的Servlet
package servlet;

import service.UserService;
import util.Connection;
import util.DBUtil;

public class Servlet {
    private UserService userService = new UserService();

    public void doGet(){
        //Thread t = Thread.currentThread();
        //System.out.println(t);
        Connection conn = DBUtil.getConnection();
        System.out.println(conn);
        userService.delete();
    }
}

修改后的Service
package service;

import dao.UserDao;
import util.Connection;
import util.DBUtil;

public class UserService {
    private UserDao userDao = new UserDao();
    public void delete(){
        //Thread t = Thread.currentThread();
        //System.out.println(t);
        Connection conn = DBUtil.getConnection();
        System.out.println(conn);
        userDao.delete();
    }
}

修改后的DAO
package dao;

import util.Connection;
import util.DBUtil;

public class UserDao {
    public void delete(){
        //Thread t = Thread.currentThread();
        //System.out.println(t);

        Connection conn = DBUtil.getConnection();
        System.out.println(conn);
    }
}

  • 运行程序,观察运行结果:

    在这里插入图片描述

  • 终于保证了一个线程一个连接

好消息,SUN公司已经为我们写好了一个ThreadLocal类
  • java.lang.ThreadLocal
使用SUN公司的ThreadLocal
  • 更改DBUtil即可
package util;

public class DBUtil {

    //private static ThreadLocal<Connection> threadLocal = new ThreadLocal<>();
    private static java.lang.ThreadLocal<Connection> threadLocal = new java.lang.ThreadLocal<>();
    
    public static Connection getConnection(){
        //Connection conn = new Connection();
        //return conn;

        Connection conn = threadLocal.get();
        if (conn == null){
            conn = new Connection();
            threadLocal.set(conn);
        }
        return conn;
    }
}

  • 运行程序,仍为相同的结果

现在,大家应该理解了怎么保证一个线程一个连接了吧,下面我们接着改造银行转账的例子吧!

改造后的银行转账代码(使用MVC架构模式的第三个版本)

DBUtil

package utils;

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

/**
 * 数据库工具类,便于JDBC的代码编写。
 */
public class DBUtil {

    private DBUtil() {}

    //类加载时绑定属性资源文件(必须放上面,先执行)
    private static ResourceBundle bundle = ResourceBundle.getBundle("resource/jdbc");

    private static ThreadLocal<Connection> threadLocal = new ThreadLocal<>();

    //注册驱动
    static {
        try {
            Class.forName(bundle.getString("driver"));
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }

    /**
     * 获取数据库连接对象
     * @return 新的连接对象
     * @throws SQLException
     */
    public static Connection getConnection() throws SQLException {
        Connection conn = threadLocal.get();

        if (conn == null) {
            String url = bundle.getString("url");
            String user = bundle.getString("user");
            String passward = bundle.getString("password");
            conn = DriverManager.getConnection(url, user, passward);
            threadLocal.set(conn);
        }
        //System.out.println(conn);

        return conn;
    }

    /**
     * 释放资源
     * @param conn 连接对象
     * @param stmt 数据库操作对象
     * @param rs 查询结果集
     */
    public static void close(Connection conn, Statement stmt, ResultSet rs){
        if (rs != null) {
            try {
                rs.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
        if (stmt != null) {
            try {
                stmt.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
        if (conn != null) {
            try {
                conn.close();

                //No operations allowed after connection closed
                //连接对象和当前线程对象解除绑定关系,防止第二次操作通过线程池拿到同一个线程,获取一个已经关闭的Connection
                threadLocal.remove();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 开启事务
     * @param conn
     * @throws SQLException
     */
    public static void startTransaction(Connection conn) throws SQLException {
        if (conn != null){
            conn.setAutoCommit(false);
        }
    }

    /**
     * 提交事务
     * @param conn
     * @throws SQLException
     */
    public static void commitTransaction(Connection conn) throws SQLException {
        if (conn != null){
            conn.commit();
        }
    }

    /**
     * 回滚事务
     * @param conn
     */
    public static void rollbackTransaction(Connection conn) {
        if (conn != null){
            try {
                conn.rollback();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 结束事务
     * @param conn
     */
    public static void endTransaction(Connection conn) {
        if (conn != null){
            try {
                conn.setAutoCommit(true);
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
    }
}

Servlet

package servlet;

import service.AccountService;
import service.impl.AccountServiceImpl;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * 也可以在Servlet中控制事务,因为Servlet比service还要高一层。
 * 但是这样事务的范围太大了,没有这个必要。(最好的控制事务放在service当中)
 */
public class TransferServlet extends HttpServlet {

    //下面的程序已经面向接口调用方法了,但还是没有完全解耦合(直接new对象了)
    private AccountService actService = new AccountServiceImpl();

    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        //数据的输入
        String fromActno = request.getParameter("fromActno");
        String toActno = request.getParameter("toActno");
        double money = Double.parseDouble(request.getParameter("money"));

        //调用Model完成业务处理
//        AccountService actService = new AccountService();
        int transferResult = actService.transfer(fromActno,toActno,money);

        //调用View完成页面展示
        if (transferResult == -1){
            response.sendRedirect(request.getContextPath() + "/transfer_error_1.jsp");
        }else if (transferResult == 2){
            response.sendRedirect(request.getContextPath() + "/transfer_success.jsp");
        }else {
            response.sendRedirect(request.getContextPath() + "/transfer_error_2.jsp");
        }
    }
}

Service

接口
package service;

public interface AccountService {
    /**
     *
     * @param fromActno
     * @param toActno
     * @param money
     * @return
     */
    int transfer(String fromActno, String toActno, double money);
}

实现类
package service.impl;

import bean.Account;
import dao.AccountDao;
import dao.impl.AccountDaoImpl;
import service.AccountService;
import utils.DBUtil;

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

/**
 * service层与业务紧密相关,当我们一个业务完全完成之后再提交数据,
 * 所以控制事务应该放再service层中,一般一个方法对应一个完整的事务。
 * 方法开始执行开启事务,方法执行结束结束事务。业务完全成功则提交,
 * 业务中间发生异常则回滚事务。
 */
public class AccountServiceImpl implements AccountService {

    //下面的程序已经面向接口调用方法了,但还是没有完全解耦合(直接new对象了)
    private AccountDao actDao = new AccountDaoImpl();

    @Override
    public int transfer(String fromActno, String toActno, double money) {

        Connection conn = null;
        int count = 0;
        try {
            conn = DBUtil.getConnection();

            //开启事务
            DBUtil.startTransaction(conn);

            //根据账号查询转出账户信息
            Account fromAct = actDao.selectByActno(fromActno);

            //根据账号查询转入账户信息
            Account toAct = actDao.selectByActno(toActno);

            //判断转出账户余额是否充足
            if (fromAct.getBalance() < money){
                return -1;
            }

            //余额充足,开始转账
            //下面两行代码表示将JVM中的java对象的balance属性修改,但是数据库中的数据并未修改
            fromAct.setBalance(fromAct.getBalance() - money);
            toAct.setBalance(toAct.getBalance() + money);

            //更新数据库中的数据
            count = actDao.update(fromAct);
            count += actDao.update(toAct);

            //提交事务
            DBUtil.commitTransaction(conn);
        } catch (SQLException e) {
            e.printStackTrace();

            //回滚事务
            DBUtil.rollbackTransaction(conn);
        }finally {
            //结束事务
            DBUtil.endTransaction(conn);

            //必须在整个业务流程完成之后,事务提交之后,才能关闭连接对象
            DBUtil.close(conn,null,null);
        }

        return count;
    }
}

DAO

接口
package dao;

import bean.Account;

import java.sql.Connection;

public interface AccountDao {
    /**
     *
     * @param actno
     * @return
     */
    Account selectByActno(String actno);

    /**
     *
     * @param newAct
     * @return
     */
    int update(Account newAct);
}

实现类
package dao.impl;

import bean.Account;
import dao.AccountDao;
import utils.DBUtil;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

/**
 * 控制事务不能在dao层控制,因为事务和业务紧密相关的,在dao中没有业务逻辑。
 * 通常是N条DML语句联合起来才能完成一个完整的事务。
 */
public class AccountDaoImpl implements AccountDao {
    @Override
    public Account selectByActno(String actno) {
        Connection conn = null;
        PreparedStatement ps = null;
        ResultSet rs = null;
        Account act = null;
        try {
            conn = DBUtil.getConnection();
            String sql = "select balance from t_act where actno=?";
            ps = conn.prepareStatement(sql);
            ps.setString(1,actno);
            rs = ps.executeQuery();
            if (rs.next()){
                act = new Account();
                act.setActno(actno);
                act.setBalance(rs.getDouble("balance"));
            }
        } catch (SQLException e) {
            e.printStackTrace();
            throw new RuntimeException("查询账户异常");
        } finally {
            DBUtil.close(null,ps,rs);
        }
        return act;
    }

    @Override
    public int update(Account newAct) {
        Connection conn = null;
        PreparedStatement ps = null;
        int count = 0;
        try {
            conn = DBUtil.getConnection();
            String sql = "update t_act set balance=? where actno=?";
            ps = conn.prepareStatement(sql);
            ps.setDouble(1,newAct.getBalance());
            ps.setString(2,newAct.getActno());
            count = ps.executeUpdate();
        } catch (SQLException e) {
            e.printStackTrace();
            //手动抛出异常
            throw new RuntimeException("更新账户异常");
        } finally {
            DBUtil.close(null,ps,null);
        }
        return count;
    }
}

欢迎大家的补充评论!!

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值