水果系统基础版-多Servlet版本
现在我们需要给页面添加几个功能:
- 点击水果的名字能查看水果的详情,然后可以修改水果的信息。
- 点击 添加新库存记录时可以添加新纪录。
- 点击x操作时,可以将响应水果的记录删除。
编辑和修改功能
将html文件中关于水果名字的那一行这么写:
<td><a th:text="${fruit.fname}" th:href="@{/edit.do(fid=${fruit.fid})}">苹果</a></td>
有点难理解,这么说,a标签是超链接标签,前面th:text就是添加超链接上去的地方,也就是水果的名字嘛,然后href是超链接跳转到的链接。
因为也是Thymeleaf,所以也要在前面加th:
然后这个超链接里面的东西就关键了。
th:href="@{/edit.do(fid=${fruit.fid})}",这个/edit.do是什么?其实这就是我们之前说的标识符(定位符),是来定位一个servlet,肯定在代码中有个servlet的标记符是edit.do.
是的,EditServlet的标记符就是edit.do.
th:href="@{/edit.do(fid=${fruit.fid})}",而写@,写/ 这些是将它写成绝对路径.
而edit.do后面的括号又是什么意思呢?
这么说吧,我们超链接时,不就相当于请求一个页面(或者servlet),反正也就是发起一个请求,那么我们在发请求时是可以顺便发送一些参数的.
所以我们就在发送请求的同时也发送了fid=${fruit.fid},也就是将此时的fruit.fid在request一起发送过去.
发送的时候确实就是发送字符串过去,会发送fid=2或者fid=3过去
而这个2和3就要从${fruti.fid}得来,这个跟linux中的取变量很相似.
这里的逻辑就是当我们点击水果的名字后,就会发送一个请求给editServlet,并同时发送一个fid=x过去.
EditServlet
那么我们来看看EditServlet的代码:
@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);
}
}
}
request.getParameter
首先看这一句: String fidStr = request.getParameter(“fid”);
我们发送过来时,请求后面会加上一个fid=x,所以我们的request可以通过request.getParameter方法,获取传过来的参数的值.(而且是内部重定位),所以这个request的Attribute是可以拿得到的.
而下面的那句 super.processTemplate(“edit”,request,response)我们刚也分析了,因为前缀后缀都已经加了,所以意思就是跳转到/edit.html
edit.html
那么我们现在来看一下edit.html文件内写了啥:
先看看需要实现什么功能:
比如我们点击了红富士,然后就给我们显示红富士的详情信息,而且也可以修改.
所以我们即可显示信息,又可以修改,我们就明确应该是input输入框,而且待会点击修改时需要提交这些信息,所以应该外面的框架应该是个表单form.
比如我们看名称那一行,空格里的"红富士"的信息应该得从刚传过来的fruit中获取.
记得我们刚刚前面index的html中获取的代码是这样:
<tr th:unless="${#lists.isEmpty(session.fruitList)}" th:each="fruit : ${session.fruitList}">
通过session.fruitList,那是因为保存作用域是session.
而如果保存作用域是request.则不需要session,直接写出fruit.name就可以.
<!-- <td><input type="text" name="fname" th:value="${fruit.fname}"/></td> -->
而且还可以更简洁,不需要写.fname
<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>
直接在最外的一层的标签上写上
<form th:action="@{/update.do}" method="post" th:object="${fruit}">
意思就是在这个层下面的都是以fruit作为object类的
而下面需要引用时就直接这样就行喽.
<td><input type="text" name="fname" th:value="*{fname}"/></td>
源代码
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>
点击修改功能
当我们点击修改功能时,要进行修改操作.
<form th:action="@{/update.do}" method="post" th:object="${fruit}">
首先看这个action是什么意思?
定义和用法:
必需的 action 属性规定当提交表单时,向何处发送表单数据。
也就是如果是表单的话,action后面的值就指明了要将表单发往的地方,后面也就是我们说用绝对路径表示的一个Servlet,所以就有一个sevlect的标识符为update.do
然后我们将表单发送过去,就是将下面的
<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>
所以表单的意思就是我们发送请求时,在后面会加上比如 fanme=x,price=x,fcount=x,remark=x。
我们前面讲过可以在发地址的后面手动加上一个括号表示要传的参数:th:href="@{/edit.do(fid=${fruit.fid})}"
如果不嫌麻烦,也可以一个个写,但是表单就是以更方法的形式实现,但是内部的原理都是在发送的请求后面写个括号加上很多参数。
所以在接收方那边同样用request.getParameter方法来接受。
刚说了有一个类的标识符为update.do,那么到底有没有呢?我们来看一下。
@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");
}
}
- 首先注意一点,form表单在接受的时候,要记得设置编码,否则可能会乱码。
- 从修改的页面获取修改的参数传过来交给数据库,让其执行修改(更新)功能。
- 资源跳转这里需要注意以下,如果是原本的//super.processTemplate(“index”,request,response);其实这一句的意思只是重新定向到index.html页面,但是html页面内的fruitList还是上个request的session保存的,还没有执行更新。
- 所以需要写成这样: response.sendRedirect(“index”);也就是让客户端重新请求indexServlet,让其重新更新保存域中的fruitList,然后再重新请求index.html.
然后我们又发现了一个点,就是我们的修改页面需要传一个fid过去让servlet判断,但是页面上并没有写,因为用户并不需要知道fid,写出来太难看,这时候可以怎么办?
1.可以像之前一样在请求后面的括号加个(fid=x)
2.如果是表单,我们还可以加一个隐藏域
<input type="hidden" name="fid" th:value="*{fid}"/>
只有type是hidden,我们就看不到,但是传表单数据的时候还是能传过去。
记得下面的提交时的按钮
<input type="submit" value="修改" />
源代码:
<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>
下面整理以下逻辑:
-
点击水果名字超链接的时候就请求editServlet来响应(同时传过去水果id)
-
editServlet获取请求中的水果id,然后通过水果id获取该水果的数据,然后打开一个关于该水果信息的页面)(用的thy跳转功能,所以是有该页面有通过thy渲染)
-
在修改页面修改,修改后点击提交按钮后,传给updateServlet处理。
-
updeateServlet接收后,更新数据库并且请求重定向,重定向请求给indexServlet
-
indexServlet从更新的数据库取到数据,并通过渲染技术打开index.html的页面。
添加功能和这个大同小异。
分页功能
如上图。
接下来是进阶功能-分页功能。
首先,先明确一下,我们之前查询数据库的时候都是直接全部给查出来,也就是传一个FruitList的话是传所有的Fruit,但是我们现在要实现分页,也就是每次查几条几条,怎么实现呢?
1.当然最底层的还是在sql的查询语句中,之前的语句是全部查询,现在需要改一下,Select*from t_fruit Limit a,b,意思就是从a开始,然后每次查询b条。
如下图
所以当我们想5页,5页分页时,也就是每次查询5条时,应该怎么写呢?
SELECT*FROM t_fruit LIMIT (pageNo-1)*5 ,5
这个pageNo就是我们要传进来的页码值。
public List<Fruit> getFruitList( Integer pageNo) {
return super.executeQuery("select * from t_fruit limit ? , 5" , (pageNo-1)*5);
}
那么我们现在知道了,在indexServlet执行的查询数据库时,应该也给一个查询的页码。
先回到html页面吧。
首页,尾页:
如图,我们要设置,点击首页时,回到首页,点击尾页时,回到尾页。怎么实现呢?这个其实很简单,点击首页,就是传一个页码为1给servlet,点击尾页就是传一个总页数(尾页)给servlet,让它去数据库中查询。
传1容易,但是总页数这个怎么办呢?这个是实时变化的。总页数和总记录条数友关系,其实我们可以在indexServlet那边写出计算公式,然后传给index.html
//总记录条数
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);
<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.pageCount})|" th:disabled="${session.pageNo==session.pageCount}"/> (这里的竖线可能是要表示page()内的值是一个表达式的值。
先看懂那个onclick那个就行,那是js函数,我们回js函数中
function page(pageNo){
window.location.href="index?pageNo="+pageNo;
}
这句话的意思是:
传入一个pageNo,然后会在当前的地址栏发请求给index,附赠pageNo=x?
简而言之,就是当我们onlick,点击的时候,该按钮就会向index(标记符为index的servlet)发请求,同时带上pageNo的参数。
所以indexServlet那边就会收到pageNo的参数,然后重新请求数据库,重新得到数据,重新将数据和index.html页面渲染并显示html页面,在我们看来就是实现了换页功能。
上一页,下一页:
当点击上一页,下一页时,我们必须得先获取此时的页数,然后我们在html这边在将当前页数减1然后再传给servlet。
所以servlet那边也要有保存此时页数的作用域,这边才能调用。
HttpSession session = request.getSession() ;
session.setAttribute("pageNo",pageNo);
<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}"/>
然后现在在讲讲后面那些什么disable是什么意思?
disabled也就是让这个按键有没有效的意思:
首页和第一页的按键(在当时页面为首页时)应该为无效。
th:disabled="${session.pageNo==1}"/这句话的意思就是:
如果pageNo1,那么就返回ture,那么disabled就true,那么就无效。
尾页的逻辑也是如此。
所以整个源代码是这样:
<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>
function page(pageNo){
window.location.href="index?pageNo="+pageNo;
}
js代码
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);
//总记录条数
int fruitCount = fruitDAO.getFruitCount();
//总页数
int pageCount = (fruitCount+5-1)/5 ;
session.setAttribute("pageCount",pageCount);
super.processTemplate("index",request,response);
}
}
逻辑总结如下:
- indexServlet默认当前页数为1,并且查出当前总页数。然后存入保存作用域中。然后跳转到index.html页面。
- 当点击上一页,下一页,首页,尾页时,会根据保存作用域中的当前页数和总页数进行运算,然后当点击时,就将计算出的需要新的展示的页数传给servlet。(请求servlet的代码在javascript中)
- servlet接收到传过来的参数(页数),并且请求数据库重新查找该页数对应的记录条数,然后再保存作用域,然后再重新渲染页面请求页面。
上面的逻辑判断都是在html中,也就是说各种对页数的判断都是在html中进行的,其实也可以不那么复杂,点击html页面时只需要根据点击按键的不同给servlet传一些不同的参数(比如0表示首页,1表示下一页,-1表示上一页等),然后sevlet这边再根据接收到的参数然后做出相应的页码计算就好了。
根据关键字查询功能
这个逻辑还是有点难理解的:
首先要在html页面上
添加一个可以输入的查询关键字,然后点击查询关键字后就能查询相关的记录。所以在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="hidden" name="oper" value="search"/>
为什么呢?因为在sevlet要传入一个判断。
这么说吧,如果我们现在点击了查询了之后,那么就得显示新的关键字的记录,然后对这些记录进行分页。
也就是说如果我们现在以果为关键字进行查询,然后查询出的记录有很多页,那么我们点击下一页,上一页时,得保存当前关键字的页。
因为无论是传关键字还是传上一页下一页本质上都是给servlet发送表单请求。(防止现在是关键字查询中的页,然后当你点击下一页时,获取到的keyword关键字为空)不理解也没事,继续看下面的代码吧。
servlet内的代码:
首先,因为刚表单都是post,所以我们必须实现dopost方法,我们可以直接在doPost中引用doGet就行。
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
doGet(req,resp);
}
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)){
`1pageNo = 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);
逻辑是这样:
- 如果在页面那边是按了关键字查询的,我们就会同时发送一个隐藏域oper。
- 所以我们通过判断oper是否为空来判断是否为点击了关键字查询
- 如果是的话,那么我们要将当前页面设置为1(查询关键字后从第一页开始展示),然后同时将当前的keyword保存下来
- 如果不是的话,那么就说明是其它方式的post方法,比如说点击上下一页,如果我们查看一下页面,看一下传过来的页数,那么接下来就关键了,为什么刚刚要保存一个keyword,因为刚刚保存的keyword可以让我们取出,然后根据keyword和page传到数据库中查到相应记录。
- 查询到记录并传入thymeleaf中渲染后显示页面
这么说吧,前面的逻辑想要实现的事就是,当你输入关键词搜索时,能搜索到关键词列表,并显示这些关键词列表的第一页。
然后比如在这个时候,你点击了下一页按钮,那么是针对当前关键词的,所以你就得保存一下上一次点击关键词搜索时的keyword。
所以实现的功能就是:
点击关键词搜索,然后显示根据该关键词的列,且你点击上一页,下一页,也是根据该关键词的。
而如果要重新查找所有,就必须把将查询栏设空,然后点击查询。
当然了,取fruitList时的代码得换成
List<Fruit> fruitList = fruitDAO.getFruitList(keyword , pageNo);
sql查询语句也得相应改一下:
public List<Fruit> getFruitList(String keyword , Integer pageNo) {
return super.executeQuery("select * from t_fruit where fname like ? or remark like ? limit ? , 5" ,"%"+keyword+"%","%"+keyword+"%", (pageNo-1)*5);
}