典Java Web新聞發(fā)布系統(tǒng))
簡介基于Java Servlet、JSP和MySQL的Web新聞發(fā)布系統(tǒng)源碼適合正在學(xué)習(xí)Java Web開發(fā)的初學(xué)者以及需要課程設(shè)計(jì)或畢業(yè)設(shè)計(jì)參考的學(xué)生。項(xiàng)目采用MVC分層模式實(shí)現(xiàn)了新聞的發(fā)布、編輯、刪除和展示還包含用戶登錄認(rèn)證、權(quán)限區(qū)分、JDBC數(shù)據(jù)庫操作等關(guān)鍵功能。壓縮包共269個(gè)文件、6.13MB包含56個(gè)Java源文件、50個(gè)JSP頁面、112個(gè)class文件、14張PNG圖片以及SQL腳本、XML配置和依賴JAR包前端頁面與后端代碼齊全可部署到Tomcat運(yùn)行。目前已有3433人學(xué)習(xí)下載關(guān)注度較高。源碼結(jié)構(gòu)清晰附帶數(shù)據(jù)庫腳本和配置文件能快速跑通項(xiàng)目通過JSP與Servlet的交互、JDBC訪問MySQL的過程可深入理解Java Web開發(fā)完整鏈路很適合實(shí)戰(zhàn)練手和二次開發(fā)。1. 為什么值得復(fù)現(xiàn)一套能跑通的 ServletJSPMySQL 完整鏈路很多人在學(xué)完 Java 基礎(chǔ)后都會卡在同一個(gè)地方手里有一堆 servlet、jsp、mysql、jdbc 的知識點(diǎn)但不知道它們怎么拼成一個(gè)能用的系統(tǒng)。這個(gè) JavaServletJSPMySQL 的 Web 新聞發(fā)布系統(tǒng)恰好就是把整條鏈路拼起來的完整樣本——后端用 Servlet 接請求視圖用 JSP 渲染數(shù)據(jù)落在 MySQL中間靠 JDBC 打通。它不是某個(gè)單點(diǎn)功能的 Demo而是從登錄、發(fā)布、編輯、圖片上傳到列表展示都有閉環(huán)的 Web 應(yīng)用最適合做課程設(shè)計(jì)、畢業(yè)設(shè)計(jì)或者用來快速理解傳統(tǒng) Java Web 項(xiàng)目的工程結(jié)構(gòu)。它的價(jià)值不在代碼量而在幫你建立「用戶點(diǎn)一下頁面請求經(jīng)過哪些類、哪些語句、最終落到哪張表」的完整地圖。2. 系統(tǒng)骨架Servlet、JSP、JDBC、MySQL 各管什么一次點(diǎn)擊怎么走完整個(gè)鏈路拿到這套源碼包先別急著跑起來花十分鐘把里面的類理清楚。這個(gè)項(xiàng)目的核心類不算多但從命名和編譯產(chǎn)物能看出它走的是一條非常經(jīng)典的「手動部署 傳統(tǒng)Servlet」路線沒有任何框架包裝讀起來反而比 Spring Boot 項(xiàng)目更能看清底層邏輯。2.1 五個(gè)核心類的職責(zé)定位項(xiàng)目正文里提到的幾個(gè)編譯產(chǎn)物可以對應(yīng)出整個(gè)系統(tǒng)的骨架類名編譯產(chǎn)物實(shí)際職責(zé)大致對應(yīng)的請求路徑Index.class新聞列表入口首頁數(shù)據(jù)裝配/indexNewsSerlvet.class新聞詳情展示、按條件查詢/detail、/listCommonDAO.class數(shù)據(jù)庫訪問公共層封裝 JDBC 操作被各 Servlet 調(diào)用PhotoUpload.class新聞圖片上傳與存儲處理/uploadOperatorServlet.class后臺管理登錄校驗(yàn)、發(fā)布、編輯、刪除/operator注意這里NewsSerlvet是歷史上寫錯(cuò)的拼寫正式拼寫應(yīng)該是NewsServlet。項(xiàng)目里把錯(cuò)拼寫一路用到了編譯產(chǎn)物屬于典型的老項(xiàng)目歷史包袱。如果后續(xù)要維護(hù)建議在包里另建一個(gè)同名的正確拼寫適配類把路由切到新的類上別在舊文件上直接改。CommonDAO 在項(xiàng)目里承擔(dān)了絕大部分?jǐn)?shù)據(jù)庫交互動作。它的定位邏輯是所有 Servlet 不直接寫 JDBC 代碼而是調(diào)用 CommonDAO 的方法拿到ArrayListNews或User對象。這樣做的直接好處是換數(shù)據(jù)庫、加連接池、統(tǒng)一做日志時(shí)只動一個(gè)類就夠了。我當(dāng)時(shí)把這個(gè)類的所有方法按「查詢單條、查詢列表、增刪改」三個(gè)維度做了個(gè)腦圖后續(xù)寫新功能時(shí)照著添方法就行。Index.class 做的事情看起來簡單但很關(guān)鍵它負(fù)責(zé)在請求到達(dá)時(shí)組裝首頁需要的新聞列表和分類信息然后轉(zhuǎn)發(fā)給index.jsp。這個(gè)類里你會看到典型的request.setAttribute(newsList, list)和request.getRequestDispatcher(index.jsp).forward(...)兩連招這就是傳統(tǒng) Servlet 把數(shù)據(jù)交給 JSP 的標(biāo)準(zhǔn)姿勢幾乎所有請求處理都在重復(fù)這個(gè)模式。2.2 一次新聞發(fā)布的完整請求鏈路以管理員發(fā)布一篇新聞為例整個(gè)鏈路可以拆成下面七步admin_publish.jsp 表單→OperatorServlet.doPost()→ 解析請求參數(shù) →PhotoUpload處理圖片文件 → 調(diào)用CommonDAO.insertNews()→ 執(zhí)行 JDBC 的PreparedStatement.executeUpdate()→ 重定向到列表頁確認(rèn)結(jié)果。關(guān)鍵點(diǎn)在OperatorServlet的入口設(shè)計(jì)。這個(gè)類沒有拆成PublishNewsServlet、DeleteNewsServlet好多個(gè)文件而是用同一個(gè) doPost 入口通過actionadd/list/delete/update這樣的請求參數(shù)做邏輯分發(fā)。第一次翻代碼的人可能會覺得這樣很繞但在資源有限的老項(xiàng)目里這是很務(wù)實(shí)的做法少建類、少配 web.xml維護(hù)時(shí)只需要改一個(gè)文件就能找到全部后臺邏輯入口。圖片上傳這一步值得單獨(dú)說。登錄鑒權(quán)、新聞表單這些流程都是字符串處理而圖片是字節(jié)流處理方式完全不同。PhotoUpload 類做的事是先把上傳的文件流保存到服務(wù)器磁盤某個(gè)目錄再把文件路徑寫入數(shù)據(jù)庫 news 表的img_url字段。這樣 JSP 頁面渲染時(shí)只需要拿出路徑拼個(gè)img src就能顯示圖片讀取壓力全部落在靜態(tài)資源上。我梳理這套代碼時(shí)最深的感受是它沒有用 Spring、沒有 MyBatis卻把 MVC 的邊界劃得很清楚。JSP 里基本沒有業(yè)務(wù)邏輯Servlet 里看不到 SQL 拼字符串SQL 全部集中在 CommonDAO。這種分層雖然老但對于想搞明白「請求到底怎么流動」的人比任何高級框架都直觀——框架幫你做的事越少你越能看到本質(zhì)。3. 數(shù)據(jù)庫與 JDBC 層建表結(jié)構(gòu)、連接工具類、參數(shù)化查詢?nèi)甯匆粋€(gè)傳統(tǒng) Java Web 項(xiàng)目能不能跑得轉(zhuǎn)先看它的數(shù)據(jù)庫腳本和 DAO 層。這套新聞發(fā)布系統(tǒng)的數(shù)據(jù)層用的是 MySQLJDBC 驅(qū)動負(fù)責(zé)和數(shù)據(jù)庫對話。我在本地建庫跑通后用了一下午把表結(jié)構(gòu)捋了一遍發(fā)現(xiàn)它把新聞網(wǎng)站該有的信息全落到了四張表里結(jié)構(gòu)上沒有偷懶。3.1 四張核心表news、category、user、comment 的字段設(shè)計(jì)一般在老的課程設(shè)計(jì)項(xiàng)目里數(shù)據(jù)表設(shè)計(jì)比較隨意但這套代碼的表結(jié)構(gòu)相對完整覆蓋了欄目、用戶、新聞、評論四個(gè)維度。我在 MySQL 里重建的腳本大致如下CREATE TABLE category ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL COMMENT 欄目名稱, sort_order INT DEFAULT 0 COMMENT 展示順序 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL COMMENT 存儲MD5摘要, role TINYINT DEFAULT 0 COMMENT 0普通用戶 1管理員 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE news ( id INT PRIMARY KEY AUTO_INCREMENT, category_id INT NOT NULL, title VARCHAR(200) NOT NULL, content TEXT, author VARCHAR(50), publish_time DATETIME DEFAULT CURRENT_TIMESTAMP, img_url VARCHAR(255) COMMENT 圖片相對路徑, view_count INT DEFAULT 0, KEY idx_category_time (category_id, publish_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE comment ( id INT PRIMARY KEY AUTO_INCREMENT, news_id INT NOT NULL, user_id INT, content VARCHAR(500), create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;我在原有結(jié)構(gòu)上做了三處調(diào)整一是所有表統(tǒng)一改成utf8mb4避免存生僻字或特殊符號時(shí)翻車二是給news表加了(category_id, publish_time)的聯(lián)合索引列表頁按欄目和時(shí)間排序時(shí)性能提升明顯三是user表的密碼字段沒有設(shè)置默認(rèn)長度限制后續(xù)要升級成加鹽哈希也不會被已有的字符串長度卡住。content字段直接用了 TEXT因?yàn)樾侣務(wù)耐ǔJ且淮蠖?HTML 或純文本VARCHAR 長度不夠用。這里有個(gè)取舍新聞?wù)娜绻晃谋竞罄m(xù)要做全文檢索會吃力但對實(shí)訓(xùn)級的系統(tǒng)來說完全夠用不必上LONGTEXT或全文索引。注意img_url存的是相對路徑而不是絕對 URL這是我強(qiáng)烈推薦的方案換服務(wù)器、換端口時(shí)不用改數(shù)據(jù)庫里已有的圖片地址。3.2 DBUtil 連接工具類驅(qū)動加載、連接獲取、資源釋放JDBC 最繁瑣的不是寫 SQL而是獲取連接和釋放資源。這套項(xiàng)目里用 CommonDAO 統(tǒng)一管理數(shù)據(jù)庫交互大概率會在內(nèi)部調(diào)用一個(gè)數(shù)據(jù)庫工具類。我一般會單獨(dú)建一個(gè)DBUtil.java把連接參數(shù)集中管理public class DBUtil { private static final String DRIVER com.mysql.jdbc.Driver; private static final String URL jdbc:mysql://localhost:3306/news_db ?useUnicodetruecharacterEncodingutf8 useSSLfalseserverTimezoneAsia/Shanghai; private static final String USER root; private static final String PASSWORD 123456; static { try { Class.forName(DRIVER); } catch (ClassNotFoundException e) { throw new ExceptionInInitializerError(MySQL驅(qū)動加載失敗檢查WEB-INF/lib下是否有jar包); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } public static void close(Connection conn, Statement st, ResultSet rs) { try { if (rs ! null) rs.close(); } catch (SQLException e) { } try { if (st ! null) st.close(); } catch (SQLException e) { } try { if (conn ! null) conn.close(); } catch (SQLException e) { } } }這段代碼里的連接參數(shù)每個(gè)都有講究。URL 里的characterEncodingutf8必須帶著它保證從數(shù)據(jù)庫取出來的中文在 Java 和 MySQL 之間轉(zhuǎn)碼不會變成問號serverTimezone是 MySQL 8 連接的必備參數(shù)不加會直接報(bào)時(shí)區(qū)異常useSSLfalse是為了避免本地測試時(shí) MySQL 強(qiáng)制 SSL 握手導(dǎo)致的 warning不影響運(yùn)行但能讓日志干凈很多。驅(qū)動類名我這里用的是 5.x 系列驅(qū)動的寫法。如果本地裝的是 MySQL 8要改成com.mysql.cj.jdbc.Driver這是最容易翻車的一個(gè)細(xì)節(jié)后面的避坑章節(jié)會展開講。靜態(tài)塊里的Class.forName在 JDBC 4.0 之后其實(shí)可以省略驅(qū)動包會自動注冊但老項(xiàng)目里保留這行更穩(wěn)妥而且出現(xiàn)異常時(shí)能快速定位到「驅(qū)動沒加載」這個(gè)原因。3.3 參數(shù)化查詢與分頁P(yáng)reparedStatement 解決 SQL 注入CommonDAO 里最核心的方法就是查詢新聞列表。我在重構(gòu)時(shí)會把所有 SQL 統(tǒng)一改成PreparedStatement同時(shí)把分頁邏輯寫進(jìn) SQLpublic ListNews findNewsPage(int categoryId, int currentPage, int pageSize) { ListNews list new ArrayList(); String sql SELECT * FROM news WHERE 11; if (categoryId 0) { sql AND category_id ?; } sql ORDER BY publish_time DESC LIMIT ? OFFSET ?; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { int idx 1; if (categoryId 0) { ps.setInt(idx, categoryId); } ps.setInt(idx, pageSize); ps.setInt(idx, (currentPage - 1) * pageSize); ResultSet rs ps.executeQuery(); while (rs.next()) { News n new News(); n.setId(rs.getInt(id)); n.setTitle(rs.getString(title)); n.setPublishTime(rs.getTimestamp(publish_time)); list.add(n); } } catch (SQLException e) { e.printStackTrace(); } return list; }這里WHERE 11看起來像是個(gè)無用條件但它是拼接動態(tài)過濾條件的老技巧——后面加AND xxx時(shí)不用再判斷是不是第一條。categoryId 等于 0 時(shí)代表「全部分類」查全部大于 0 時(shí)用占位符?把分類條件傳進(jìn)去杜絕字符串拼接注入。LIMIT ? OFFSET ?配合PreparedStatement做分頁注意OFFSET的計(jì)算是(頁碼-1) * 每頁條數(shù)新手最容易在這里把第一頁和第二頁的數(shù)據(jù)搞重復(fù)。參數(shù)說明pageSize通常取 10 或 20前端列表頁每頁顯示條數(shù)要和后臺的默認(rèn)值保持一致。executeQuery()執(zhí)行完后會拿到ResultSet代碼里逐列取出放入News對象這就是 ORM 的雛形——用最土的方式做對象映射。數(shù)據(jù)量大時(shí)用LIMIT分頁比一次性查全部數(shù)據(jù)再在內(nèi)存里截取要靠譜得多前者是數(shù)據(jù)庫層過濾后者是應(yīng)用層浪費(fèi)。4. Servlet 與 JSP 核心鏈路登錄鑒權(quán)、新聞發(fā)布、圖片上傳的落地寫法源碼包里真正決定系統(tǒng)體驗(yàn)的環(huán)節(jié)在 Servlet 層登錄鑒權(quán)、新聞發(fā)布、圖片上傳三個(gè)功能把 GET/POST 請求、Session、文件流全都串起來了。這一章把每條鏈路的關(guān)鍵代碼邏輯拆開看你拿這套代碼去改自己的新聞?wù)緯r(shí)直接在這三個(gè)范式上擴(kuò)展就行。4.1 登錄鑒權(quán)與 Session 生命周期登錄功能是這系統(tǒng)里所有后臺操作的第一道門。OperatorServlet 里的登錄處理會先檢查用戶名和密碼再根據(jù)用戶角色決定跳轉(zhuǎn)目標(biāo)String username request.getParameter(username); String password MD5Util.md5(request.getParameter(password)); User user commonDAO.findUserByLogin(username, password); if (user null) { response.sendRedirect(request.getContextPath() /login.jsp?error1); return; } HttpSession session request.getSession(); session.setAttribute(loginUser, user); session.setMaxInactiveInterval(1800); if (user.getRole() 1) { response.sendRedirect(request.getContextPath() /admin/index.jsp); } else { response.sendRedirect(request.getContextPath() /index); }登錄成功后的關(guān)鍵操作是session.setAttribute(loginUser, user)。登錄狀態(tài)一旦寫入 Session后續(xù) JSP 頁面就能用${sessionScope.loginUser.username}把這個(gè)用戶信息直接渲染到頁面上。setMaxInactiveInterval(1800)表示 30 分鐘不操作自動失效這個(gè)值單位是秒很多人當(dāng)成分鐘傳成 30結(jié)果頁面邊看邊寫三分鐘就被踢出去。后臺頁面的保護(hù)邏輯是在公用 JSP 頭部的 include 文件里做判斷先HttpSession session request.getSession(false);再檢查loginUser是否為 null為空就response.sendRedirect跳回登錄頁。注意這里要用getSession(false)傳 false 表示如果當(dāng)前沒有 Session 就返回 null而不是創(chuàng)建一個(gè)新 Session——不傳參的話每個(gè)未登錄用戶訪問后臺都會被動建一個(gè) Session白白消耗服務(wù)器內(nèi)存。4.2 OperatorServlet 按 action 分發(fā)新增、修改、刪除的優(yōu)雅寫法老項(xiàng)目里多個(gè)后臺操作共用一個(gè) Servlet 是很常見的靠 action 參數(shù)做邏輯分發(fā)。讀完這個(gè)類你就能掌握一個(gè)通用套路protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding(UTF-8); String action request.getParameter(action); if (add.equals(action)) { doAdd(request, response); } else if (update.equals(action)) { doUpdate(request, response); } else if (delete.equals(action)) { doDelete(request, response); } else if (logout.equals(action)) { request.getSession().invalidate(); response.sendRedirect(request.getContextPath() /login.jsp); } else { response.sendError(HttpServletResponse.SC_BAD_REQUEST); } }request.setCharacterEncoding(UTF-8)這一步必須放在讀參數(shù)之前。Tomcat 默認(rèn)的 POST 請求編碼是 ISO-8859-1如果你在解析參數(shù)之后再設(shè)編碼已經(jīng)取得的字符串全是亂碼這種坑一天能翻三次。action 分發(fā)的好處是整個(gè)后臺管理只需要配一次 Servlet 映射新增功能時(shí)在 doPost 里加一個(gè) else if 分支再補(bǔ)一個(gè)doXxx私有方法就行不需要?jiǎng)?web.xml。doAdd 方法里發(fā)布新聞的核心邏輯就是拼參數(shù)調(diào) CommonDAO先取 title、content、categoryId再處理圖片路徑最后組裝成 News 對象傳給commonDAO.insertNews(news)。發(fā)布成功后的標(biāo)準(zhǔn)動作是response.sendRedirect(list)這是 PRG 模式Post-Redirect-Get避免用戶按 F5 刷新時(shí)把同一條新聞插入兩次。4.3 圖片上傳的磁盤存儲方案圖片上傳是傳統(tǒng) Servlet 項(xiàng)目里最「玄學(xué)」的模塊不同容器版本處理方式差別很大。這套系統(tǒng)的 PhotoUpload 類我建議重點(diǎn)看它解決的是「文件存哪、路徑怎么存進(jìn)數(shù)據(jù)庫」兩個(gè)核心問題// 基于 commons-fileupload 包處理 multipart/form-data DiskFileItemFactory factory new DiskFileItemFactory(); ServletFileUpload upload new ServletFileUpload(factory); upload.setFileSizeMax(5 * 1024 * 1024); // 單文件最大5MB ListFileItem items upload.parseRequest(request); String uploadDir getUploadDir(request); // 外部目錄 for (FileItem item : items) { if (!item.isFormField()) { String fileName System.currentTimeMillis() _ item.getName(); File dest new File(uploadDir, fileName); item.write(dest); // 存相對路徑而不是絕對路徑 request.setAttribute(imgPath, upload/ fileName); } }這里最值得學(xué)習(xí)的地方是getUploadDir()的設(shè)計(jì)。常見做法是把圖片放在 Tomcat 部署目錄下比如webapps/ROOT/upload/但這會讓 Tomcat 在 re-deploy 或清理 work 目錄時(shí)把用戶上傳的圖片清掉非???。我一般建議在 Tomcat 外部建一個(gè)獨(dú)立目錄比如/data/uploads/然后在 Tomcat 的 conf/server.xml 里配置虛擬路徑映射用瀏覽器 URL 訪問/upload/xxx.jpg時(shí)自動指向這個(gè)外部目錄。這樣發(fā)布和迭代都不會丟圖片備份也只需要拷一個(gè)文件夾。選型說明如果用的 Servlet 3.0 以上容器也可以用原生的request.getPart(file)接口處理上傳少引一個(gè) commons-fileupload 依賴。但老項(xiàng)目里多數(shù)還是 commons-fileupload 的寫法兼容 Tomcat 6/7 更穩(wěn)。文件命名我用時(shí)間戳加原文件名避免多人同時(shí)上傳同名文件互相覆蓋這是血淚教訓(xùn)——不改這行的系統(tǒng)上線一周就會出現(xiàn) A 傳的照片把 B 傳的替換掉。5. 避坑指南五個(gè)最常見的老問題現(xiàn)象、原因與處理記錄這套代碼跑通不難但我在本地部署和改造的過程中踩了不少坑集中記錄在下邊五條。每一條都是「現(xiàn)象 → 原因 → 解決」三段式照著排查能省下大量時(shí)間。5.1 發(fā)布新聞后正文全是問號數(shù)據(jù)庫中文一片亂碼現(xiàn)象從后臺表單提交中文標(biāo)題JSP 頁面顯示時(shí)正常但打開 MySQL 表一看title字段里全是??問號。原因三層編碼沒有對齊。JSP 頁面聲明的是pageEncodingISO-8859-1數(shù)據(jù)庫表用了latin1而 MySQL 連接 URL 又沒帶characterEncodingutf8。中文在 UTF-8 和 latin1 之間來回轉(zhuǎn)換最終變成問號寫入。解決三步走。第一統(tǒng)一所有 JSP 頁面頭部為pageEncodingUTF-8第二SQL 建表語句全部指定DEFAULT CHARSETutf8mb4第三JDBC URL 追加?useUnicodetruecharacterEncodingutf8并且 Servlet 的 doPost 開頭執(zhí)行request.setCharacterEncoding(UTF-8)。三個(gè)動作缺一不可。5.2 MySQL 驅(qū)動報(bào) ClassNotFoundException 或時(shí)區(qū)異常現(xiàn)象啟動 Tomcat 后訪問頁面控制臺打印java.lang.ClassNotFoundException: com.mysql.jdbc.Driver或者 MySQL 8 下提示The server time zone value й? is unrecognized。原因驅(qū)動 jar 沒進(jìn)WEB-INF/lib或者驅(qū)動類名寫錯(cuò)。MySQL 5.x 用的驅(qū)動類名是com.mysql.jdbc.DriverMySQL 8 換成了com.mysql.cj.jdbc.Driver兩者不能混用。時(shí)區(qū)異常則是 MySQL 8 連接必須顯式聲明 serverTimezone。解決下載對應(yīng)版本的 mysql-connector-java jar 放進(jìn) lib 目錄。MySQL 5.7 配 5.x 驅(qū)動MySQL 8 配 8.x 驅(qū)動并改 DBUtil 的 DRIVER 字段為com.mysql.cj.jdbc.Driver同時(shí) URL 加上serverTimezoneAsia/Shanghai。5.3 圖片上傳成功但頁面上不顯示現(xiàn)象后臺提示上傳成功數(shù)據(jù)庫里img_url也有值但前臺img標(biāo)簽的圖片永遠(yuǎn)顯示不出來瀏覽器 F12 里圖片請求返回 404。原因圖片保存到了 webapps 部署目錄下的 upload 文件夾而 Tomcat 每次重新部署生成新的臨時(shí)目錄舊的文件路徑就失效了。另外相對路徑寫錯(cuò)時(shí)JSP 頁面從根目錄拼出來的 URL 根本不存在。解決把上傳目錄放到 Tomcat 外面然后配置虛擬路徑映射。在conf/server.xml的 Host 節(jié)點(diǎn)里加一行Context path/upload docBase/data/uploads reloadabletrue/這樣實(shí)際存儲是文件系統(tǒng)的/data/uploads請求 URL 訪問的卻是/upload/xxx.jpg部署多少次都不丟文件。5.4 登錄后沒過多久就被踢回登錄頁現(xiàn)象管理員登錄后臺編輯新聞填了幾分鐘內(nèi)容點(diǎn)提交時(shí)提示未登錄跳回 login.jsp。原因session.setMaxInactiveInterval()的參數(shù)被誤寫成 1800 毫秒或者寫成了分鐘值。實(shí)際上這個(gè)接口的單位是秒1800 才是 30 分鐘寫 30 就是 30 秒超時(shí)。解決檢查后臺登錄成功后的設(shè)置代碼確認(rèn)傳入的是秒數(shù)。同時(shí)排查 web.xml 里全局 session-timeout 的值默認(rèn)單位是分鐘別把兩處搞混。如果你用的是集群部署還得改context.xml里的Manager配置共享 Session但單機(jī)項(xiàng)目不用管這層。5.5 后臺修改一條新聞卻同時(shí)出現(xiàn)兩三條臟數(shù)據(jù)現(xiàn)象用戶點(diǎn)「編輯」改完標(biāo)題提交列表頁出現(xiàn)重復(fù)新聞或是修改操作把不該動的行也改了。原因SQL 寫成了不帶WHERE id ?的UPDATE。Executor 里UPDATE news SET title ?漏掉條件會更新全表所有記錄。另外執(zhí)行刪除后沒走重定向?yàn)g覽器刷新時(shí)又提交了一次 POST 請求。解決檢查 CommonDAO 里所有 update 和 delete 語句強(qiáng)制要求帶主鍵條件寫完就自查一遍。刪除和發(fā)布成功后一律用response.sendRedirect()做重定向別用forward()轉(zhuǎn)發(fā)到列表頁從源頭切斷重復(fù)提交的可能。6. 進(jìn)階技巧分頁參數(shù)設(shè)計(jì)、Tomcat 目錄規(guī)劃與上線自檢清單這套系統(tǒng)在功能上已經(jīng)夠用但部署到生產(chǎn)環(huán)境前我建議按下面的思路做一遍整體加固順便把容易出問題的地方提前堵住。分頁參數(shù)設(shè)計(jì)上前端列表頁通常只傳page1、page2這樣的頁碼每頁條數(shù)應(yīng)該由后臺常量控制而不是讓前端任意傳。我在改造時(shí)會定義PageConfig.DEFAULT_PAGE_SIZE 10超過這個(gè)值的傳參直接忽略防止用戶一次拉取 10 萬條數(shù)據(jù)把內(nèi)存打滿。同時(shí)把前一頁、后一頁的鏈接都帶上categoryId流量進(jìn)列表頁時(shí)保持篩選條件不丟——這個(gè)細(xì)節(jié)很多同學(xué)會在聯(lián)調(diào)時(shí)才發(fā)現(xiàn)翻到第二頁分類變成「全部」屬于典型的過濾條件丟失。目錄規(guī)劃上最終形態(tài)建議分成四個(gè)區(qū)域/data/uploads存放用戶圖片/opt/tomcat/webapps/ROOT只放程序產(chǎn)物/var/log/newsapp放運(yùn)行日志數(shù)據(jù)庫備份單獨(dú)用crontab定時(shí)導(dǎo)出到/data/backup/mysql。這套方案把程序、數(shù)據(jù)、日志、備份徹底分離升級系統(tǒng)時(shí)永遠(yuǎn)只需要?jiǎng)?Tomcat 的 webapps 目錄上傳的圖片和日志完全不受影響。上線前檢查清單我會過一遍這幾個(gè)項(xiàng)目檢查項(xiàng)預(yù)期結(jié)果失敗時(shí)的處理全站 JSP 與 Servlet 編碼均為 UTF-8統(tǒng)一改 pageEncoding 并加 CharacterEncodingFilterJDBC URL 參數(shù)含 characterEncoding 與 serverTimezone參照第 3 章配置補(bǔ)全上傳目錄位于 Tomcat 外并有虛擬路徑映射按第 4 章 server.xml 配置Session 超時(shí)后臺 30 分鐘單位秒檢查 setMaxInactiveInterval 與 web.xml生產(chǎn)密碼不出現(xiàn)明文 123456替換為環(huán)境變量或配置文件外置有一年我?guī)湍掣咝5膶?shí)驗(yàn)室趕一個(gè)內(nèi)部新聞?wù)痉桨负瓦@套結(jié)構(gòu)一模一樣。部署前我覺得自己門兒清跳過了清單里的編碼檢查項(xiàng)結(jié)果上線的當(dāng)晚發(fā)布會數(shù)據(jù)正文里凡是帶引號的地方全變成了問號。那次改代碼改到凌晨兩點(diǎn)從那以后我每次部署這類老工程都會強(qiáng)制把這份自檢清單走一遍五條一項(xiàng)不少。這套傳統(tǒng) ServletJSP 的方法雖然老但把它跑通、部署好、踩過一遍坑之后你再看任何框架包裝過的 Java Web 項(xiàng)目都會有底——希望幫到你。本文還有配套的精品資源點(diǎn)擊獲取