后端系统开发
Servlet
完善水果系统-编辑和修改特定库存信息
新需求:将名称,比如说山竹,改成超链接,跳转到另一个页面
在.html文件中,修改显示名称的代码
`苹果
再考虑edit.do所在的目录,他是上面页面的跳转页面,默认情况下他们在同一个目录下,为了保证他们在同一个目录
苹果 `分析上面“跳转”这个新的需求,我们在点击之后,相当于客户端向服务器端发起了一个新的请求,经过前面的学习,我们已经知道请求的获取,处理,响应都是通过servlet这个结构来完成的,所以我们需要设计一个XxxServlet类来做和请求相关的事情
一个请求该调用哪一个XxxServlet类呢??怎么确定的??
一方面前端的html代码会有一个标签a,一方面后端有XxxServlet类这个类名,也可以当做是标签b。我们在两个标签之间建立映射关系
有注解以前,在web.xml中建立映射关系
<servlet>
<servlet-name>AddServlet</servlet-name>
<servlet-class>com.atguigu.servlets.AddServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>AddServlet</servlet-name>
<url-pattern>/add</url-pattern>
</servlet-mapping>
<!--
1. 用户发请求,action=add
2. 项目中,web.xml中找到url-pattern = /add -> 第12行
3. 找第11行的servlet-name = AddServlet
4. 找和servlet-mapping中servlet-name一致的servlet , 找到第7行
5. 找第8行的servlet-class -> com.atguigu.servlets.AddServlet
6. 用户发送的是post请求(method=post) , 因此 tomcat会执行AddServlet中的doPost方法
-->
有了注解就非常方便了,就在XxxServlet类上边加上一句@WebServlet("/标签a")
如果标签a是add,那么注解就是@WebServlet("/add")
如果标签a是edit.do,那么注解就是@WebServlet("/edit.do")
新需求:让请求携带信息发给服务器,非手动输入
后端的XxxServlet在处理请求的时候,有时候需要前端提供数据,这些数据被保存在请求当中。比如说之前的AddServlet,但是区别在于AddServlet的数据是由用户在客户端手动输入的
可以这样写<td><a th:text="${fruit.fname}" th:href="@{/edit.do?fid=fruit.fid}">苹果</a></td>
?是一个连接符号,用于连接后面的参数。这在之前的数据库部分使用过
新需求:如何解析url
在上一个需求,我们写了 /edit.do?fid=fruit.fid,这是一个字符串。如果直接这样写,thymeleaf是无法识别的
<td><a th:text="${fruit.fname}" th:href="@{/edit.do(fid=${fruit.fid})}">苹果</a></td>
to be honest,前端这块我是真没看明白
新需求:服务器响应请求,返回一个新的页面
通过请求中携带的id数据,到数据库中查询相关数据,然后将数据反映到跳转的页面上
跳转的页面也是用thymeleaf来进行渲染
这一块就是后端的EditServlet,可以看到调用的processTemplate方法要求跳转到一个叫做edit的页面上
@WebServlet("/edit.do")
public class EditServlet extends ViewBaseServlet {
private FruitDAO fruitDAO = new FruitDAOImpl();
@Override
public void doGet(HttpServletRequest request , HttpServletResponse response)throws IOException, ServletException {
String fidStr = request.getParameter("fid");
if(StringUtil.isNotEmpty(fidStr)){
int fid = Integer.parseInt(fidStr);
Fruit fruit = fruitDAO.getFruitByFid(fid);
request.setAttribute("fruit",fruit);
super.processTemplate("edit",request,response);
}
}
}
这一块就是edit.html这个页面的写法
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="css/edit.css">
</head>
<body>
<div id="div_container">
<div id="div_fruit_list">
<p class="center f30">编辑库存信息3</p>
<form th:action="@{/update.do}" method="post" th:object="${fruit}">
<!-- 隐藏域 : 功能类似于文本框 , 它的值会随着表单的发送也会发送给服务器,但是界面上用户看不到 -->
<input type="hidden" name="fid" th:value="*{fid}"/>
<table id="tbl_fruit">
<tr>
<th class="w20">名称:</th>
<!-- <td><input type="text" name="fname" th:value="${fruit.fname}"/></td> -->
<td><input type="text" name="fname" th:value="*{fname}"/></td>
</tr>
<tr>
<th class="w20">单价:</th>
<td><input type="text" name="price" th:value="*{price}"/></td>
</tr>
<tr>
<th class="w20">库存:</th>
<td><input type="text" name="fcount" th:value="*{fcount}"/></td>
</tr>
<tr>
<th class="w20">备注:</th>
<td><input type="text" name="remark" th:value="*{remark}"/></td>
</tr>
<tr>
<th colspan="2">
<input type="submit" value="修改" />
</th>
</tr>
</table>
</form>
</div>
</div>
</body>
</html>
最终的效果,注意看url
新需求:编辑完一条记录后,提交数据
这就和之前的AddServlet那里一样了,我们自定义的数据被存到请求中,后端相应的Servlet收到以后,取出数据,写到数据库当中去
edit.html写明了action=update.do,method=post,所以说我们设计一个UpdateServlet类,重写里面的doPost方法
@WebServlet("/update.do")
public class UpdateServlet extends ViewBaseServlet {
private FruitDAO fruitDAO = new FruitDAOImpl();
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//1.设置编码
request.setCharacterEncoding("utf-8");
//2.获取参数
String fidStr = request.getParameter("fid");
Integer fid = Integer.parseInt(fidStr);
String fname = request.getParameter("fname");
String priceStr = request.getParameter("price");
int price = Integer.parseInt(priceStr);
String fcountStr = request.getParameter("fcount");
Integer fcount = Integer.parseInt(fcountStr);
String remark = request.getParameter("remark");
//3.执行更新
fruitDAO.updateFruit(new Fruit(fid,fname, price ,fcount ,remark ));
//4.资源跳转
//super.processTemplate("index",request,response);
//request.getRequestDispatcher("index.html").forward(request,response);
//此处需要重定向,目的是重新给IndexServlet发请求,重新获取furitList,然后覆盖到session中,这样index.html页面上显示的session中的数据才是最新的
response.sendRedirect("index");
}
}
最后这个资源跳转值得说一下,原先在thymeleaf都回去调用父类里边的processTemplate方法,它本质上就是做一次服务器端转发。但是,这就意味着保存到HttpSession容器的查询结果fruitList还是旧的那一个,无法显示更新后的结果。因此,需要做客户端重定向,重新发起请求,这样对应的servlet需要重新处理请求,重新获取一次fruitList,覆盖掉之前的那一个
总结
在回顾一下实现这个功能的流程:
(1)index.html页面
(2)点击水果名称之后(这是超链接,不是跳转),发起请求,由EditServlet类来处理
(3)EditServlet类在最后跳转到edit.html页面(thymeleaf)
(4)在edit.html页面点击提交数据之后,相当于又发起请求
(5)这个请求由UpdateServlet类来处理
(6)UpdateServlet类最后跳转到index.html页面(thymeleaf)
完善水果系统-删除和添加
总的需求:通过点击右边的×,删除一条记录
新需求:点击×,进行弹窗
这个行为是JS的代码来控制
和 编辑和修改特定库存信息 这一需求不同,这里不是把×变成超链接,而是调用js的函数
首先在index.html中进行修改,修改表格中和×相关的部分
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="css/index.css">
<script language="JavaScript" src="js/index.js"></script>
</head>
<tr th:unless="${#lists.isEmpty(session.fruitList)}" th:each="fruit : ${session.fruitList}">
<td><a th:text="${fruit.fname}" th:href="@{/edit.do(fid=${fruit.fid})}">苹果</a></td>
<td th:text="${fruit.price}">5</td>
<td th:text="${fruit.fcount}">20</td>
<!-- <td><img src="imgs/del.jpg" class="delImg" th:οnclick="'delFruit('+${fruit.fid}+')'"/></td> -->
<td><img src="imgs/del.jpg" class="delImg" th:onclick="|delFruit(${fruit.fid})|"/></td>
</tr>
然后在js的代码里面定义好使用的函数
function delFruit(fid){
if(confirm('是否确认删除?')){
window.location.href='del.do?fid='+fid;
}
}
新需求:点击确认以后,在数据库中删除这一条记录
js的代码中,这个超文本引用是del.do,同时还在请求中携带了id信息,那么跟请求相关,就需要XxxServlet类了
设计DelServlet类
@WebServlet("/del.do")
public class DelServlet extends ViewBaseServlet {
private FruitDAO fruitDAO = new FruitDAOImpl();
@Override
public void doGet(HttpServletRequest request , HttpServletResponse response)throws IOException, ServletException {
String fidStr = request.getParameter("fid");
if(StringUtil.isNotEmpty(fidStr)){
int fid = Integer.parseInt(fidStr);
fruitDAO.delFruit(fid);
//super.processTemplate("index",request,response);
response.sendRedirect("index");
}
}
}
最后做一次客户端重定向,回到index.html页面
新需求:在index.html页面加上一个添加的超链接
超链接的标签是<a></a>
<p class="center f30">欢迎使用水果库存后台管理系统</p>
<div style="border:0px solid red;width:60%;margin-left:20%;text-align:right;">
<a th:href="@{/add.html}" style="border:0px solid blue;margin-bottom:4px;">添加新库存记录</a>
</div>
注意看,这里超链接是直接跳转到add.html页面,而之前
<a th:text="${fruit.fname}" th:href="@{/edit.do(fid=${fruit.fid})}">
是调用了servlet组件,然后在重写的doPost方法里边跳转到edit.html页面。真的恶心
新需求:点击超链接,跳转到add.html页面
编写add.html的相关代码
一个bug
这个bug隐藏于无形,没有报错,却导致无法调用AddServlet类,使得无法向数据库添加新的纪录
<form action="add.do" method="post">
就是在这里是否需要给add.do加上绝对路径,如果要加,就是加thymeleaf的那个组件,加了就导致上面说的那个bug
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="css/add.css">
</head>
<body>
<div id="div_container">
<div id="div_fruit_list">
<p class="center f30">新增库存信息2</p>
<!--<form action="add.do" method="post">-->
<form action="add.do" method="post">
<table id="tbl_fruit">
<tr>
<th class="w20">名称:</th>
<!-- <td><input type="text" name="fname" th:value="${fruit.fname}"/></td> -->
<td><input type="text" name="fname" /></td>
</tr>
<tr>
<th class="w20">单价:</th>
<td><input type="text" name="price" /></td>
</tr>
<tr>
<th class="w20">库存:</th>
<td><input type="text" name="fcount" /></td>
</tr>
<tr>
<th class="w20">备注:</th>
<td><input type="text" name="remark" /></td>
</tr>
<tr>
<th colspan="2">
<input type="submit" value="添加" />
</th>
</tr>
</table>
</form>
</div>
</div>
</body>
</html>
可以看到,在表单部分action=“add.do” method=“post”,这就说明,提交表单之后,会调用XxxServlet来处理请求
新需求:向数据库增加一条新的纪录
编写标签为add.do的AddServlet类
@WebServlet("/add.do")
public class AddServlet extends ViewBaseServlet {
private FruitDAO fruitDAO = new FruitDAOImpl();
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
request.setCharacterEncoding("UTF-8");
String fname = request.getParameter("fname");
Integer price = Integer.parseInt(request.getParameter("price")) ;
Integer fcount = Integer.parseInt(request.getParameter("fcount"));
String remark = request.getParameter("remark");
Fruit fruit = new Fruit(0,fname , price , fcount , remark ) ;
fruitDAO.addFruit(fruit);
response.sendRedirect("index");
}
}
最后,再重定向到index.html页面
完善水果系统-添加分页功能
新需求:每次只查询固定数量的记录
这一部分和DAO有关,在接口中定义新的方法
@Override
public List<Fruit> getFruitList(Integer pageNo) {
return super.executeQuery("select * from t_fruit limit ? , 5" , (pageNo-1)*5);
}
每次只查询(pageNo-1)*5到(pageNo-1)*5+5的记录
新需求:在index.html页面显示翻页的按键
我们需要在页面下方设置一个区域用于显示页数,翻页等
在这里我才知道,部署的时候pro10/index访问的并不是index.html,而是调用IndexServlet类,最后再跳转到index.html页面,ciao
在index.html下加上这么一个部分
<div style="width:60%;margin-left:20%;border:0px solid red;padding-top:4px;" class="center">
<input type="button" value="首 页1" class="btn" th:onclick="|page(1)|"/>
<input type="button" value="上一页" class="btn" th:onclick="|page(${session.pageNo-1})|"/>
<input type="button" value="下一页" class="btn" th:onclick="|page(${session.pageNo+1})|"/>
<input type="button" value="尾 页" class="btn" th:onclick="|page(${session.pageCount})|"/>
</div>
服务器端需要客户端把页码传过来
这里的page呢是一个JS方法,因此在js里边定义这个page方法
function page(pageNo){
window.location.href="index?pageNo="+pageNo;
}
然后在IndexServlet类中加上
Integer pageNo = 1 ;
String pageNoStr = request.getParameter("pageNo");
if(StringUtil.isNotEmpty(pageNoStr)){
pageNo = Integer.parseInt(pageNoStr);
}
HttpSession session = request.getSession() ;
session.setAttribute("pageNo",pageNo);
FruitDAO fruitDAO = new FruitDAOImpl();
List<Fruit> fruitList = fruitDAO.getFruitList(pageNo);
session.setAttribute("fruitList",fruitList);
新需求:尾页在最大页数位置
需要计算最大页数,在DAO中增加一个求总记录条数的方法
然后在IndexServlet中计算总页数
//总记录条数
int fruitCount = fruitDAO.getFruitCount();
//总页数
int pageCount = (fruitCount+5-1)/5 ;
/*
总记录条数 总页数
1 1
5 1
6 2
10 2
11 3
fruitCount (fruitCount+5-1)/5
*/
session.setAttribute("pageCount",pageCount);
新需求:将页数固定在1-最大页数之间,如果超出范围,则按钮不可用
这里就是前端额活了,有disable这么一个属性,为true可以点击,为false不可以点击
前端使用下来,感觉比javaPython这些更抽象,更接近自然语言
<div style="width:60%;margin-left:20%;border:0px solid red;padding-top:4px;" class="center">
<input type="button" value="首 页1" class="btn" th:onclick="|page(1)|" th:disabled="${session.pageNo==1}"/>
<input type="button" value="上一页" class="btn" th:onclick="|page(${session.pageNo-1})|" th:disabled="${session.pageNo==1}"/>
<input type="button" value="下一页" class="btn" th:onclick="|page(${session.pageNo+1})|" th:disabled="${session.pageNo==session.pageCount}"/>
<input type="button" value="尾 页" class="btn" th:onclick="|page(${session.pageCount})|" th:disabled="${session.pageNo==session.pageCount}"/>
</div>
完善水果系统-添加根据关键字查询
新需求:显示一个搜索框,输入搜索内容之后,可以提交搜索内容
用一个表单来充当搜索框
<form th:action="@{/index}" method="post" style="float:left;width:60%;margin-left:20%;">
<input type="hidden" name="oper" value="search"/>
请输入关键字:<input type="text" name="keyword" th:value="${session.keyword}"/>
<input type="submit" value="查询" class="btn"/>
</form>
提交表单之后,就会调用标签为index的servlet
前后端就是这么配合的。前端把数据装到请求里边传到服务器,我服务器这边的servlet捕获到请求,取出数据,然后进行一系列操作。操作的结果可以往那三个容器里边放,HTTPServletRequest,HttpSession,ServletContext,然后跳转到html,html里边有从容器中取出数据的代码
新需求:实现模糊匹配,比如说输入“果”,返回所有与果相关的结果,并分页
这个需求是在servlet中实现,这里的逻辑还是挺复杂的,前后端都要理顺才行
实现了前面那些需求之后,点击首页,上一页,下一页,尾页,添加库存新纪录,查询都会向服务器发起新的请求。他们有啥区别呢??
点击首页,上一页,下一页,尾页不必多说
点击添加库存新纪录,会跳转到edit.html页面
点击查询,无论当前是在哪一页,都需要在查询结果的第一页
或者说从请求中携带的数据这个角度来看,
点击首页,上一页,下一页,尾页,就会将页数传给服务器
点击查询,如果说要用同一个表示页数的变量,那么这里的逻辑必须发生变化。你就必须区分点击的是查询,还是其他位置
要做区分,在java se中,我们只要能找到一个分支条件即可,但在web项目里,如此重要的分支条件其实是由前端给出的,前端在查询相关的html代码处,增加一个隐藏域变量,这个变量会随着请求一起发给服务器。如果说servlet能获取到请求中的隐藏域变量,那就表示点击的是查询
<form th:action="@{/index}" method="post" style="float:left;width:60%;margin-left:20%;">
//隐藏域
<input type="hidden" name="oper" value="search"/>
请输入关键字:<input type="text" name="keyword" th:value="${session.keyword}"/>
<input type="submit" value="查询" class="btn"/>
</form>
现在需要按关键字查询,那么之前那种查询可以兼容吗???当然可以,之前那种查询可以看作是关键字为“”的特殊情况
所以说,不管是点击查询,还是点击其他,都需要捕获这个keyword。因此IndexServlet里边的逻辑需要改为
//Servlet从3.0版本开始支持注解方式的注册
@WebServlet("/index")
public class IndexServlet extends ViewBaseServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
doGet(req,resp);
}
@Override
public void doGet(HttpServletRequest request , HttpServletResponse response)throws IOException, ServletException {
request.setCharacterEncoding("UTF-8");
HttpSession session = request.getSession() ;
Integer pageNo = 1 ;
String oper = request.getParameter("oper");
//如果oper!=null 说明 通过表单的查询按钮点击过来的
//如果oper是空的,说明 不是通过表单的查询按钮点击过来的
String keyword = null ;
if(StringUtil.isNotEmpty(oper) && "search".equals(oper)){
//说明是点击表单查询发送过来的请求
//此时,pageNo应该还原为1 , keyword应该从请求参数中获取
pageNo = 1 ;
keyword = request.getParameter("keyword");
if(StringUtil.isEmpty(keyword)){
keyword = "" ;
}
session.setAttribute("keyword",keyword);
}else{
//说明此处不是点击表单查询发送过来的请求(比如点击下面的上一页下一页或者直接在地址栏输入网址)
//此时keyword应该从session作用域获取
String pageNoStr = request.getParameter("pageNo");
if(StringUtil.isNotEmpty(pageNoStr)){
pageNo = Integer.parseInt(pageNoStr);
}
Object keywordObj = session.getAttribute("keyword");
if(keywordObj!=null){
keyword = (String)keywordObj ;
}else{
keyword = "" ;
}
}
session.setAttribute("pageNo",pageNo);
FruitDAO fruitDAO = new FruitDAOImpl();
List<Fruit> fruitList = fruitDAO.getFruitList(keyword , pageNo);
session.setAttribute("fruitList",fruitList);
//总记录条数
int fruitCount = fruitDAO.getFruitCount(keyword);
//总页数
int pageCount = (fruitCount+5-1)/5 ;
/*
总记录条数 总页数
1 1
5 1
6 2
10 2
11 3
fruitCount (fruitCount+5-1)/5
*/
session.setAttribute("pageCount",pageCount);
//此处的视图名称是 index
//那么thymeleaf会将这个 逻辑视图名称 对应到 物理视图 名称上去
//逻辑视图名称 : index
//物理视图名称 : view-prefix + 逻辑视图名称 + view-suffix
//所以真实的视图名称是: / index .html
super.processTemplate("index",request,response);
}
}
新需求:进行查询之后,即便翻页,查询框也要显示我们查询的内容
其实keyword也不一定需要存到HttpSession中,但是呢,我们希望在index.html页面的查询框一直显示我们查询的内容,这就需要把keyword的值存起来
在index.html的代码中,设置搜索框的默认值
<form th:action="@{/index}" method="post" style="float:left;width:60%;margin-left:20%;">
<input type="hidden" name="oper" value="search"/>
请输入关键字:<input type="text" name="keyword" th:value="${session.keyword}"/>
<input type="submit" value="查询" class="btn"/>
</form>
这一句:
请输入关键字:<input type="text" name="keyword" th:value="${session.keyword}"/>
我们的逻辑是,默认先调用IndexServlet,然后keyword的默认值就被设为了“”,然后调用processTemplate方法跳转到index.html页面
二. MVC优化
MVC全名是Model View Controller,是模型(model)-视图(view)-控制器(controller)的缩写,一种软件设计典范
用一种业务逻辑、数据、界面显示分离的方法组织代码
Model(模型):数据模型,提供要展示的数据,因此包含数据和行为,主要提供了模型数据查询和模型数据的状态更新等功能,包括数据和业务。主要使用的技术:数据模型:实体类(JavaBean),数据访问:JDBC,Hibernate等。
View(视图):负责进行模型的展示,一般就是我们见到的用户界面,比如JSP,Html等
Controller(控制器):接收用户请求,委托给模型进行处理(状态改变),处理完毕后把返回的模型数据返回给视图,由视图负责展示。主要使用的技术:servlet,Struts中的Action类等
MVC是一个框架模式,它强制性的使应用程序的输入、处理和输出分开。使用MVC应用程序被分成三个核心部件:模型、视图、控制器。它们各自处理自己的任务。最典型的MVC就是JSP + servlet + javabean的模式
SSM:Spring+SpringMVC+MyBatis
所以说MVC是一种规范,一种思想。然后呢有一些框架是按照MVC来设计的,你也可以不遵守MVC这种规范
背景
这是我们当目前为止的servlet结构,如果发展下去,可以预想到,servlet的数量会非常庞大,而且很难进行管理
那么按照在java se中的经验,我们是否可以设计这样一个结构
servlet优化1
之前我们讲过servlet的继承树以及核心的几个方法
1. 继承关系
javax.servlet.Servlet接口
javax.servlet.GenericServlet抽象类
javax.servlet.http.HttpServlet抽象子类
XxxServlet子类(对应增删改查)
2. 相关方法
javax.servlet.Servlet接口:
void init(config) - 初始化方法
void service(request,response) - 服务方法
void destory() - 销毁方法
javax.servlet.GenericServlet抽象类:
void service(request,response) - 仍然是抽象的
javax.servlet.http.HttpServlet 抽象子类:
void service(request,response) - 不是抽象的
1. String method = req.getMethod(); 获取请求的方式
2. 各种if判断,根据请求方式不同,决定去调用不同的do方法
if (method.equals("GET")) {
this.doGet(req,resp);
} else if (method.equals("HEAD")) {
this.doHead(req, resp);
} else if (method.equals("POST")) {
this.doPost(req, resp);
} else if (method.equals("PUT")) {
3. 在HttpServlet这个抽象类中,do方法都差不多:
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String protocol = req.getProtocol();
String msg = lStrings.getString("http.method_get_not_supported");
if (protocol.endsWith("1.1")) {
resp.sendError(405, msg);
} else {
resp.sendError(400, msg);
}
}
3.小结:
1) 继承关系: HttpServlet -> GenericServlet -> Servlet
2) Servlet中的核心方法: init() , service() , destroy()
3) 服务方法: 当有请求过来时,service方法会自动响应(其实是tomcat容器调用的)
在HttpServlet中我们会去分析请求的方式:到底是get、post、head还是delete等等
然后再决定调用的是哪个do开头的方法
那么在HttpServlet中这些do方法默认都是405的实现风格-要我们子类去实现对应的方法,否则默认会报405错误
4) 因此,我们在新建Servlet时,我们才会去考虑请求方法,从而决定重写哪个do方法
重写service方法
我一直觉得重写service方法很别扭,因为在HttpServlet这个类中已经实现了service方法,系统在用这个方法,怎么能重写呢???
实际上,之前并不是直接调用了XxxServlet类中重写的doPost方法,而是先调用service方法,然后service方法根据method的值判断应该调用XxxServlet中的哪一个方法。现在重写service方法,使用的是同一个逻辑;并且我们也不需要再重写doGet,doPost之类的方法了
核心思路是这样的:把之前的XxxServlet类都写成方法,让他们都变成FruitServlet类里边的方法,然后在FruitServlet类中重写的service方法调用
判断调用哪一个方法的依据:
和前面实现关键字查询的方法一样,前端会向服务器端回传一个隐藏域变量,通过这个变量来判断调用哪一个方法,用switch case这个结构
程序
假设现在设计一个FruitServlet类,标签为fruit.do,那么所有的.html文件的相关位置都要改为fruit.do,并且都要回传一个隐藏域变量operate
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
request.setCharacterEncoding("UTF-8");
String operate = request.getParameter("operate");
if(StringUtil.isEmpty(operate)){
operate = "index";
}
switch (operate){
case "index":
index(request,response);
break;
case "add":
add(request,response);
break;
case "del":
del(request,response);
break;
case "edit":
edit(request,response);
break;
case "update":
update(request,response);
break;
default:
throw new RuntimeException("operate值非法");
}
}
operate的默认值设为index,也就是说,启动改项目之后,请求发到服务器,先调用service方法,在这个方法中,operate被设为index,这样就调用index方法,然后跳转到index.html页面
还有就是isEmpty方法,如果字符串a = null或者a = “”,那么判定字符串a为空
servlet优化2-反射的引入
在优化1中,我们将多个XxxServlet类改写为了FruitServlet类中的多个方法,并在switch case结构中选择不同的方法
如果FruitServlet类中有100个方法呢,那么switch case不就会特别长吗!!之前有项目一个servlet中有超过5000行的代码,好几百个方法,非常难以管理
所以,引入反射机制来替代switch case这一结构。一句话总结,利用反射机制来调用和operate同名的方法
首先先获取目标类的所有方法名,每次调用service方法,都遍历方法名,找到和operate同名的方法,调用这个方法
//使用反射机制来调用和operate同名的方法
//获取当前类中的所有方法
Method[] methods = this.getClass().getDeclaredMethods();
for(Method m : methods){
//获取方法名称
String methodName = m.getName();
if(operate.equals(methodName)){
try {
//找到和operate同名的方法,通过反射技术调用该方法
m.invoke(this,request,response);
return;
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
} catch (InvocationTargetException e) {
throw new RuntimeException(e);
}
}
}
throw new RuntimeException("operate值非法!");
这样一来,XxxServlet的主体部分代码量就减少了很多,额而且结构清晰
servlet优化3-dispatcherServlet的引入
再进一步增加代码量,增加和FruitServlet同级别的Servlet,为了管理这些Servlet,仿照前面的结构,就再加一层servlet用于管理和FruitServlet同级别的这些Servlet
从代码的层面来说,前面的优化也导致每一个XxxServlet类里边都有重复的反射代码用于确定调用的方法,这些重复代码是否也可以向上抽取
如上图所示,MVC的结构就逐渐显现出来了。引入了一个controller的概念
dispatcherServlet:作为核心控制器
FruitServlet:作为Fruit controller
核心控制器dispatcherServlet怎么知道该选择FruitServlet还是UserServlet呢???
按照以往的经验,这个判断的依据应该是由前端负责的,服务器端可以在请求中获取到这个信息。具体来讲是通过解析url得到的,客户端会向某一个url发起请求,这个url中就携带了访问哪一个控制器的信息,比如说fruit.do或者是user.do等等。dispatcherServlet的标签是*.do,对所有后缀是.do的请求都会有核心控制器dispatcherServlet先进行处理
解析url
//解析字符串url
//假设url是: http://localhost:8080/pro15/hello.do
//那么servletPath是: /hello.do
// 我的思路是:
// 第1步: /hello.do -> hello 或者 /fruit.do -> fruit
// 第2步: hello -> HelloController 或者 fruit -> FruitController
String servletPath = request.getServletPath();//得到 /hello.do
servletPath = servletPath.substring(1);//处理掉斜杠
int lastDotIndex = servletPath.lastIndexOf(".do") ;//处理掉最后的.do
servletPath = servletPath.substring(0,lastDotIndex);
xml配置文件
叫什么可标记扩展语言
解析url之后,我们就得到了一个字符串,比如说,fruit,我们的目的是去调用FruitController类,因此我们需要建立字符串fruit和FruitController类的映射关系
xml文件的标签是除自定义的,包括标签内部的属性也是自定义的
<?xml version="1.0" encoding="utf-8"?>
<beans>
<!-- 这个bean标签的作用是 将来servletpath中涉及的名字对应的是fruit,那么就要FruitController这个类来处理 -->
<bean id="fruit" class="com.atguigu.fruit.controllers.FruitController"/>
</beans>
<!--
1.概念
HTML : 超文本标记语言
XML : 可扩展的标记语言
HTML是XML的一个子集
2.XML包含三个部分:
1) XML声明 , 而且声明这一行代码必须在XML文件的第一行
2) DTD 文档类型定义
3) XML正文
-->
xml配置文件的解析-DOM技术
解析XML配置文件,并最终创建一个map容器,beanMap,的技术就叫做DOM技术
DOM:Document Object Mode,文档对象模型
之前在python的DP项目中做过xml的解析,实际上就是一系列固定的步骤,挺恶心的
在java中解析xml需要用到好几个类,专门用于xml文件的解析工作。DocumentBuilderFactory,DocumentBuilder,Document,NodeList,Node,Element
DocumentBuilderFactory,DocumentBuilder都是抽象类
Document,NodeList,Node,Element都是接口
Document和Element都继承了Node接口
private Map<String,Object> beanMap = new HashMap<>();
InputStream inputStream = getClass().getClassLoader().getResourceAsStream("applicationContext.xml");
//1.创建DocumentBuilderFactory
DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
//2.创建DocumentBuilder对象
DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder() ;
//3.创建Document对象
Document document = documentBuilder.parse(inputStream);
//4.获取所有的bean节点,拿到每一组<bean id="fruit" class="com.atguigu.fruit.controllers.FruitController"/>
NodeList beanNodeList = document.getElementsByTagName("bean");
for(int i = 0 ; i<beanNodeList.getLength() ; i++){
//拿到一个bean标签
Node beanNode = beanNodeList.item(i);
if(beanNode.getNodeType() == Node.ELEMENT_NODE){
//Element有更好的API支持
Element beanElement = (Element)beanNode ;
String beanId = beanElement.getAttribute("id");
String className = beanElement.getAttribute("class");
//最终是希望在map容器中放入字符串id以及对应的实例
Class controllerBeanClass = Class.forName(className);
Object beanObj = controllerBeanClass.newInstance() ;
beanMap.put(beanId , beanObj) ;
}
}
比如说我们想要调用FruitController,那么我们就希望从xml中解析出另个东西存到map容器中,一个是字符串id=fruit,一个是FruitController的实例
当然,从我现在的水平,我还看不出为什么非得要这么做。我听人说这是什么IOC容器的底层实现
从map容器中获取对应的Controller实例
前面我们重写service方法的时候,解析了url,就是说我们知道了前端发起的请求需要哪一个servlet,或者说controller来处理。比如说,需要FruitController来处理请求,解析完url之后,我们就得到了一个字符串fruit,然后我们就可以在beanMap容器中去找对应的Controller实例了,fruit对应的Controller实例是FruitController类的实例
Object controllerBeanObj = beanMap.get(servletPath);
调用Controller中的目标方法
核心控制器这一块的逻辑我认为还是挺复杂的, 但是他的还是很固定的几个步骤,到时候上MVC框架以后,就是一条指令的事儿
当采用了核心控制器+控制器的架构之后,控制器,或者说原来的XxxServlet就变成一个类似于工具类的存在了,里边有处理各种请求的方法
(1)所有类型的请求都汇总到核心控制器dispatcherServlet类
(2)由核心控制器重写service方法
(3)解析请求url,得到一个字符串servletPath,比如fruit,user,book,用于映射到对应的控制器
(4)核心控制器在调用init方法的时候,完成了xml配置文件的解析,得到了一个map容器,里面存了字符串beanID以及这个字符串对应的控制器实例,比如说fruit与FruitController实例,user与UserController实例
(5)以解析url的结果从map容器中取出控制器实例
(6)获取HTTPServletRequest请求中的operate属性的值,这个值用于确定调用控制器实例中的哪一个方法。operate的值比如update,del,edit,index
(7)利用反射机制捕获到控制器的某一个方法,并调用这个方法
在上面的操作步骤中,出现了好几个字符串。其中,我在练习的时候感觉有两个字符串特别容易混淆,一个是解析url的结果servletPath,一个是operate属性的值,但实际上两者的作用完全不同,两者的值天差地别
servletPath:用来充当map容器的key,来确定当前要使用的Controller
operate:用来确定调用Controller的哪一个方法
小bug
之前我们使用thymeleaf渲染的时候,设计了一个叫做ViewBaseServlet类,由ViewBaseServlet去继承HttpServlet,然后我们设计的XxxServlet去继承ViewBaseServlet
现在引入了控制器的概念之后,由谁去继承ViewBaseServlet类呢???
现在控制器已经不再是Servlet组件了,只有核心控制器才是servlet组件,核心控制器DispatcherServlet处理所有的.do请求。
核心控制器只是负责调用控制器,并不实际处理请求,所以DispatcherServlet继承的是HttpServlet,控制器才是实际处理请求的结构,控制器继承的就是ViewBaseServlet类,但是控制器又不是servlet组件
这就导致ViewBaseServlet类,这个父类中的init方法没有被调用。如果控制器还是servlet组件,那么他的父类中的init方法是会被隐式调用的。ViewBaseServlet类中的init方法没有被调用,就导致了一个空指针异常
//这里的*是一个通配符,表示前端以.do结尾的action,都要调用DispatcherServlet
@WebServlet("*.do")
public class DispatcherServlet extends HttpServlet {
private Map<String,Object> beanMap = new HashMap<>();
public DispatcherServlet(){
}
public void init(){
try {
InputStream inputStream = getClass().getClassLoader().getResourceAsStream("applicationContext.xml");
//1.创建DocumentBuilderFactory
DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
//2.创建DocumentBuilder对象
DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder() ;
//3.创建Document对象
Document document = documentBuilder.parse(inputStream);
//4.获取所有的bean节点
NodeList beanNodeList = document.getElementsByTagName("bean");
for(int i = 0 ; i<beanNodeList.getLength() ; i++){
Node beanNode = beanNodeList.item(i);
if(beanNode.getNodeType() == Node.ELEMENT_NODE){
Element beanElement = (Element)beanNode ;
String beanId = beanElement.getAttribute("id");
String className = beanElement.getAttribute("class");
Class controllerBeanClass = Class.forName(className);
Object beanObj = controllerBeanClass.newInstance() ;
Method setServletContextMethod = controllerBeanClass.getDeclaredMethod("setServletContext",ServletContext.class);
setServletContextMethod.invoke(beanObj , this.getServletContext());
beanMap.put(beanId , beanObj) ;
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//设置编码
request.setCharacterEncoding("UTF-8");
//解析字符串url
//假设url是: http://localhost:8080/pro15/hello.do
//那么servletPath是: /hello.do
// 我的思路是:
// 第1步: /hello.do -> hello 或者 /fruit.do -> fruit
// 第2步: hello -> HelloController 或者 fruit -> FruitController
String servletPath = request.getServletPath();
servletPath = servletPath.substring(1);//处理掉斜杠
int lastDotIndex = servletPath.lastIndexOf(".do") ;//处理掉最后的.do
servletPath = servletPath.substring(0,lastDotIndex);
Object controllerBeanObj = beanMap.get(servletPath);
String operate = request.getParameter("operate");
if(StringUtil.isEmpty(operate)){
operate = "index" ;
}
try {
Method method = controllerBeanObj.getClass().getDeclaredMethod(operate,HttpServletRequest.class,HttpServletResponse.class);
if(method!=null){
method.setAccessible(true);
method.invoke(controllerBeanObj,request,response);
}else{
throw new RuntimeException("operate值非法!");
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
控制器里边都是些私有方法,使用反射调用私有方法,需要method.setAccessible(true);
servlet优化4-提取视图资源处理通用代码
这个视图,view,我一直觉得特别抽象。视图??
视图资源处理都是由各个controller的各个方法来实现的
这里的优化集中在两点:
(1)从HTTPServletRequest请求中提取参数
(2)服务器端转发或者客户端重定向
一种新的客户端重定向方式
以FruitController中的update方法为例,原来的客户端重定向方式是在update方法当中显式地调用sendRedirect方法
response.sendRedirect("fruit.do");
现在,由核心控制器来实现客户端重定向,控制器的update方法返回一个字符串
return "redirect:fruit.do";
那么核心控制器的职能其实就扩大了,不只是调度控制器了。在核心控制器中解析update方法返回的字符串,也就是拿到fruit.do这个servlet组件标签
//2.controller组件中的方法调用
method.setAccessible(true);
Object returnObj = method.invoke(controllerBeanObj,parameterValues);
//3.视图处理
String methodReturnStr = (String)returnObj ;
if(methodReturnStr.startsWith("redirect:")){ //比如: redirect:fruit.do
String redirectStr = methodReturnStr.substring("redirect:".length());
response.sendRedirect(redirectStr);
}
}
一种新的thymeleaf跳转
和上面一样,FruitController中有一个edit方法,原本是在方法最后调用父类ViewBaseServlet中的processTemplate方法,现在edit方法返回一个叫做edit的字符串,跳转到edit.html页面上
也就是从这里开始,核心控制器继承的不再是HttpServlet,而是ViewBaseServlet,这样核心控制器内部才可以调用processTemplate方法
//2.controller组件中的方法调用
method.setAccessible(true);
Object returnObj = method.invoke(controllerBeanObj,parameterValues);
//3.视图处理
String methodReturnStr = (String)returnObj ;
if(methodReturnStr.startsWith("redirect:")){ //比如: redirect:fruit.do
String redirectStr = methodReturnStr.substring("redirect:".length());
response.sendRedirect(redirectStr);
}else{
super.processTemplate(methodReturnStr,request,response); // 比如: "edit"
}
因为客户端重定向以及跳转的实现由核心控制器负责了,那么控制器中各个方法的形参也就不再需要HTTPServletResponse响应
thymeleaf的初始化
前面说了,核心控制器不在继承HttpServlet,而是继承ViewBaseServlet,而ViewBaseServlet需要调用init方法
因此,直接在DispatcherServlet重写的init方法中,先调用父类的init方法
servlet优化5-在核心控制器中统一获取参数以及进行视图处理
核心控制器的作用
经过前面的优化,我们发现核心控制器的职能被扩大了:
(1)负责调度控制器
(2)负责执行通用操作
(3)因为要做和thymeleaf相关的操作,继承ViewBaseServlet
private void update(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//1.设置编码
request.setCharacterEncoding("utf-8");
//2.获取参数
String fidStr = request.getParameter("fid");
Integer fid = Integer.parseInt(fidStr);
String fname = request.getParameter("fname");
String priceStr = request.getParameter("price");
int price = Integer.parseInt(priceStr);
String fcountStr = request.getParameter("fcount");
Integer fcount = Integer.parseInt(fcountStr);
String remark = request.getParameter("remark");
//3.执行更新
fruitDAO.updateFruit(new Fruit(fid,fname, price ,fcount ,remark ));
//4.资源跳转
//super.processTemplate("index",request,response);
//request.getRequestDispatcher("index.html").forward(request,response);
//此处需要重定向,目的是重新给IndexServlet发请求,重新获取furitList,然后覆盖到session中,这样index.html页面上显示的session中的数据才是最新的
response.sendRedirect("fruit.do");
}
//修改以后
private String update(Integer fid , String fname , Integer price , Integer fcount , String remark ){
//3.执行更新
fruitDAO.updateFruit(new Fruit(fid,fname, price ,fcount ,remark ));
//4.资源跳转
return "redirect:fruit.do";
}
可以明显地看到,不再由控制器的方法自己去获取参数,而是中央控制器在调用的时候提供实参
还是大量地在使用反射机制,这里为了获取类中方法的形参名,需要做一个设置。在file/setting/builder/compiler/java compiler/additional command line parameters处加上-parameters,这表示在编译生成的.class字节码文件中会加上每个方法的形参名
还有一点我之前都没有注意到,从HttpServletRequest实例中取到的数据全都是String类型的,和input方法一样
从代码中可以看到,HttpServletRequest实例调用getParameter方法的返回值都是String类型
String fidStr = request.getParameter("fid");
Integer fid = Integer.parseInt(fidStr);
String fname = request.getParameter("fname");
String priceStr = request.getParameter("price");
int price = Integer.parseInt(priceStr);
String fcountStr = request.getParameter("fcount");
Integer fcount = Integer.parseInt(fcountStr);
String remark = request.getParameter("remark");
在核心控制器中完成从HttpServletRequest实例中获取实参argument的代码
实际体验下来,真的需要特别熟悉反射
try {
Method[] methods = controllerBeanObj.getClass().getDeclaredMethods();
for(Method method : methods){
if(operate.equals(method.getName())){
//1.统一获取请求参数
//1-1.获取当前方法的参数,返回参数数组,比如说fid,fname,price,fcount,remark
Parameter[] parameters = method.getParameters();
//1-2.parameterValues 用来承载参数的值
Object[] parameterValues = new Object[parameters.length];
for (int i = 0; i < parameters.length; i++) {
Parameter parameter = parameters[i];
//取到了一个形参名,比如说fid
String parameterName = parameter.getName() ;
//如果参数名是request,response,session 那么就不是通过请求中获取参数的方式了
if("request".equals(parameterName)){
//我以为request是字符串,看错了,这里的request是一个对象,传到
//重写的service方法的一个参数HttpServletRequest request
parameterValues[i] = request ;
}else if("response".equals(parameterName)){
parameterValues[i] = response ;
}else if("session".equals(parameterName)){
parameterValues[i] = request.getSession() ;
}else{
//从请求中获取参数值
String parameterValue = request.getParameter(parameterName);
parameterValues[i] = parameterValue ;
}
}
//2.controller组件中的方法调用
method.setAccessible(true);
Object returnObj = method.invoke(controllerBeanObj,parameterValues);
//3.视图处理
String methodReturnStr = (String)returnObj ;
if(methodReturnStr.startsWith("redirect:")){ //比如: redirect:fruit.do
String redirectStr = methodReturnStr.substring("redirect:".length());
response.sendRedirect(redirectStr);
}else{
super.processTemplate(methodReturnStr,request,response); // 比如: "edit"
}
}
}
/*
}else{
throw new RuntimeException("operate值非法!");
}
*/
}
一个bug
首页没有问题,但是点击下一页报错,说是实参类型不匹配,IllegalArgumentException: argument type mismatch
其原因在于,在上面的代码中,我们从HttpServletRequest实例中取到的数据都是String类型,然后将他们作为实参传给了控制器的某个方法。但是这个方法需要的实参并不都是String
//原版
//从请求中获取参数值
String parameterValue = request.getParameter(parameterName);
parameterValues[i] = parameterValue ;
还是使用反射机制,获得每个形参的类型parameter.getType().getName(),依据类型进行强转。又一次说明反射机制的重要性
//从请求中获取参数值
String parameterValue = request.getParameter(parameterName);
String typeName = parameter.getType().getName();
Object parameterObj = parameterValue ;
if(parameterObj!=null) {
if ("java.lang.Integer".equals(typeName)) {
parameterObj = Integer.parseInt(parameterValue);
}
}
parameterValues[i] = parameterObj ;
总结
控制器和中央控制器的作用
经过前面的一系列优化,控制器只负责业务相关的特有操作,而那些通用的处理,比如获取参数,跳转等,都有中央控制器负责
反射机制中的invoke,有“调用、挑取、启用、引动”之意,Method实例调用invoke函数就相当于说在调用方法。invoke有个很直接的意思是援引
- 最初的做法是: 一个请求对应一个Servlet,这样存在的问题是servlet太多了
- 把一些列的请求都对应一个Servlet, IndexServlet/AddServlet/EditServlet/DelServlet/UpdateServlet -> 合并成FruitServlet
通过一个operate的值来决定调用FruitServlet中的哪一个方法
使用的是switch-case - 在上一个版本中,Servlet中充斥着大量的switch-case,试想一下,随着我们的项目的业务规模扩大,那么会有很多的Servlet,也就意味着会有很多的switch-case,这是一种代码冗余
因此,我们在servlet中使用了反射技术,我们规定operate的值和方法名一致,那么接收到operate的值是什么就表明我们需要调用对应的方法进行响应,如果找不到对应的方法,则抛异常 - 在上一个版本中我们使用了反射技术,但是其实还是存在一定的问题:每一个servlet中都有类似的反射技术的代码。因此继续抽取,设计了中央控制器类:DispatcherServlet
DispatcherServlet这个类的工作分为两大部分:
1.根据url定位到能够处理这个请求的controller组件:
1)从url中提取servletPath : /fruit.do -> fruit
2)根据fruit找到对应的组件:FruitController , 这个对应的依据我们存储在applicationContext.xml中
<bean id=“fruit” class="com.atguigu.fruit.controllers.FruitController/>
通过DOM技术我们去解析XML文件,在中央控制器中形成一个beanMap容器,用来存放所有的Controller组件
3)根据获取到的operate的值定位到我们FruitController中需要调用的方法
2.调用Controller组件中的方法:- 获取参数
获取即将要调用的方法的参数签名信息: Parameter[] parameters = method.getParameters();
通过parameter.getName()获取参数的名称;
准备了Object[] parameterValues 这个数组用来存放对应参数的参数值
另外,我们需要考虑参数的类型问题,需要做类型转化的工作。通过parameter.getType()获取参数的类型 - 执行方法
Object returnObj = method.invoke(controllerBean , parameterValues); - 视图处理
String returnStr = (String)returnObj;
if(returnStr.startWith(“redirect:”)){
…
}else if…
- 获取参数
中央控制器
中央控制器DispatcherServlet重写了两个方法:
(1)重写init方法,解析applicationContext.xml配置文件,创建包含键值对id+对应类实例的beanMap容器
(2)重写service方法,解析url确定调用控制器的哪一个方法,统一获取参数,视图处理