dustland

dustball in dustland

后端MVC框架

后端MVC框架

参考SMBMS: 跟着狂神做超市订单管理系统 (gitee.com)

MVC框架的使用时机

当需求分析结束后,数据库也已经建立完毕了.建立了四张数据表

1
2
3
4
5
6
7
8
9
10
11
12
mysql> use smbms;
Database changed
mysql> show tables;
+-----------------+
| Tables_in_smbms |
+-----------------+
| smbms_bill |
| smbms_provider |
| smbms_role |
| smbms_user |
+-----------------+
4 rows in set (0.00 sec)

下面的任务就是写后端逻辑代码了,此时使用MVC框架建立项目

project1

MVC框架是什么

整个后端在MVC框架中分了三层,DAO,Service,Controller

DAO层:data access object,数据访问层.每一个DAO类一定对应一个数据表,DAO类的函数,只做原子操作,CRUD

一个函数只实现一个功能,比如给定一个账单号,查库删除该账单

或者给定一个用户名和新密码,修改其密码.

可以理解为,DAO类的函数,一般只执行一条sql语句,一般只影响一行

Service层:服务层.对Dao层的包装,一个Service层函数可能调用一个或者多个Dao层函数.返回pojo对象.

比如service上一个login(userCode,userPassword),首先会用userCode去查smbms_user表,然后用获取到的表项实例化一个user pojo,然后user.getPassword和userPassword进行对比,相同则返回user对象,否则返回null指针

Controller层:控制层,由Servlet实现,直接接收来自前端的get或者post请求,提取参数,决定使用哪种服务

输入 输出 任务
Controller request with parameters respond 提取参数,决定服务
Service raw parameters pojo or else 执行事务,组合业务
DAO connection,raw parameters pojo or else 执行原子操作,CRUD

画个图表示一下

image-20230104191129638

MVC框架如何作用

以登录逻辑为例,分析从前端请求到获得响应的整个过程

image-20230104184242886

1.前端发起HTTP请求

请求服务端你的login.do资源,携带两个get参数

目标DNS:localhost,会被本地hosts文件映射到IP:127.0.0.1

目标端口:8080

2.该请求到达了服务端

经过TCP/IP协议栈,将HTTP报文交给位于应用层的web服务器(tomcat)

3.服务器首先查询web.xml

找到login.do这路径应该映射给控制层的LoginServlet

1
2
3
4
5
6
7
8
<servlet>
<servlet-name>LoginServlet</servlet-name>
<servlet-class>com.threepure.servlet.user.LoginServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>LoginServlet</servlet-name>
<url-pattern>/login.do</url-pattern>
</servlet-mapping>

至于tomcat具体怎么将HTTP请求报文交给LoginServlet的,目前不清楚,留作后话

可能手写一个玩具tomcat服务器就知道了,但不是现在

于是将该get请求交给LoginServlet.doGet(req,resp)

4.调用控制层LoginServlet.doGet函数

首先提取参数

1
2
String userCode = req.getParameter("userCode");
String userPassword = req.getParameter("userPassword");

然后调用服务层,查询用户名密码是否正确

1
2
UserServiceImpl userService = new UserServiceImpl();
User user = userService.login(userCode, userPassword);

这里user是一个pojo类型,其属性和user数据表的属性一一对应,一个user对象对应一条user表的记录

然后根据user是否为空,决定是否登陆成功

1
2
3
4
5
6
7
if (user!=null){//user非空说明存在该用户且用户名密码正确
req.getSession().setAttribute(Constants.USER_SESSION, user);//设置session
resp.sendRedirect("jsp/frame.jsp");//跳转登录之后的界面
}else {//要么不存在用户名,要么密码错误,反正登不上
req.setAttribute("error", "用户名或者密码错误");
req.getRequestDispatcher("login.jsp").forward(req, resp);//重新登录
}

结果有两个,

一个是设置session,保存用户登录状态,也就是设置了访问权限,此后过滤器会根据是否有session决定能否访问网站资源

一个是返回resp,设置页面重定向.实际上是返回了重定向的HTTP respond报文

5.调用服务层userService.login(userCode, userPassword)函数

首先提升作用域,建立两个局部变量

1
2
Connection connection = null;
User user = null;

connection的建立和销毁都由本login函数决定,也就是资源的创建和销毁是Service服务层决定的

然后调用Dao层函数,根据userDao.getLoginUser(connection, userCode)查询是否存在用户

1
2
3
4
5
6
7
8
try {
connection = DruidDao.getConnection();
user = userDao.getLoginUser(connection, userCode);
} catch (SQLException throwables) {
throwables.printStackTrace();
} finally {
DruidDao.closeResource(null, null, connection);
}

查完了立刻在finally中释放资源

如果存在该用户,Dao层会返回一个user pojo,和数据库中该用户的记录对应.

然后要判断用户密码是否正确,如果正确返回该pojo,控制层会使用该pojo执行其他业务.

如果密码错误或者user不存在,均会返回null,控制层发现该返回值为null就知道了用户名密码错误

1
2
3
4
5
6
if (user != null) {
if (!user.getUserPassword().equals(password)) {
user = null;
}
}
return user;

6.调用DAO层userDao.getLoginUser(connection, userCode)函数

首先提升作用域,创建局部变量

1
2
3
PreparedStatement pstm = null;
ResultSet rs = null;
User user = null;

然后调用DAO公共底层DruidDao.execute函数,查库

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
       if (connection != null) {
String sql = "select * from smbms_user where userCode=?";
Object[] params = {userCode};
try {
rs = DruidDao.execute(connection, pstm, rs, sql, params);//rs可能有多条
if (rs.next()) {//最终user是rs的最后一条记录
user = new User();
user.setId(rs.getInt("id"));//使用rs记录实例化一个pojo
user.setUserCode(rs.getString("userCode"));
user.setUserName(rs.getString("userName"));
user.setUserPassword(rs.getString("userPassword"));
user.setGender(rs.getInt("gender"));
user.setBirthday(rs.getDate("birthday"));
user.setPhone(rs.getString("phone"));
user.setAddress(rs.getString("address"));
user.setUserRole(rs.getInt("userRole"));
user.setCreatedBy(rs.getInt("createdBy"));
user.setCreationDate(rs.getTimestamp("creationDate"));
user.setModifyBy(rs.getInt("modifyBy"));
user.setModifyDate(rs.getTimestamp("modifyDate"));
}
DruidDao.closeResource(rs, pstm, connection);//释放资源
} catch (SQLException throwables) {
throwables.printStackTrace();
}
}
return user;

7.调用DAO层公共函数Druid.execute

CRUD根据读写性质分成两种,只读的查,和读写的增删改

并且只有查询要返回记录,其他的操作只需要返回几行受到影响即可

因此execute有两个重载,一个负责只读的查,一个负责读写的增删改

这里使用sql预编译,目的是提高速度

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/**
* @description:编写查询公共方法
*/
public static ResultSet execute(Connection connection, PreparedStatement preparedStatement, ResultSet resultSet, String sql, Object[] params) throws SQLException {
preparedStatement = connection.prepareStatement(sql);
for (int i = 0; i < params.length; i++) {
//setObject()占位符是从1开始,而数组是从0开始;
preparedStatement.setObject(i + 1, params[i]);
}
System.out.println("DruidDao>>查>>SQL :" + preparedStatement.toString());
resultSet = preparedStatement.executeQuery();
return resultSet;
}

/**
* @description:编写增删改公共方法
*/
public static int execute(Connection connection, PreparedStatement preparedStatement, String sql, Object[] params) throws SQLException {
preparedStatement = connection.prepareStatement(sql);
for (int i = 0; i < params.length; i++) {
//setObject()占位符是从1开始,而数组是从0开始;
preparedStatement.setObject(i + 1, params[i]);
}
System.out.println("DruidDao>>增删改>>SQL :" + preparedStatement.toString());
return preparedStatement.executeUpdate();
}

MVC框架的建立过程

参考SMBMS: 跟着狂神做超市订单管理系统 (gitee.com)

准备工作

0.搭建服务器环境,使用tomcat服务器部署javaweb项目

1.建立pojo目录,在该目录下,给每个数据表建立一个一一对一个的pojo类

1
2
3
4
5
smbms/src/main/java/top/dustball/pojo/
User.java
Provider.java
Bill.java
Role.java

2.建立Dao层公共底层类,实现execute函数

1
2
smbms/src/main/java/top/dustball/dao/
DaoBase.java

3.引入资源,包括前端的jsp页面,js,css文件.数据库连接设置

1
2
smbms/src/main/java/resources/
db.properties
1
2
3
4
5
6
7
8
9
10
11
12
smbms/src/main/webapp/
jsp/
...
js/
...
css/
...
images/
...
login.jsp
error.jsp
...

其中smbms/src/main/webapp/这下面的jsp,js,css,images等都是前端的工作.

前端和后端的接口是jsp和web.xml共同决定的,请求路径以及get或者post的参数名称

1
2
login.jsp
<form class="loginForm" action="${pageContext.request.contextPath }/login.do" name="actionForm" id="actionForm" method="post" >
1
2
3
4
5
6
7
8
9
web.xml
<servlet>
<servlet-name>LoginServlet</servlet-name>
<servlet-class>com.threepure.servlet.user.LoginServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>LoginServlet</servlet-name>
<url-pattern>/login.do</url-pattern>
</servlet-mapping>

至于前端的页面长什么样,后端不用管,后端只关心控制层接收到的get参数叫什么,是叫"username",还是"UserName"还是"userCode".

前端需要知道一个get请求应该发往哪一个Servlet

只有这两个是前后端开发者需要协商的

4.建立字符过滤器,设置前后端都使用utf-8编码

自顶向下还是自底向上

目前感觉两种方法均可

两头都很具体,顶上是前端写好的请求,参数名都已知,交给哪一个Servlet也是确定的

底下有几个数据表,都长什么样也是确定的

中间的service层很多情况下只是对Dao层的一个薄封装,以service层的UserServiceImpl.login函数为例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public User login(String userCode, String password) {
Connection connection = null;
User user = null;

try {
connection = DruidDao.getConnection();
user = userDao.getLoginUser(connection, userCode);
} catch (SQLException throwables) {
throwables.printStackTrace();
} finally {
DruidDao.closeResource(null, null, connection);
}
//进行密码匹配
if (user != null) {
if (!user.getUserPassword().equals(password)) {
user = null;
}
}
return user;
}

实际上这么一长段的核心功能就是

接收一个userCode一个password,返回一个user pojo.就是多加了一个密码判断

UserServiceImpl.updataPwd更明显,直接调用了Dao层,除此之外啥也没干

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public boolean updatePwd(int id, String pwd) {
Connection connection = null;
boolean flag = false;

try {
connection = DruidDao.getConnection();
if (userDao.updatePwd(connection, id, pwd) > 0) {
flag = true;
}
} catch (SQLException throwables) {
throwables.printStackTrace();
} finally {
DruidDao.closeResource(null, null, connection);
}
return flag;
}

还是采用自顶向下吧,根据前端存在的需求,决定后端写哪些服务

比如对于用户表,不存在修改用户名的需求,因为有新建用户就够了.

如果自底向上写的话,不知道需要对用户表的哪些字段进行什么操作.

如果自顶向下写的话,首先知道的就是需求,然后根据需求决定下层要提供什么服务,然后再实现该服务,不需要的服务不写

建立Controller层

前端和后端商量好的,前端可能会产生哪些请求,每个请求发给哪个servlet

再细分一下,感觉

1
2
3
4
5
这是前端写的
<servlet-mapping>
<servlet-name>LoginServlet</servlet-name>
<url-pattern>/login.do</url-pattern>
</servlet-mapping>
1
2
3
4
5
6
这是后端写的
<servlet>
<servlet-name>LoginServlet</servlet-name>
<servlet-class>com.threepure.servlet.user.LoginServlet</servlet-class>
</servlet>

现在就面临一个问题

用户的行为可能有很多,比如登录,注销,修改密码,订单管理,供应商管理等等

这么多行为,应该用几个servlet实现?

针对用户的行为,有三个Servlet,LoginServlet,LogoutServlet,UserServlet

为啥要这样划分?

首先考虑权限问题,前端是无法过滤请求的,权限控制只能是后端的权限过滤器来做

权限过滤器根据session是否设置决定能否请求资源,对任何/jsp/*的请求生效

用户的行为中,只有登录界面是在设置session前可以访问的,因此webapp下面的login.jsp不会被权限过滤,任何情况均可访问

因此登录行为自己建立一个LoginServlet

登录和注销,两个行为都会更改session状态,因此各自一个Servlet处理业务

其他的用户行为都需要登录权限,因此在jsp目录下,收到权限过滤器保护,这些行为都有session状态,都发往一个UserServlet

通过一个method参数,决定该UserServlet调用何种服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String method = req.getParameter("method");
if ("savepwd".equals(method) && method != null) {
this.updatePwd(req, resp);
} else if ("pwdmodify".equals(method) && method != null) {
this.pwdmodify(req, resp);
} else if ("query".equals(method) && method != null) {
this.userQuery(req, resp);
} else if ("add".equals(method) && method != null) {
this.add(req, resp);
} else if ("getrolelist".equals(method) && method != null) {
this.getRoleList(req, resp);
} else if ("ucexist".equals(method) && method != null) {
this.userCodeExist(req, resp);
} else if ("view".equals(method) && method != null) {
this.getUserById(req, resp, "userview.jsp");
} else if ("modify".equals(method) && method != null) {
this.getUserById(req, resp, "usermodify.jsp");
} else if ("modifyexe".equals(method) && method != null) {
this.userModify(req, resp);
} else if ("deluser".equals(method) && method != null) {
this.deleteUser(req, resp);
}
}

实际上是控制耦合,这个method参数就是控制开关

Bill和Provider各自只有一个Servlet,也在doGet方法上采用控制耦合

总的来说,尽量少建立Servlet,可以使用控制耦合,大体上还是一个数据表对应一个Servlet,所有发生在该表上的行为,用一个Servlet处理,通过method参数的不同区分Service层的服务

建立Service层

Controller层建立完毕之后,Service实际上已经固定了

User.getUserById给定一个用户名,查库建立一个user pojo,从控制层到服务层到数据访问层逐层往下扔就是了

1
2
3
4
5
6
7
8
9
10
11
12
Controller层User.getUserById
private void getUserById(HttpServletRequest req, HttpServletResponse resp, String url) throws ServletException, IOException {
//获取传入的id
String uid = req.getParameter("uid");
if (!StringUtils.isNullOrEmpty(uid)) {
UserServiceImpl userService = new UserServiceImpl();
User userById = userService.getUserById(uid);//往下扔
req.setAttribute("user", userById);
req.getRequestDispatcher(url).forward(req, resp);
}
}

建立DAO层

每一个数据表都会对应一个pojo类,一个Dao层类

每一个数据表都会对应一个pojo类,一个Dao层类

每一个数据表都会对应一个pojo类,一个Dao层类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Dao/
│ DaoBase.java

├─bill
│ BillDao.java
│ BillDaoImpl.java

├─provider
│ ProviderDao.java
│ ProviderDaoImpl.java

├─role
│ RoleDao.java
│ RoleDaoImpl.java

└─user
UserDao.java
UserDaoImpl.java

比如用户表user,对应一个pojo User,然后在Dao/user/下面有一个UserDao接口和一个UserDaoImpl实现

这个UserDao接口干了啥呢?

image-20230104200556894

每一个函数,都对应于一种user表中的原子操作(一条sql语句)

每一个函数,都对应于一种user表中的原子操作(一条sql语句)

每一个函数,都对应于一种user表中的原子操作(一条sql语句)

UserDao函数 user表原子操作
getLoginUser( connection, userCode) select * from smbms_user where userCode=?
updatePwd(connection, id, password) update smbms_user set userPassword = ? where id = ?
add(Connection connection, User user) insert into smbms_user (userCode,userName,userPassword," + "userRole,gender,birthday,phone,address,creationDate,createdBy) " + "values(?,?,?,?,?,?,?,?,?,?)
... ...

"中途优化是万恶之源"

等整个网站正常运行起来之后,再考虑如何优化,到那时候再考虑使用什么Druid连接池

最初就用最普通的mysql就可以了

Service层: