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;
}
}
欢迎大家的补充评论!!