第 10 章 综合电子留言板.docx
《第 10 章 综合电子留言板.docx》由会员分享,可在线阅读,更多相关《第 10 章 综合电子留言板.docx(15页珍藏版)》请在冰豆网上搜索。
![第 10 章 综合电子留言板.docx](https://file1.bdocx.com/fileroot1/2023-1/22/c627363d-6062-4436-b737-56806db382ca/c627363d-6062-4436-b737-56806db382ca1.gif)
第10章综合电子留言板
第 10 章 综合电子留言板
注意
将前九章的知识结合起来,实现一个电子留言板,包括注册登录,发帖回复功能。
如果你不满足以下任一条件,请继续阅读,否则请跳过此后的部分,进入下一章:
第 11 章文件上传。
1.对电子留言板不感兴趣。
10.1. 电子留言板用户指南
首页显示的是主题列表。
用户如果想发表新主题或者对主题进行回复,必须先注册为会员。
注册后进入登录页面进行登录。
登录后即出现在用户在线列表中。
点击标题可以看到主题的详细信息。
登录以后即可发布新主题。
10.2. 数据库设计
数据库er图
共定义了三张表:
1.user用户,保存注册用的信息。
2.thread主题,用户发起的主题帖子,外键关联user,对应发表主题的用户
3.comment回复,对主题帖子发起的回复,外键关联user和thread,对应发表回复的用户和回复的主题。
建表sql脚本放在10-01/WEB-INF/sql/import.sql。
--用户
createtableuser(
idbigint,--主键
usernamevarchar(100),--帐号
passwordvarchar(100),--密码
reg_timedatetime,--注册时间
last_logindatetime--上次登录时间
);
--主题
createtablethread(
idbigint,--主题
titlevarchar(200),--标题
contentvarchar(2000),--内容
create_timedatetime,--发帖时间
update_timedatetime,--更新时间
hitinteger,--点击数
userbigint--发帖用户
);
--回复
createtablecomment(
idbigint,--主题
contentvarchar(2000),--内容
create_timedatetime,--发布时间
userbigint,--回复用户
threadbigint--回复的主题
);
根据数据库表建模。
每张表对应三部分:
domain,dao和servlet。
domain是简单的javabean用来封装数据表中的数据,dao中进行对数据库的业务操作,servlet作为控制器处理请求调用dao和domain实现业务功能。
为了便于管理,将使用到的类分成四个包,domain,dao,utils和web。
domain,dao,web中分别包含domain,dao和servlet类,utils包中是数据库连接工具和过滤器。
这里的domain和dao都是按照理想状态编写的,将数据库表中的字段对应到domain类中,然后dao提供CRUD功能,不过dao中的有些功能并没有用到,比如update和remove。
10.3. 功能设计
整个在线留言板可分为两大功能部分:
用户管理与主题回复管理。
10.3.1. 用户管理
用户管理功能包括:
新用户注册,用户登录,用户注销。
用户登录的时候顺便带上一个用户在线列表。
这部分的页面主要在security目录下,操作代码都放在anni.web.UserServlet.java和对应的anni.domain.User,anni.dao.UserDao中。
1.新用户注册
这是CRUD中的create,向用户表中添加一条新信息,我们只在前台页面中使用javascript进行数据校验,要求用户输入用户名,密码,并且在两次密码输入相同的时候才能提交。
提交的请求交由UserServlet的register()方法处理。
/**
*注册新用户.
*/
publicvoidregister(HttpServletRequestrequest,HttpServletResponseresponse)throwsException{
Stringusername=request.getParameter("username");
Stringpassword=request.getParameter("password");
StringconfirmPassword=request.getParameter("confirmPassword");
booleanuserExists=userDao.checkExists(username);
if(userExists){
request.setAttribute("error","用户名:
"+username+"已被使用了,请更换其他用户名注册。
");
request.getRequestDispatcher("/security/register.jsp").forward(request,response);
}else{
Useruser=newUser();
user.setUsername(username);
user.setPassword(password);
userDao.save(user);
response.sendRedirect(request.getContextPath()+"/security/registerSuccess.jsp");
}
}
获得用户名和密码后,先通过userDao.checkExists()检测数据库中是否已经有了同名的用户,如果用户名重复,就跳转到/security/register.jsp显示错误信息。
如果用户名没有重复,则将此用户信息添加入库,然后页面重定向到/security/registerSuccess.jsp显示注册成功信息。
保存信息之后使用redirect是个避免重复提交的简易方法,如果使用forward,浏览器上的url不会改变,用户刷新页面就会导致重复提交信息。
2.用户登录与注销
登录与注销的流程与之前介绍的大体相同。
第 4.2 节“例子:
在线列表”
/**
*登录.
*/
publicvoidlogin(HttpServletRequestrequest,HttpServletResponseresponse)throwsException{
Stringusername=request.getParameter("username");
Stringpassword=request.getParameter("password");
Useruser=userDao.login(username,password);
if(user!
=null){
user.setLastLogin(newDate());
userDao.update(user);
HttpSessionsession=request.getSession();
session.setAttribute("user",user);
//加入在线列表
session.setAttribute("onlineUserBindingListener",newOnlineUserBindingListener(username));
response.sendRedirect(request.getContextPath()+"/security/loginSuccess.jsp");
}else{
request.setAttribute("error","用户名或密码错误!
");
request.getRequestDispatcher("/security/login.jsp").forward(request,response);
}
}
/**
*注销.
*/
publicvoidlogout(HttpServletRequestrequest,HttpServletResponseresponse)throwsException{
request.getSession().invalidate();
response.sendRedirect(request.getContextPath()+"/security/logoutSuccess.jsp");
}
我们先根据请求中的用户名和密码去数据库搜索用户信息。
如果能找到,说明用户输入无误可以登录,这时更新用户最后登录时间,并将user保存到session中,同时使用listener操作在线列表。
如果用户名或密码错误,则将请求转发至/security/login.jsp页面,显示错误信息。
3.控制用户访问权限
与用户操作相关的还有anni.utils.SecurityFilter,我们使用它来控制用户的访问权限。
可以参考之前的讨论:
第 7.2 节“用filter控制用户访问权限”。
web.xml中对SecurityFilter的配置如下:
SecurityFilter
anni.utils.SecurityFilter
SecurityFilter
/*
因为filter-mapping太不灵活,我们让SecurityFilter过滤所有的请求,在代码里判断哪些请求需要保护。
publicvoiddoFilter(ServletRequestrequest,
ServletResponseresponse,
FilterChainchain)
throwsIOException,ServletException{
HttpServletRequestreq=(HttpServletRequest)request;
HttpServletResponseres=(HttpServletResponse)response;
Stringurl=req.getServletPath();
Stringmethod=req.getParameter("method");
if("/create.jsp".equals(url)||
("/thread.do".equals(url)&&"post".equals(method))||
("/comment.do".equals(url)&&"post".equals(method))){
HttpSessionsession=req.getSession();
if(session.getAttribute("user")==null){
res.sendRedirect(req.getContextPath()+"/security/securityFailure.jsp");
return;
}
}
chain.doFilter(request,response);
}
在此我们只保护三个请求:
/create.jsp(进入发布新主题的页面),/thread.do?
method=post(发布新主题),/comment.do?
method=post(发布回复)。
这三个操作只有在用户登录之后才能访问,如果用户还没有的登录就会页面重定向到/security/securityFailure.jsp,显示权限不足无法访问的提示信息。
10.3.2. 主题回复管理
主题回复管理功能包括:
查看所有主题,查看某一主题的详细信息和对应回复,发表新主题,发表回复。
点击主题时还会计算点击数。
1.查看所有主题信息
进入应用,index.jsp会立即跳转到/forum.do?
method=list,并在list.jsp中显示所有主题,包括主题标题,回复数,作者,点击数,最后回复时间,最后回复人。
这些信息按照“最后回复时间”进行逆序排列。
实现代码在anni.web.ForumServlet的list()方法内。
/**
*显示所有帖子.
*/
privatevoidlist(HttpServletRequestrequest,HttpServletResponseresponse)throwsException{
Listlist=forumDao.getAll();
request.setAttribute("list",list);
request.getRequestDispatcher("/list.jsp").forward(request,response);
}
调用anni.dao.ForumDao的pagedQuery()方法返回我们需要的信息,这里只用domain中定义的类已经无法满足我们了(显示的信息包含了三个表的信息),为了方便起见我们直接使用了Map来传递数据。
publicListgetAll()throwsException{
Connectionconn=null;
Statementstate=null;
Listlist=newArrayList();
try{
conn=DbUtils.getConn();
state=conn.createStatement();
Stringsql="select"+
"t.id,"+
"t.title,"+
"(selectcount(id)fromcommentwherethread=t.id)asreply,"+
"(selectusernamefromuserwhereid=t.user)asauthor,"+
"t.hit,"+
"(selecttop1create_timefromcommentwherethread=t.idorderbycreate_timedesc)ascreate_time,"+
"(selecttop1u.usernamefromcommentc,useruwherec.thread=t.idandc.user=u.id"+
"orderbycreate_timedesc)asuser"+
"fromthreadt"+
"orderbyuserdesc";
ResultSetrs=state.executeQuery(sql);
while(rs.next()){
Mapmap=newHashMap();
map.put("id",rs.getLong
(1));//主键
map.put("title",rs.getString
(2));//标题
map.put("reply",rs.getInt(3));//回复数
map.put("author",rs.getString(4));//作者
map.put("hit",rs.getInt(5));//点击数
map.put("updateDate",rs.getTimestamp(6));//最后发言时间
map.put("user",rs.getString(7));//最后发言人
list.add(map);
}
}finally{
DbUtils.close(null,state,conn);
}
returnlist;
}
或许有人会奇怪为什么不直接使用ResultSet。
这其实是一种理念问题,如果你返回ResultSet到jsp页面,的确免去了封装成Map的步骤,但是同时产生了两个问题。
第一,数据库操作对应的代码蔓延到前台页面,有违我们分层设计的初衷。
如果觉得我们这是过度设计的话,那么第二个问题则是更严重的,将ResultSet放到jsp上很难控制何时关闭数据库连接,如果发生了异常可能来不及关闭数据连接,用不了多长时间就会耗尽资源了。
ForumDao中,勉强拼凑出三个表连接查询的sql,还不清楚性能是否有保证。
2.显示主题详细信息
点击主题标题/forum.do?
method=view&id=1,会进入显示对应详细信息的页面/view.jsp。
顶部显示的是主题帖子的标题,发布时间,作者和内容。
主题内容下面列出所有的回复内容,页面底部是回复使用的表单,只有登录之后才能使用。
ForumServlet中的view()方法用来获得我们需要的主题信息和对应的回复信息。
/**
*显示帖子内容.
*/
privatevoidview(HttpServletRequestrequest,HttpServletResponseresponse)throwsException{
longid=Long.parseLong(request.getParameter("id"));
Mapthread=forumDao.viewThread(id);
Listlist=forumDao.getCommentsByThread(id);
request.setAttribute("thread",thread);
request.setAttribute("list",list);
request.getRequestDispatcher("/view.jsp").forward(request,response);
}
我们从请求中获得主题的id,获得主题详细信息和对应的回复信息列表,这两项都是使用Map传递数据传递到view.jsp页面中再使用el和jstl显示出来。
在显示主题详细信息时,顺便讲主题的点击数加一。
publicMapviewThread(longid)throwsException{
Connectionconn=null;
PreparedStatementstate=null;
Mapmap=newHashMap();
try{
conn=DbUtils.getConn();
state=conn.prepareStatement("selectt.id,t.title,t.content,t.create_time,u.username"+
"fromthreadt,useruwheret.user=u.idandt.id=?
");
state.setLong(1,id);
ResultSetrs=state.executeQuery();
if(rs.next()){
map.put("id",rs.getLong
(1));//主键
map.put("title",rs.getString
(2));//标题
map.put("content",rs.getString(3));//内容
map.put("createTime",rs.getTimestamp(4));//发布时间
map.put("username",rs.getString(5));//作者名
}
//增加点击数
state=conn.prepareStatement("updatethreadsethit=hit+1whereid=?
");
state.setLong(1,id);
state.executeUpdate();
}finally{
DbUtils.close(null,state,conn);
}
returnmap;
}
我们把这个更新操作放到查询之后,使用update将hit字段加一,也是为了避免在异常情况下找不到对应主题时,不必出现更新异常。
3.发布新主题和发布回复
这两项对应了anni.web.ThreadServlet和anni.web.CommentServlet中的post()方法。
为了简易起见,我们仅仅在页面上使用javascript检验输入的数据不能为空。
提交之后会调用对应dao中的save()方法将数据保存进数据库。
最后页面重定向到/forum.do?
method=list或/forum.do?
method=view&id=1。
实际上它们都是单纯的create操作(CRUD中的C)。
10.3.3. 显示在线用户列表
我们使用了HttpSessionBindingListener来实现在线用户列表。
详细介绍见第 8.2 节“使用HttpSessionBindingListener”。
/list.jsp和/view.jsp两个页面上的在线用户列表显示效果完全一样,如果有可能的话,我们希望将这些重复的部分从原来的页面中剥离出来,集中在一起让其他页面调用,这样更容易管理和维护。
为了实现这一功能,我们需要借用另一个jsp指令(directive):
include。
<%@includefile="/include/onlineUser.jsp"%>
这里的file可以使用相对路径,也可以使用绝对路径。
这里的绝对路径与使用forward时一致,都是以应用目录为根目录,参考这里的讨论第 3.4.1.2 节“绝对路径”。
我们顺便再看一下/include/onlineUser.jsp的内容:
<%@pagecontentType="text/html;charset=gb2312"%>
这就是一个单独的jsp页面,可以在里边使用jsp指令(directive),el,甚至是taglib。
不过taglib还是要在使用前定义的,因为每个页面都使用了相同的taglib定义和其他一些相同的html配置(编码,css等),我们也把这部分提取成一个jsp页面,让其他页面引用。
这个页面也放在include目录下,meta.jsp的内容如下。
<%@taglibprefix="c"uri="
setvar="ctx"value="${pageContext.requ