將前九章的知識結合起來,實現(xiàn)一個電子留言板,包括注冊登錄,發(fā)帖回復功能。
如果你不滿足以下任一條件,請繼續(xù)閱讀,否則請?zhí)^此后的部分,進入下一章:第 11 章 文件上傳。
對電子留言板不感興趣。
首頁顯示的是主題列表。
用戶如果想發(fā)表新主題或者對主題進行回復,必須先注冊為會員。
注冊后進入登錄頁面進行登錄。
登錄后即出現(xiàn)在用戶在線列表中。
點擊標題可以看到主題的詳細信息。
登錄以后即可發(fā)布新主題。
數據庫er圖
共定義了三張表:
user用戶,保存注冊用的信息。
thread主題,用戶發(fā)起的主題帖子,外鍵關聯(lián)user,對應發(fā)表主題的用戶
comment回復,對主題帖子發(fā)起的回復,外鍵關聯(lián)user和thread,對應發(fā)表回復的用戶和回復的主題。
建表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, -- 發(fā)帖時間 update_time datetime, -- 更新時間 hit integer, -- 點擊數 user bigint -- 發(fā)帖用戶 ); -- 回復 create table comment( id bigint, -- 主題 content varchar(2000), -- 內容 create_time datetime, -- 發(fā)布時間 user bigint, -- 回復用戶 thread bigint -- 回復的主題 );
根據數據庫表建模。每張表對應三部分:domain,dao和servlet。domain是簡單的javabean用來封裝數據表中的數據,dao中進行對數據庫的業(yè)務操作,servlet作為控制器處理請求調用dao和domain實現(xiàn)業(yè)務功能。
為了便于管理,將使用到的類分成四個包,domain,dao,utils和web。domain, dao, web中分別包含domain, dao和servlet類,utils包中是數據庫連接工具和過濾器。
這里的domain和dao都是按照理想狀態(tài)編寫的,將數據庫表中的字段對應到domain類中,然后dao提供CRUD功能,不過dao中的有些功能并沒有用到,比如update和remove。
整個在線留言板可分為兩大功能部分:用戶管理與主題回復管理。
用戶管理功能包括:新用戶注冊,用戶登錄,用戶注銷。用戶登錄的時候順便帶上一個用戶在線列表。
這部分的頁面主要在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 節(jié) “例子:在線列表”
/** * 登錄. */ 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操作在線列表。
如果用戶名或密碼錯誤,則將請求轉發(fā)至/security/login.jsp頁面,顯示錯誤信息。
控制用戶訪問權限
與用戶操作相關的還有anni.utils.SecurityFilter,我們使用它來控制用戶的訪問權限??梢詤⒖贾暗挠懻摚?a target="_blank" >第 7.2 節(jié) “用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(進入發(fā)布新主題的頁面),/thread.do?method=post(發(fā)布新主題),/comment.do?method=post(發(fā)布回復)。這三個操作只有在用戶登錄之后才能訪問,如果用戶還沒有的登錄就會頁面重定向到/security/securityFailure.jsp,顯示權限不足無法訪問的提示信息。
主題回復管理功能包括:查看所有主題,查看某一主題的詳細信息和對應回復,發(fā)表新主題,發(fā)表回復。點擊主題時還會計算點擊數。
查看所有主題信息
進入應用,index.jsp會立即跳轉到/forum.do?method=list,并在list.jsp中顯示所有主題,包括主題標題,回復數,作者,點擊數,最后回復時間,最后回復人。這些信息按照“最后回復時間”進行逆序排列。
實現(xiàn)代碼在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)); // 最后發(fā)言時間 map.put("user", rs.getString(7)); // 最后發(fā)言人 list.add(map); } } finally { DbUtils.close(null, state, conn); } return list; }
或許有人會奇怪為什么不直接使用ResultSet。這其實是一種理念問題,如果你返回ResultSet到jsp頁面,的確免去了封裝成Map的步驟,但是同時產生了兩個問題。
第一,數據庫操作對應的代碼蔓延到前臺頁面,有違我們分層設計的初衷。如果覺得我們這是過度設計的話,那么第二個問題則是更嚴重的,將ResultSet放到jsp上很難控制何時關閉數據庫連接,如果發(fā)生了異??赡軄聿患瓣P閉數據連接,用不了多長時間就會耗盡資源了。
ForumDao中,勉強拼湊出三個表連接查詢的sql,還不清楚性能是否有保證。
顯示主題詳細信息
點擊主題標題/forum.do?method=view&id=1,會進入顯示對應詳細信息的頁面/view.jsp。頂部顯示的是主題帖子的標題,發(fā)布時間,作者和內容。主題內容下面列出所有的回復內容,頁面底部是回復使用的表單,只有登錄之后才能使用。
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)); // 發(fā)布時間 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字段加一,也是為了避免在異常情況下找不到對應主題時,不必出現(xiàn)更新異常。
發(fā)布新主題和發(fā)布回復
這兩項對應了anni.web.ThreadServlet和anni.web.CommentServlet中的post()方法。
為了簡易起見,我們僅僅在頁面上使用javascript檢驗輸入的數據不能為空。
提交之后會調用對應dao中的save()方法將數據保存進數據庫。最后頁面重定向到/forum.do?method=list或/forum.do?method=view&id=1。實際上它們都是單純的create操作(CRUD中的C)。
我們使用了HttpSessionBindingListener來實現(xiàn)在線用戶列表。詳細介紹見第 8.2 節(jié) “使用HttpSessionBindingListener”。
/list.jsp和/view.jsp兩個頁面上的在線用戶列表顯示效果完全一樣,如果有可能的話,我們希望將這些重復的部分從原來的頁面中剝離出來,集中在一起讓其他頁面調用,這樣更容易管理和維護。
為了實現(xiàn)這一功能,我們需要借用另一個jsp指令(directive):include。
<%@ include file="/include/onlineUser.jsp"%>
這里的file可以使用相對路徑,也可以使用絕對路徑。這里的絕對路徑與使用forward時一致,都是以應用目錄為根目錄,參考這里的討論第 3.4.1.2 節(jié) “絕對路徑”。
我們順便再看一下/include/onlineUser.jsp的內容:
<%@ page contentType="text/html; charset=gb2312"%> <fieldset> <legend>在線用戶</legend> <div> <c:forEach var="item" items="${onlineUserList}"> ${item} </c:forEach> </div> </fieldset>
這就是一個單獨的jsp頁面,可以在里邊使用jsp指令(directive),el,甚至是taglib。
不過taglib還是要在使用前定義的,因為每個頁面都使用了相同的taglib定義和其他一些相同的html配置(編碼,css等),我們也把這部分提取成一個jsp頁面,讓其他頁面引用。這個頁面也放在include目錄下,meta.jsp的內容如下。
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%> <c:set var="ctx" value="${pageContext.request.contextPath}"/> <meta http-equiv="Content-Type" content="text/html; charset=gb2312" /> <link rel="stylesheet" type="text/css" href="${ctx}/styles/forum.css" />
meta.jsp里定義了我們使用的taglib,設置了默認的contextPath,gb2312的編碼格式和forum.css樣式表。在其他頁面里對它進行引用就可以讓其他頁面內容也得到里邊定義的功能,包括標簽庫定義和使用c:set設置的變量。
<head> <%@ include file="/include/meta.jsp"%> <title>index</title> </head>
include之間的jsp中的定義和變量都是可以相互調用的,但是我們必須為每個jsp頁面都指定正確的編碼格式。這依然是為了處理中文亂碼,meta.jsp中沒有指定編碼格式就是因為這也里沒有中文。/include/onlineUser.jsp里包含了中文,如果不設置charset就會顯示亂碼,使用include的時候需要注意這一點。
這個在線留言板包含了之前討論過的問題:
使用過濾器第 7.1 節(jié) “批量設置請求編碼”,處理中文亂碼第 2.2 節(jié) “中文亂碼”。
使用servlet處理轉發(fā)請求第 6 章 貼近servlet,結合數據庫進行CRUD操作第 5 章 結合javabean實現(xiàn)CRUD,并使用foward和redirect進行請求轉發(fā)和頁面重定向第 3 章 請求的跳轉與轉發(fā)。
頁面顯示數據的時候使用了el和taglib第 9 章 封裝taglib組件。
使用過濾器控制訪問權限第 7.2 節(jié) “用filter控制用戶訪問權限”,使用監(jiān)聽器操作在線用戶列表第 8 章 配置listener監(jiān)聽器。
例子在10-01目錄下,將目錄復制到tomcat的webapps目錄下即可使用。
源代碼在10-01/WEB-INF/src目錄下,在將整個目錄復制到webapps下后,可以使用compile.bat進行編譯。
數據庫腳本在10-01/WEB-INF/sql目錄下,修改import.sql后,執(zhí)行run.bat可改變數據庫中的初始數據。