目录
1 传统方式处理业务的缺点
首先创建一个数据库表:
新增两个数据:
代码如下:
package com.itzw.bank.web.servlet;
import com.itzw.bank.exceptions.AppException;
import com.itzw.bank.exceptions.MoneyNotEnoughException;
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.sql.*;
@WebServlet("/tansfer")
public class AccountTransferServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
response.setContentType("text/html");
PrintWriter out = response.getWriter();
//获取前端提交的信息
String formAct = request.getParameter("formAct");
String toAct = request.getParameter("toAct");
double money = Double.parseDouble(request.getParameter("money"));
//连接数据库
//1.转账前判断余额够不够
Connection conn = null;
PreparedStatement ps = null;
PreparedStatement ps2 = null;
PreparedStatement ps3 = null;
ResultSet rs = null;
try {
//注册驱动
Class.forName("com.mysql.cj.jdbc.Driver");
//获取连接
String url = "jdbc:mysql://127.0.0.1:3306/mvc";
String user = "root";
String password = "123456";
conn = DriverManager.getConnection(url,user,password);
//开启事务(不再自动提交,改为手动提交)
conn.setAutoCommit(false);
//获取预编译的数据库操作对象
String sql1 = "select balance from t_act where actno = ?";
ps = conn.prepareStatement(sql1);
ps.setString(1,formAct);
//执行sql
rs = ps.executeQuery();
//处理结果集
if (rs.next()){
double balance = rs.getDouble("balance");
if (balance < money){
//余额不足(报余额不足异常)
throw new MoneyNotEnoughException("您的余额不足,赶紧充钱!");
}
//到这余额是够的
//给001减10000,
//给002加10000
String sql2 = "update t_act set balance = balance - ? where actno = ?";
ps2 = conn.prepareStatement(sql2);
ps2.setDouble(1,money);
ps2.setString(2,formAct);
int count = ps2.executeUpdate();
//手动设置一个异常
/* String s = null;
s.toString();*/
String sql3 = "update t_act set balance = balance + ? where actno = ?";
ps3 = conn.prepareStatement(sql3);
ps3.setDouble(1,money);
ps3.setString(2,toAct);
//累计
count += ps3.executeUpdate();
if (count != 2){
throw new AppException("App异常,请联系管理员");
}
//手动提交事务
conn.commit();
//转账成功
out.print("转账成功!");
}
} catch (Exception e) {
//事务回滚
if (conn != null) {
try {
conn.rollback();
} catch (SQLException ex) {
ex.printStackTrace();
}
}
//报错提示
//e.printStackTrace();
out.print(e.getMessage());
}finally {
if (rs != null) {
try {
rs.close();
} catch (SQLException throwables) {
throwables.printStackTrace();
}
}
if (ps != null) {
try {
ps.close();
} catch (SQLException throwables) {
throwables.printStackTrace();
}
}
if (ps2 != null) {
try {
ps2.close();
} catch (SQLException throwables) {
throwables.printStackTrace();
}
}
if (ps3 != null) {
try {
ps3.close();
} catch (SQLException throwables) {
throwables.printStackTrace();
}
}
if (conn != null) {
try {
conn.close();
} catch (SQLException throwables) {
throwables.printStackTrace();
}
}
}
}
}
<%@ 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>
</head>
<body>
<form action="tansfer" method="post">
转出账户:<input type="text" name="formAct"><br>
转入账户:<input type="text" name="toAct"><br>
转账金额:<input type="text" name="money"><br>
<input type="submit" value="转账"><br>
</form>
</body>
</html>
这种银行转账的代码我们之前就写过,首先写一个前端页面,在数据库中创建一个表,然后使用Servlet连接数据库并进行核心业务处理,注意还要手动提交事务,最终完成转账,但我们也发现一个问题,本次转账代码中的Servlet基本上完成了所有任务,比如数据接收、核心业务处理、数据库表中数据的曾删改查、页面数据展示。这就导致这个程序的复用性很低,当我还有别的业务需求的时候,需要再写一遍相同的代码;耦合度高,程序很难扩展;操作数据库的代码和业务逻辑混杂在一起,很容易出错。无法专注业务逻辑的编写。
2 MVC架构模式理论基础
系统为什么要分层?
希望专人干专事,各司其职,职能分工明确,这样可以让代码耦合度降低,扩展能力增强。
软件架构中,有一个非常著名的架构模式:MVC架构模式
- M(Model:数据/业务) V(View:视图/展示) C(Controller:控制器)
- C是核心,是控制器,是司令官
- M处理业务,是处理数据的一个秘书
- V负责页面的展示的另一个秘书
- MVC:一个司令官,调度两个秘书去做这件事
实现MVC大致流程:用户发送请求,Controller接收请求,它会把请求发给Model处理,Model连接数据库处理完数据在返回给Controller,拿到数据再发给View展示数据,然后返回给Controller,最终响应到用户。
4 MVC架构模式如何设计
4.1 设计JDBC工具类的封装
package com.itzw.bank.utils;
import java.sql.*;
import java.util.ResourceBundle;
/**
* JDBC工具类的封装
* @author zhouzhouzhou
* @version 1.0
* @since 1.0
*/
public class DBUtil {
private static ResourceBundle bundle = ResourceBundle.getBundle("resources/jdbc");
private static String driver = bundle.getString("driver");
private static String url = bundle.getString("url");
private static String user = bundle.getString("user");
private static String password = bundle.getString("password");
//编写私有无参构造是因为不让创建对象,因为工具类中的方法都是静态的,不需要创建对象
private DBUtil(){}
//在类加载时注册驱动
static {
try {
Class.forName(driver);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
/**
* 获取连接,没有使用数据库连接池
* @return connection
* @throws SQLException
*/
public static Connection getConnection() throws SQLException {
Connection connection = DriverManager.getConnection(url, user, password);
return connection;
}
public static void close(Connection conn, Statement ps, ResultSet rs){
if (rs != null) {
try {
rs.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (ps != null) {
try {
ps.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (conn != null) {
try {
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
4.2 创建封装数据的对象
创建名为Account的类,用它来封装对象,有的人会把这种专门封装数据的对象称之为bean对象,也有人称之为pojo对象,还有人称为domain对象,都一样,称呼不同而已。还需要注意的是属性的类型选择,一般不建议设计为基本数据类型,建议使用包装类,防止null带来问题,就比如我这次的数据类型,它是对象数据库的,数据库中的bigint对应java的long,但是我用Long代替long,还有数据库中的decimal表示java中的double,我用Double代替double:
package com.itzw.bank.mvc;
public class Account {
//id
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 +
'}';
}
}
4.3 设计Dao模式
- 它是JavaEE设计模式之一
- 创建AccountDao类,它用来负责对Account数据的增删改查
- 什么是Dao:Data Access Object(数据访问对象)
- Dao只负责对数据库表的CRUD,没有任何业务逻辑在里面。
- 一般情况下一张表对应一个Dao
package com.itzw.bank.mvc;
import com.itzw.bank.utils.DBUtil;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
public class AccountDao {
/**
* 插入数据
* @param act
* @return
*/
public int insert(Account act){
Connection conn = null;
PreparedStatement ps = null;
int count = 0;
try {
conn = DBUtil.getConnection();
String sql = "insert into t_act (actno,balance) values (?,?)";
ps = conn.prepareStatement(sql);
ps.setString(1,act.getActno());
ps.setDouble(2,act.getBalance());
count = ps.executeUpdate();
} catch (SQLException e) {
e.printStackTrace();
}finally {
DBUtil.close(conn,ps,null);
}
return count;
}
/**
* 根据id删除数据
* @param id
* @return
*/
public int deleteById(Long id){
Connection conn = null;
PreparedStatement ps = null;
int count = 0;
try {
conn = DBUtil.getConnection();
String sql = "delete from t_act where id = ?";
ps = conn.prepareStatement(sql);
ps.setLong(1,id);
count = ps.executeUpdate();
} catch (SQLException e) {
e.printStackTrace();
}finally {
DBUtil.close(conn,ps,null);
}
return count;
}
/**
* 修改数据
* @param act
* @return
*/
public int update(Account act){
Connection conn = null;
PreparedStatement ps = null;
int count = 0;
try {
conn = DBUtil.getConnection();
String sql = "update t_act set actno = ?,balance = ? where id = ?";
ps = conn.prepareStatement(sql);
ps.setString(1,act.getActno());
ps.setDouble(2,act.getBalance());
ps.setLong(3,act.getId());
count = ps.executeUpdate();
} catch (SQLException e) {
e.printStackTrace();
}finally {
DBUtil.close(conn,ps,null);
}
return count;
}
/**
* 根据账户查找数据
* @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 id,balance from t_act where actno = ?";
ps = conn.prepareStatement(sql);
ps.setString(1,actno);
rs = ps.executeQuery();
if (rs.next()){
long id = rs.getLong("id");
double balance = rs.getDouble("balance");
//将结果封装成java对象
act = new Account(id,actno,balance);
}
} catch (SQLException e) {
e.printStackTrace();
}finally {
DBUtil.close(conn,ps,rs);
}
return act;
}
/**
* 返回所有数据
* @return
*/
public List<Account> selectAll(){
Connection conn = null;
PreparedStatement ps = null;
ResultSet rs = null;
List<Account> list = new ArrayList<>();
try {
conn = DBUtil.getConnection();
String sql = "select id,actno,balance from t_act";
ps = conn.prepareStatement(sql);
rs = ps.executeQuery();
while (rs.next()){
long id = rs.getLong("id");
String actno = rs.getString("actno");
double balance = rs.getDouble("balance");
Account act = new Account(id,actno,balance);
list.add(act);
}
} catch (SQLException e) {
e.printStackTrace();
}finally {
DBUtil.close(conn,ps,rs);
}
return list;
}
}
4.4 业务逻辑编写
package com.itzw.bank.mvc;
import com.itzw.bank.exceptions.AppException;
import com.itzw.bank.exceptions.MoneyNotEnoughException;
public class AccountService {
private AccountDao accountDao = new AccountDao();
public void transfer(String fromActno,String toActno,double money) throws MoneyNotEnoughException, AppException {
//判断钱够不够
Account fromAct = accountDao.selectByActno(fromActno);
if (fromAct.getBalance() < money){
//余额不足
throw new MoneyNotEnoughException("余额不足,感觉充钱吧!");
}
//余额充足
Account toAct = accountDao.selectByActno(toActno);
//更改内存中的金额
fromAct.setBalance(fromAct.getBalance() - money);
toAct.setBalance(toAct.getBalance() + money);
//更改数据库中的金额
int count = accountDao.update(fromAct);
count += accountDao.update(toAct);
if (count != 2){
throw new AppException("数据库异常,请联系管理员!");
}
}
}
4.5 调度中心的编写
package com.itzw.bank.mvc;
import com.itzw.bank.exceptions.AppException;
import com.itzw.bank.exceptions.MoneyNotEnoughException;
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;
/**
* 账户小程序,它是一个司令官或者说是Controller
* @author zhouzhou
* @version 2.0
* @since 2.0
*/
@WebServlet("/tansfer")
public class AccountServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
//接收数据
String formAct = request.getParameter("formAct");
String toAct = request.getParameter("toAct");
double money = Double.parseDouble(request.getParameter("money"));
//调用业务方法处理
AccountService accountService = new AccountService();
try {
accountService.transfer(formAct,toAct,money);
//成功了,展示处理结果
response.sendRedirect(request.getContextPath()+"/success.jsp");
} catch (MoneyNotEnoughException e) {
//失败了(余额不足)
response.sendRedirect(request.getContextPath()+"/moneynotenough.jsp");
} catch (Exception e) {
//失败了
response.sendRedirect(request.getContextPath()+"/error.jsp");
}
}
}
4.6 MVC框架和三层架构之间的关系
三层架构是什么样的呢?
- 三层架构的三层分别为:表现层(web层/表示层)、业务逻辑层、持久化层
- 其中表现层中包含的有Servlet、JSP等等;业务逻辑层是service;持久化层是Dao
- 当用户发送请求的时候,会先发送给表现层,然后表现层发给业务逻辑层,然后业务逻辑层发给持久化层,持久化层再连接数据库,得到数据再原路返回,最终返回到用户。
- 就像前面我们写的代码一样,Servlet中包含着service,service包含着Dao,而Dao又连接着DB数据库
MVC框架之前我们就讲过:
Controller像总司令一样负责控制Model和View,而其中Model也就是负责处理数据的环节,它包含了业务逻辑层和持久化层,这也很好理解,Model负责业务处理,而业务逻辑层也就是service就是负责业务处理的,而业务处理必然是包含着Dao也就是持久化层的。总司令controller和view就相当于表现层,因为他们是直接与用户接触的。这也MVC框架和三层架构的关系就了然了。
4.7 解决事务问题
我们发现将各种事情分开来做之后,事务就很难写进去,需要改很多东西,而改过之后代码复杂很多,而且失去了原有的专人做专事的特性,改造过程有一点麻烦,懒得改了。。。究其原因,为什么改造麻烦,因为Connection会创建多个,为了不创建多个不得不在多个文件中传入相同的Connection。然后我们发现线程Thread是不会变得,有没有可能存在一个Map集合,它里面的key装着当前的Thread,而value就是Connection,这样我们不需要传入Connection,只需要获取Thread即可。还真有,那就是ThreadLocal集合,可以将Connection对象装入其中,需要放在DBUtil文件中的getConnection中,在Dao文件使用这个工具类时或者其它文件使用这个工具类时拿到的Connection对象都是同一个Connection对象。主要修改getConnection方法即可,如下:
private static ThreadLocal<Connection> local = new ThreadLocal<>();
/**
* 获取连接,没有使用数据库连接池
* @return connection
* @throws SQLException
*/
public static Connection getConnection() throws SQLException {
Connection connection = local.get();
if (connection == null){
connection = DriverManager.getConnection(url, user, password);
local.set(connection);
}
return connection;
}
注意最后要将conn从Map集合中移除
if (conn != null) {
try {
conn.close();
//思考,为什么conn关闭之后要从大Map中移除呢
//根本原因是:Tomcat服务器是支持线程池的,也就是说一个人用过了t1线程,t1线程还有其它用户使用
local.remove();
} catch (SQLException e) {
e.printStackTrace();
}
}
至此,这个小项目已经相当完美了!!但还是可以改进,比如目录,给不同任务的文件都新建一个对应的包,如下:
还有一点,一般层与层直接是用接口连接,这样可以降低耦合度,提高扩展力。也就是业务逻辑层和持久化层用接口实现。也就是dao文件和service文件需要使用接口实现,如下:
package com.itzw.bank.dao;
import com.itzw.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();
}
package com.itzw.bank.service;
import com.itzw.bank.exceptions.AppException;
import com.itzw.bank.exceptions.MoneyNotEnoughException;
public interface AccountService {
public void transfer(String fromActno,String toActno,double money) throws MoneyNotEnoughException, AppException;
}
然后在新建impl包在里面新建类实现对应接口即可,注意实现接口要使用多态了,如下:
private AccountDao accountDao = new AccountDaoImpl();//多态
到这就差不多结束了,依然有一些问题,不过大致格式就是这样,以后写项目就按这种目录结构编写,非常的规范。