后端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框架建立项目
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 |
画个图表示一下
MVC框架如何作用
以登录逻辑为例,分析从前端请求到获得响应的整个过程
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){ req.getSession().setAttribute(Constants.USER_SESSION, user); 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); if (rs.next()) { user = new User(); user.setId(rs.getInt("id")); 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
|
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++) { preparedStatement.setObject(i + 1, params[i]); } System.out.println("DruidDao>>查>>SQL :" + preparedStatement.toString()); resultSet = preparedStatement.executeQuery(); return resultSet; }
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++) { 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 { 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接口干了啥呢?
每一个函数,都对应于一种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层: