本系列文章旨在记录和总结自己在Java Web开发之路上的知识点、经验、问题和思考,希望能帮助更多码农和想成为码农的人。
本文转发自头条号【普通的码农】的文章,大家可以关注一下,直接在今日头条的移动端APP中阅读。因为平台不同,会出现有些格式、图片、链接无效方面的问题,我尽量保持一致。
介绍
上篇文章使用Servlet技术实现了一个简单的租房网,它仅仅实现了登录页面,以及登录后向用户推送感兴趣的房源列表。
本篇文章继续实现房源详细信息页面,以及对房源信息的编辑功能。
House实体类
首先,之前房源列表页面中的房源信息都是直接写在HTML页面中的,现在把它抽出来用一个实体类来存储:
package houserenter.entity;
public class House {
private String id;
private String name;
private String detail;
public House(String id, String name, String detail) {
super();
this.id = id;
this.name = name;
this.detail = detail;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getDetail() {
return detail;
}
public void setDetail(String detail) {
this.detail = detail;
}
@Override
public String toString() {
return "House [id=" + id + ", name=" + name + ", detail=" + detail + "]";
}
}
House实体类只有三个字段(为演示而简化),构造方法、getter方法、setter方法和toString()方法都可以用Eclipse中源代码文件右键菜单中的Source -> Generate Constructor using Fields…,Generate Getters and Setters… 和Generate toString()…工具自动生成。
OK,这三个字段都没什么好解释的,需要提一点就是,一般情况下,都要为实体附加一个id属性。
优化模拟的房源数据
现在需要把原来直接写死在HTML页面中的模拟房源数据优化一下,这里利用Servlet的init()方法,它是在Servlet的第一个(仅仅是第一个,即只执行一次)请求到来之时先执行的,然后再执行Servlet的service()方法。
HouseServlet添加如下代码:
private List<House> mockHouses;
@Override
public void init() {
mockHouses = new ArrayList<House>();
mockHouses.add(new House("1", "金科嘉苑3-2-1201", "详细信息"));
mockHouses.add(new House("2", "万科橙9-1-501", "详细信息"));
}
添加了一个私有属性mockHouses列表来存储假的房源数据,然后在init()方法中填充假的房源数据。
然后修改doGet()方法中模拟房源数据有关的部分:
writer.println("<h6>共找到你感兴趣的房源 "+mockHouses.size()+" 条</h6>");
writer.println("<ul>");
for (House house : mockHouses) {
writer.println("<li><h2><a>"+house.getName()+"</a></h2></li>");
}
writer.println("</ul>");
我这里用了HTML的无序列表标签<ul>
和<li>
,这很符合房源列表的语义啊!
OK,这样看起来就清爽多了,运行验证一下,没什么问题。
不过,Servlet的init()方法一般是用来初始化一些资源的。
房源详细信息页面
先想象一下用户的操作流程,一般来说,用户会在房源列表页面中寻找更感兴趣的某个房源,然后点击那一项进入该房源的详细信息页面。
所以,我们应该在上面的每一项房源中都加上链接标签<a>
,不过上面已经加上了,差的是没有添加链接标签的href属性。
那href属性的值应该是什么呢?上篇文章我设计的方案是有关房源的请求都是交给HouseServlet来处理的,所以它的值应该就是HouseServlet配置的“house.html”。
如果是这样的话,那么房源列表页面和房源详细信息页面的请求都将触发执行HouseServlet的doGet()方法,那doGet()方法该如何区分这两种情况呢?
上篇文章我们使用了URL重写技术来跟踪会话,同样,我们在这里仍然可以使用它,只要将链接标签的href属性值附加房源ID,然后在doGet()方法中通过URL中是否存在房源ID参数来判断是返回房源列表页面,还是返回某房源的详细信息页面。
当进入某房源的详细页面之后,用户往往需要再次回到之前的房源列表页面,所以我们还需要一个回到列表的链接,而这个链接的href属性值就不需要houseId参数了。
不过,这里的所有链接的href属性值都需要带有userName参数,因为这是用户登录后的操作,我们需要用它来判断用户是否登录。
于是,我们的doGet()方法就可以改成下面这个样子:
String userName = request.getParameter("userName");
if (userName == null || userName.isEmpty()) {
System.out.println("invalid user!");
response.sendRedirect("login.html");
}
response.setCharacterEncoding("UTF-8");
PrintWriter writer = response.getWriter();
writer.println("<!DOCTYPE html>");
writer.println("<html>");
writer.println("<head>");
writer.println("<meta charset=\"UTF-8\">");
writer.println("<title>租房网</title>");
writer.println("</head>");
writer.println("<body>");
writer.println("<h1>你好,"+userName+"!欢迎来到租房网! <a href=\"login.html\">退出</a></h1>");
writer.println("<br><br>");
String houseId = request.getParameter("houseId");
if (houseId == null || houseId.isEmpty()) {
//查找该用户感兴趣的房源,这里省略
System.out.println("userName: " + userName + " access house.html!");
writer.println("<h6>共找到你感兴趣的房源 "+mockHouses.size()+" 条</h6>");
writer.println("<ul>");
for (House house : mockHouses) {
writer.println("<li><h2><a href=\"house.html?userName="+userName+"&houseId="+house.getId()+"\">"+house.getName()+"</a></h2></li>");
}
writer.println("</ul>");
} else {
//根据houseId查找该房源的详细信息
System.out.println("userName: " + userName + " access house.html for house detail!");
House target = null;
for (House house : mockHouses) {
if (houseId.equals(house.getId())) {
target = house;
break;
}
}
writer.println("<h2>"+target.getName()+"</h2>");
writer.println("<h3>"+target.getDetail()+"</h3>");
writer.println("<h4><a href=\"house.html?userName="+userName+"\">回到列表</a></h4>");
}
writer.println("</body>");
writer.println("</html>");
同时,把填充HTML共同的部分放到条件判断之外。
至此,房源详细信息页面也实现完毕,运行验证一下,应该没有什么问题。
房源编辑功能
房源编辑是特定用户(往往是租房中介的业务人员)才可以看到和执行的操作,不过,我在这里就不进行权限的验证了。
首先,用户每次只能编辑一个房源信息,然后需要一个编辑某房源的点击入口,然后给用户返回该房源的编辑页面,这个页面当然是一个表单,不过填充的是当前的房源信息。
编辑入口比较合理的设计是直接在房源详细信息页面的房源名字之后加一个编辑该房源的链接。那么这个编辑链接的href属性值又该是什么呢?
我们之前的设计是所有房源相关的请求都是交给HouseServlet来处理,所以编辑链接的href属性值仍然是house.html。
如果是这样的话,又跟上面一样,出现了房源列表页面、房源详细信息页面、房源编辑页面的请求都将触发执行HouseServlet的doGet()方法,现在doGet()方法又该如何区分这三种情况呢?
前面使用houseId参数来区分房源列表页面和房源详细信息页面,现在房源编辑页面肯定仍然需要houseId参数,我们可以再加一个参数来进一步区分房源详细信息页面和房源编辑页面,姑且命名为editHouse,其值可以是任意的。于是,房源详细信息页面中内容变为:
writer.println("<h2>"+target.getName()+"<a href=\"house.html?userName="+userName+"&houseId="+houseId+"&editHouse=true\">编辑</a></h2>");
writer.println("<h3>"+target.getDetail()+"</h3>");
writer.println("<h4><a href=\"house.html?userName="+userName+"\">回到列表</a></h4>");
只有第一行添加了一个编辑链接。
然后,我们需要获取editHouse参数,并且条件判断需要再添加一个判断editHouse参数的分支,返回房源编辑页面的分支是一个表单:
String houseId = request.getParameter("houseId");
String editHouse = request.getParameter("editHouse");
if (houseId == null || houseId.isEmpty()) {
//查找该用户感兴趣的房源,这里省略
System.out.println("userName: " + userName + " access house.html!");
writer.println("<h6>共找到你感兴趣的房源 "+mockHouses.size()+" 条</h6>");
writer.println("<ul>");
for (House house : mockHouses) {
writer.println("<li><h2><a href=\"house.html?userName="+userName+"&houseId="+house.getId()+"\">"+house.getName()+"</a></h2></li>");
}
writer.println("</ul>");
} else if (editHouse == null) {
//根据houseId查找该房源的详细信息
System.out.println("userName: " + userName + " access house.html for house detail!");
House target = null;
for (House house : mockHouses) {
if (houseId.equals(house.getId())) {
target = house;
break;
}
}
writer.println("<h2>"+target.getName()+"<a href=\"house.html?userName="+userName+"&houseId="+houseId+"&editHouse=true\">编辑</a></h2>");
writer.println("<h3>"+target.getDetail()+"</h3>");
writer.println("<h4><a href=\"house.html?userName="+userName+"\">回到列表</a></h4>");
} else {
//存在editHouse参数,返回指定房源的编辑页面
System.out.println("userName: " + userName + " access house.html to edit house!");
House target = null;
for (House house : mockHouses) {
if (houseId.equals(house.getId())) {
target = house;
break;
}
}
writer.println("<form action=\"house.html?userName="+userName+"&houseId="+houseId+"\" method=\"post\">");
writer.println("<label for=\"house_name\">房源名字:</label><input type=\"text\" id=\"house_name\" name=\"houseName\" value=\""+target.getName()+"\" />");
writer.println("<label for=\"house_detail\">房源详细信息:</label><input type=\"text\" id=\"house_detail\" name=\"houseDetail\" value=\""+target.getDetail()+"\" />");
writer.println("<input type=\"submit\" value=\"提交\" />");
writer.println("</form>");
}
注意,房源编辑页面中的表单,其action属性值仍然是HouseServlet配置的URL,即房源编辑表单的提交仍然交给HouseServlet来处理,不过,其method属性值是post,所以会触发执行doPost方法。
当然,提交后该返回哪个页面呢?比较合理的设计是返回该房源的详细信息页面,所以,action属性值仍然带有userName和houseId参数。不过,我们可以采用另外一个会话跟踪技术,就是表单的隐藏域,将上述代码中输出表单的第一行改为下面的第一行,然后添加两个<input>
标签,不过它们的type是hidden,即浏览器是不会把它们展示出来的,但提交的时候仍然将它们的数据送给服务端:
writer.println("<form action=\"house.html\" method=\"post\">");
writer.println("<input type=\"hidden\" name=\"userName\" value=\""+userName+"\"/>");
writer.println("<input type=\"hidden\" name=\"houseId\" value=\""+houseId+"\"/>");
然后,我们剩下的就是实现doPost方法:
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String userName = request.getParameter("userName");
if (userName == null || userName.isEmpty()) {
System.out.println("invalid user!");
response.sendRedirect("login.html");
}
String houseId = request.getParameter("houseId");
//获取提交的房源信息,并保存
System.out.println("userName: " + userName + " access house.html to save house detail!");
String houseName = request.getParameter("houseName");
String houseDetail = request.getParameter("houseDetail");
for (House house : mockHouses) {
if (houseId.equals(house.getId())) {
house.setName(houseName);
house.setDetail(houseDetail);
break;
}
}
response.sendRedirect("house.html?userName="+userName+"&houseId="+houseId);
}
doPost()方法仍然需要先验证用户登录情况,然后获取到提交的房源信息,并保存。
最后重定向到该房源的详细信息页面。
中文乱码问题
运行验证一下房源编辑页面,不幸的是,当编辑房源信息后提交,中文乱码问题再次出现:
根据上篇文章的思路,可以猜想是否在从定向之前也要设置字符编码,再次添加上试试:
response.setCharacterEncoding("UTF-8");
考虑到这句代码可能有顺序要求,直接放在doPost()方法体的第一行。
经过验证,仍然有中文乱码。
那问题只能出现在读取到的表单数据本身就是乱码,因为我们一直考虑的是从Servlet容器返回给浏览器的页面存在乱码的问题,但此时对于表单提交来说,是浏览器端提交数据给Servlet容器,因为编辑页面中存在<meta charset="UTF-8">
,所以猜测提交给Servlet容器的HTTP报文应该是UTF-8编码的。
另一方面,HttpServletRequest读取参数的时候,是否跟HttpServletResponse一样,默认是ISO-8859-1呢?我们姑且也设置一下HttpServletRequest的字符编码,通过Eclipse中的自动补全功能,还真的发现它也有setCharacterEncoding()方法。
好,我们在doPost()方法体的第一行再加上:
request.setCharacterEncoding("UTF-8");
再验证一下,注意,直接重启一下Tomcat,因为之前的模拟数据已经是乱码了,除非编辑的时候把之前的全部删掉重新输入中文。
这次真的可以了!
总结
至此,我们的租房网平台已经实现了登录页面、房源列表页面、房源详细信息页面、房源编辑功能。
功能比较简单,并且设计上也有很多改进的地方,但总算能运转起来,虽然数据是模拟的,这块可以很容易的替换成访问数据库的。
从界面上看,也是挺简单的,但后台服务端的代码可比界面要多很多,当然比那些大型Web应用来说还是少得可怜。
还有很多代码重复的地方,比如当用户登录后,基本上每一个请求的处理都需要进行用户的登录验证,这块是不是可以使用Servlet技术中的过滤器来实现呢?还记得我们的设计原则之一就是消除重复吗。
还有,服务端出现问题之后排查问题的重要手段就是日志,基本上每一个请求的输入和每一个响应的输出都需要记录日志,这块是不是也可以使用Servlet技术中的过滤器来实现呢?
还有,Servlet中输出HTML内容实在是太难看了,而且容易出错,这属于展示层/视图层的内容,这就是MVC框架派上场的地方了,当然也可以直接使用JSP技术来实现,JSP技术本质上是Servlet技术,这点后续再介绍。
还有,就是能将模拟数据的代码和真正的部署到生产系统的代码能够分开就好了,本质上就是测试代码要与生产代码分离。这块也有框架可以实现。
总之,还有太多太多要考虑的地方,慢慢学习吧。
• 会话跟踪技术也可以使用表单的隐藏域;
• 中文乱码问题还有可能是服务端以不同的字符编码读取浏览器提交表单数据引起的。