综合电子留言板
来源:百度文库 编辑:神马文学网 时间:2024/05/01 15:26:16
第 10 章 综合电子留言板
注意
将前九章的知识结合起来,实现一个电子留言板,包括注册登录,发帖回复功能。
如果你不满足以下任一条件,请继续阅读,否则请跳过此后的部分,进入下一章:第 11 章 文件上传。
-
对电子留言板不感兴趣。
10.1. 电子留言板用户指南
首页显示的是主题列表。
用户如果想发表新主题或者对主题进行回复,必须先注册为会员。
注册后进入登录页面进行登录。
登录后即出现在用户在线列表中。
点击标题可以看到主题的详细信息。
登录以后即可发布新主题。
10.2. 数据库设计
数据库er图
共定义了三张表:
-
user用户,保存注册用的信息。
-
thread主题,用户发起的主题帖子,外键关联user,对应发表主题的用户
-
comment回复,对主题帖子发起的回复,外键关联user和thread,对应发表回复的用户和回复的主题。
建表sql脚本放在10-01/WEB-INF/sql/import.sql。
-- 用户create table user(id bigint, -- 主键username varchar(100), -- 帐号password varchar(100), -- 密码reg_time datetime, -- 注册时间last_login datetime -- 上次登录时间);-- 主题create table thread(id bigint, -- 主题title varchar(200), -- 标题content varchar(2000), -- 内容create_time datetime, -- 发帖时间update_time datetime, -- 更新时间hit integer, -- 点击数user bigint -- 发帖用户);-- 回复create table comment(id bigint, -- 主题content varchar(2000), -- 内容create_time datetime, -- 发布时间user bigint, -- 回复用户thread bigint -- 回复的主题);
根据数据库表建模。每张表对应三部分: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中。
-
新用户注册
这是CRUD中的create,向用户表中添加一条新信息,我们只在前台页面中使用javascript进行数据校验,要求用户输入用户名,密码,并且在两次密码输入相同的时候才能提交。
提交的请求交由UserServlet的register()方法处理。
/** * 注册新用户. */ public void register(HttpServletRequest request,HttpServletResponse response) throws Exception { String username = request.getParameter("username"); String password = request.getParameter("password"); String confirmPassword = request.getParameter("confirmPassword"); boolean userExists = userDao.checkExists(username); if (userExists) { request.setAttribute("error", "用户名:" + username + "已被使用了,请更换其他用户名注册。"); request.getRequestDispatcher("/security/register.jsp").forward(request, response); } else { User user = new User(); 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不会改变,用户刷新页面就会导致重复提交信息。
-
用户登录与注销
登录与注销的流程与之前介绍的大体相同。第 4.2 节 “例子:在线列表”
/** * 登录. */ public void login(HttpServletRequest request,HttpServletResponse response) throws Exception { String username = request.getParameter("username"); String password = request.getParameter("password"); User user = userDao.login(username, password); if (user != null) { user.setLastLogin(new Date()); userDao.update(user); HttpSession session = request.getSession(); session.setAttribute("user", user); // 加入在线列表 session.setAttribute("onlineUserBindingListener", new OnlineUserBindingListener(username)); response.sendRedirect(request.getContextPath() + "/security/loginSuccess.jsp"); } else { request.setAttribute("error", "用户名或密码错误!"); request.getRequestDispatcher("/security/login.jsp").forward(request, response); } } /** * 注销. */ public void logout(HttpServletRequest request,HttpServletResponse response) throws Exception { request.getSession().invalidate(); response.sendRedirect(request.getContextPath() + "/security/logoutSuccess.jsp"); }
我们先根据请求中的用户名和密码去数据库搜索用户信息。如果能找到,说明用户输入无误可以登录,这时更新用户最后登录时间,并将user保存到session中,同时使用listener操作在线列表。
如果用户名或密码错误,则将请求转发至/security/login.jsp页面,显示错误信息。
-
控制用户访问权限
与用户操作相关的还有anni.utils.SecurityFilter,我们使用它来控制用户的访问权限。可以参考之前的讨论:第 7.2 节 “用filter控制用户访问权限”。
web.xml中对SecurityFilter的配置如下:
<filter> <filter-name>SecurityFilter</filter-name> <filter-class>anni.utils.SecurityFilter</filter-class> </filter> <filter-mapping> <filter-name>SecurityFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>
因为filter-mapping太不灵活,我们让SecurityFilter过滤所有的请求,在代码里判断哪些请求需要保护。
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req = (HttpServletRequest) request; HttpServletResponse res = (HttpServletResponse) response; String url = req.getServletPath(); String method = req.getParameter("method"); if ("/create.jsp".equals(url) || ("/thread.do".equals(url) && "post".equals(method)) || ("/comment.do".equals(url) && "post".equals(method))) { HttpSession session = 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. 主题回复管理
主题回复管理功能包括:查看所有主题,查看某一主题的详细信息和对应回复,发表新主题,发表回复。点击主题时还会计算点击数。
-
查看所有主题信息
进入应用,index.jsp会立即跳转到/forum.do?method=list,并在list.jsp中显示所有主题,包括主题标题,回复数,作者,点击数,最后回复时间,最后回复人。这些信息按照“最后回复时间”进行逆序排列。
实现代码在anni.web.ForumServlet的list()方法内。
/** * 显示所有帖子. */ private void list(HttpServletRequest request, HttpServletResponse response) throws Exception { List list = forumDao.getAll(); request.setAttribute("list", list); request.getRequestDispatcher("/list.jsp").forward(request, response); }
调用anni.dao.ForumDao的pagedQuery()方法返回我们需要的信息,这里只用domain中定义的类已经无法满足我们了(显示的信息包含了三个表的信息),为了方便起见我们直接使用了Map来传递数据。
public List getAll() throws Exception { Connection conn = null; Statement state = null; List list = new ArrayList(); try { conn = DbUtils.getConn(); state = conn.createStatement(); String sql = "select " + "t.id, " + "t.title, " + "(select count(id) from comment where thread=t.id) as reply, " + "(select username from user where id=t.user) as author, " + "t.hit, " + "(select top 1 create_time from comment where thread=t.id order by create_time desc) as create_time, " + "(select top 1 u.username from comment c,user u where c.thread=t.id and c.user=u.id " + "order by create_time desc) as user " + "from thread t " + "order by user desc"; ResultSet rs = state.executeQuery(sql); while (rs.next()) { Map map = new HashMap(); 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); } return list; }
或许有人会奇怪为什么不直接使用ResultSet。这其实是一种理念问题,如果你返回ResultSet到jsp页面,的确免去了封装成Map的步骤,但是同时产生了两个问题。
第一,数据库操作对应的代码蔓延到前台页面,有违我们分层设计的初衷。如果觉得我们这是过度设计的话,那么第二个问题则是更严重的,将ResultSet放到jsp上很难控制何时关闭数据库连接,如果发生了异常可能来不及关闭数据连接,用不了多长时间就会耗尽资源了。
ForumDao中,勉强拼凑出三个表连接查询的sql,还不清楚性能是否有保证。
-
显示主题详细信息
点击主题标题/forum.do?method=view&id=1,会进入显示对应详细信息的页面/view.jsp。顶部显示的是主题帖子的标题,发布时间,作者和内容。主题内容下面列出所有的回复内容,页面底部是回复使用的表单,只有登录之后才能使用。
ForumServlet中的view()方法用来获得我们需要的主题信息和对应的回复信息。
/** * 显示帖子内容. */ private void view(HttpServletRequest request, HttpServletResponse response) throws Exception { long id = Long.parseLong(request.getParameter("id")); Map thread = forumDao.viewThread(id); List list = forumDao.getCommentsByThread(id); request.setAttribute("thread", thread); request.setAttribute("list", list); request.getRequestDispatcher("/view.jsp").forward(request, response); }
我们从请求中获得主题的id,获得主题详细信息和对应的回复信息列表,这两项都是使用Map传递数据传递到view.jsp页面中再使用el和jstl显示出来。
在显示主题详细信息时,顺便讲主题的点击数加一。
public Map viewThread(long id) throws Exception { Connection conn = null; PreparedStatement state = null; Map map = new HashMap(); try { conn = DbUtils.getConn(); state = conn.prepareStatement("select t.id,t.title,t.content,t.create_time,u.username " + "from thread t,user u where t.user=u.id and t.id=?"); state.setLong(1, id); ResultSet rs = 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("update thread set hit=hit+1 where id=?"); state.setLong(1, id); state.executeUpdate(); } finally { DbUtils.close(null, state, conn); } return map; }
我们把这个更新操作放到查询之后,使用update将hit字段加一,也是为了避免在异常情况下找不到对应主题时,不必出现更新异常。
-
发布新主题和发布回复
这两项对应了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。
<%@ include file="/include/onlineUser.jsp"%>
这里的file可以使用相对路径,也可以使用绝对路径。这里的绝对路径与使用forward时一致,都是以应用目录为根目录,参考这里的讨论第 3.4.1.2 节 “绝对路径”。
我们顺便再看一下/include/onlineUser.jsp的内容:
<%@ page contentType="text/html; charset=gb2312"%>