:JavaWeb期末作業(yè)實(shí)現(xiàn)與避坑指南)
簡(jiǎn)介這是一款采用JavaWeb技術(shù)棧開發(fā)的賓館管理系統(tǒng)源碼運(yùn)行環(huán)境包括MySQL數(shù)據(jù)庫(kù)、IDEA開發(fā)工具、Tomcat服務(wù)器頁(yè)面與邏輯分別基于JSP和Servlet技術(shù)實(shí)現(xiàn)。項(xiàng)目面向計(jì)算機(jī)相關(guān)專業(yè)學(xué)生的期末作業(yè)與課程設(shè)計(jì)適合需要快速搭建管理系統(tǒng)或完成實(shí)訓(xùn)任務(wù)的在校學(xué)生。壓縮包大小22.34MB包含1370個(gè)文件其中主要文件類型有JSP視圖頁(yè)面、Java業(yè)務(wù)源碼、Servlet控制類、編譯后的class字節(jié)碼、前端樣式與交互腳本、數(shù)據(jù)庫(kù)建表腳本以及XML配置文件完整覆蓋了表示層、業(yè)務(wù)層與數(shù)據(jù)層目錄結(jié)構(gòu)清晰便于按模塊研讀與二次開發(fā)。系統(tǒng)已實(shí)現(xiàn)客房信息管理、預(yù)訂訂單處理、會(huì)員檔案維護(hù)等核心功能界面簡(jiǎn)潔美觀操作流程直觀代碼包含詳細(xì)注釋新手可以理解關(guān)鍵邏輯附帶的文檔說(shuō)明也能幫助讀者快速完成部署并運(yùn)行到Tomcat上省去從零搭建的麻煩。資源已有375人學(xué)習(xí)下載可作為期末大作業(yè)、課程設(shè)計(jì)的高分參考實(shí)用性強(qiáng)也方便按照自身需求擴(kuò)展改造。1. JSPServlet 賓館管理系統(tǒng)JavaWeb 期末作業(yè)最快落地的那一套做 JavaWeb 期末作業(yè)最怕的不是不會(huì)寫代碼而是選錯(cuò)技術(shù)棧、裝錯(cuò)環(huán)境、交上去一個(gè)跑不起來(lái)的半成品。這套基于 MySQL IDEA Tomcat JSP Servlet 的賓館管理系統(tǒng)我拆完之后最大的感受是它把課程設(shè)計(jì)里最容易扣分的那幾塊——登錄鑒權(quán)、房間狀態(tài)流轉(zhuǎn)、訂單日期計(jì)算、后臺(tái)管理——全部用最經(jīng)典的原生 Servlet 實(shí)現(xiàn)了沒有花哨框架每一行都能在答辯現(xiàn)場(chǎng)講清楚。源碼里帶完整注釋和文檔說(shuō)明新手不用猜每個(gè)類是干什么的打開就知道哪段對(duì)應(yīng)哪個(gè)功能。適合拿去做期末大作業(yè)、課程設(shè)計(jì)或者只是想搞明白 JSP Servlet 到底怎么協(xié)作的人。2. 項(xiàng)目結(jié)構(gòu)與技術(shù)選型五個(gè)核心類如何撐起前臺(tái)到后臺(tái)2.1 為什么這個(gè)項(xiàng)目適合課程設(shè)計(jì)很多同學(xué)一上來(lái)就想用 Spring Boot但期末作業(yè)的評(píng)分標(biāo)準(zhǔn)往往不是你用了多新的框架而是你能不能講清楚請(qǐng)求怎么進(jìn)來(lái)、數(shù)據(jù)怎么查、頁(yè)面怎么渲染。Servlet JSP 這套組合雖然老卻是 JavaWeb 的根基老師最熟悉也最容易在答辯時(shí)給出高分。這套賓館管理系統(tǒng)的類文件集中在幾個(gè)關(guān)鍵類上LoginServlet、TraverseDate、Query、ServiceManage、OrderView從名字就能看出它的職責(zé)劃分——登錄驗(yàn)證、日期遍歷、數(shù)據(jù)查詢、服務(wù)管理、訂單視圖每個(gè)類對(duì)應(yīng)一個(gè)明確的功能模塊代碼量適中注釋齊全新手照著讀就能理清 MVC 的脈絡(luò)。技術(shù)選型上MySQL 負(fù)責(zé)數(shù)據(jù)持久化Tomcat 負(fù)責(zé)運(yùn)行環(huán)境IDEA 負(fù)責(zé)開發(fā)調(diào)試JSP 負(fù)責(zé)頁(yè)面展示Servlet 負(fù)責(zé)請(qǐng)求處理。這套組合的好處是沒有太多隱性的依賴配置只要版本匹配跑起來(lái)非常穩(wěn)。我見過(guò)太多期末項(xiàng)目死在框架版本沖突上而這個(gè)項(xiàng)目用的全是 JavaWeb 最基礎(chǔ)的組件任何一本教材里的環(huán)境配置步驟都能適用排查起來(lái)也容易。2.2 核心類文件與功能模塊映射拿到資源后先別急著啟動(dòng)花十分鐘把類文件對(duì)應(yīng)到功能模塊后面調(diào)試會(huì)省很多事。我拆解后的對(duì)應(yīng)關(guān)系如下類文件對(duì)應(yīng)的功能模塊關(guān)鍵職責(zé)LoginServlet管理員登錄接收賬號(hào)密碼、校驗(yàn)數(shù)據(jù)庫(kù)、寫入 SessionTraverseDate日期計(jì)算引擎遍歷入住到退房日期、計(jì)算住宿天數(shù)與房費(fèi)Query數(shù)據(jù)查詢門戶房間信息查詢、訂單狀態(tài)查詢、客戶信息檢索ServiceManage后臺(tái)服務(wù)管理服務(wù)項(xiàng)目增刪改、價(jià)格調(diào)整OrderView訂單視圖層訂單展示、狀態(tài)流轉(zhuǎn)、明細(xì)查看這個(gè)結(jié)構(gòu)的巧妙之處在于每一層都只做一件事。LoginServlet 只負(fù)責(zé)登錄不摻業(yè)務(wù)邏輯TraverseDate 只算日期不碰數(shù)據(jù)庫(kù)Query 只做查詢不做修改。這樣的拆分讓答辯時(shí)可以非常清晰地回答老師的問(wèn)題「你這個(gè)訂單金額是怎么算出來(lái)的」——打開 TraverseDate邏輯一目了然。2.3 數(shù)據(jù)庫(kù)設(shè)計(jì)的四張核心表項(xiàng)目對(duì)應(yīng)的數(shù)據(jù)庫(kù)設(shè)計(jì)也是課程設(shè)計(jì)標(biāo)準(zhǔn)答案級(jí)別的。表結(jié)構(gòu)大致分為四張管理員表t_admin、房間表t_room、訂單表t_order、服務(wù)表t_service。房間表和訂單表通過(guò)房間號(hào)關(guān)聯(lián)訂單表記錄了入住時(shí)間和退房時(shí)間服務(wù)表記錄額外的消費(fèi)項(xiàng)目。表結(jié)構(gòu)的設(shè)計(jì)思路很實(shí)用管理員表只存賬號(hào)密碼房間表存房間號(hào)、類型、價(jià)格、狀態(tài)訂單表存客戶姓名、房間號(hào)、入住日、退房日、總金額服務(wù)表存服務(wù)項(xiàng)目名稱和價(jià)格。四張表沒有任何冗余字段外鍵關(guān)系簡(jiǎn)單SQL 查詢不會(huì)出現(xiàn)復(fù)雜的多表關(guān)聯(lián)對(duì)剛接觸數(shù)據(jù)庫(kù)的初學(xué)者來(lái)說(shuō)非常友好。-- 訂單表核心字段示例 CREATE TABLE t_order ( order_id INT PRIMARY KEY AUTO_INCREMENT, customer_name VARCHAR(50) NOT NULL, room_id INT NOT NULL, checkin_date DATE NOT NULL, checkout_date DATE NOT NULL, total_amount DECIMAL(10,2) DEFAULT 0, order_status VARCHAR(20) DEFAULT 已入住, create_time DATETIME DEFAULT CURRENT_TIMESTAMP );這段建表 SQL 里有兩個(gè)字段值得注意total_amount 默認(rèn)值為 0原因是訂單創(chuàng)建時(shí)可能還沒退房金額需要根據(jù)實(shí)際住宿天數(shù)計(jì)算后更新order_status 用字符串而不是數(shù)字是為了讓 JSP 頁(yè)面直接展示「已入住」「已退房」這樣的中文狀態(tài)省去前端再做一次狀態(tài)映射。這種設(shè)計(jì)很貼合課程設(shè)計(jì)場(chǎng)景——簡(jiǎn)單、直觀、答辯時(shí)不用繞彎子解釋狀態(tài)碼含義。3. 從零部署IDEA Tomcat MySQL 的三件套配置與運(yùn)行驗(yàn)證3.1 MySQL 版本與編碼設(shè)置的第一個(gè)坑部署的第一件事是準(zhǔn)備數(shù)據(jù)庫(kù)。項(xiàng)目是圍繞 MySQL 設(shè)計(jì)的建議直接用 MySQL 5.7 或 8.0 版本。裝好之后不要急著導(dǎo)入數(shù)據(jù)先把字符集設(shè)置好否則 JSP 頁(yè)面顯示中文會(huì)變成問(wèn)號(hào)。我一般會(huì)在 MySQL 配置文件里加上默認(rèn)字符集配置然后重啟服務(wù)再創(chuàng)建數(shù)據(jù)庫(kù)。# 創(chuàng)建數(shù)據(jù)庫(kù)時(shí)強(qiáng)制指定字符集 CREATE DATABASE hotel_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;這里用 utf8mb4 而不是 utf8是因?yàn)樵?MySQL 8.0 里 utf8 實(shí)際上是 utf8mb3遇到一些特殊符號(hào)存不進(jìn)去。課程設(shè)計(jì)項(xiàng)目雖然大概率用不到 emoji但統(tǒng)一用 utf8mb4 可以避免一個(gè)隱藏的中文亂碼問(wèn)題——尤其是 JSP 頁(yè)面提交表單后往數(shù)據(jù)庫(kù)寫入中文編碼鏈路差了任何一環(huán)都會(huì)亂。字符集設(shè)置好后再導(dǎo)入項(xiàng)目附帶的 SQL 文件順序不能反。Tomcat 版本建議用 Tomcat 9對(duì)應(yīng) Servlet 4.0 規(guī)范和項(xiàng)目里的 Servlet 代碼兼容性最好。我遇到過(guò)不少同學(xué)裝 Tomcat 10 跑老項(xiàng)目結(jié)果所有 javax.servlet 包名全部報(bào)錯(cuò)因?yàn)?Tomcat 10 換成了 jakarta.servlet這是版本換代引起的包名遷移問(wèn)題。項(xiàng)目源碼里用的是 javax.servlet配 Tomcat 9 就能完美避開。3.2 IDEA 中配置 Tomcat 的運(yùn)行三步走在 IDEA 里跑這個(gè)項(xiàng)目關(guān)鍵是把 Tomcat 和 Web 項(xiàng)目正確關(guān)聯(lián)。很多同學(xué)在這里翻車是因?yàn)闆]有把項(xiàng)目打成 Artifact 部署到 Tomcat 的 webapps 目錄導(dǎo)致啟動(dòng) Tomcat 后訪問(wèn) 404。# 第一步確保項(xiàng)目結(jié)構(gòu)里有 src/main/webapp 目錄 # IDEA 中右鍵項(xiàng)目 - Add Framework Support - 勾選 Web Application # 第二步配置 Artifact # File - Project Structure - Artifacts - 點(diǎn)擊 - Web Application: Exploded # 第三步配置 Tomcat # Run - Edit Configurations - 點(diǎn)擊 - Tomcat Server - Local # 在 Deployment 標(biāo)簽頁(yè)添加 ArtifactApplication context 填 /hotel這里我詳細(xì)解釋一下第二步和第三步背后的邏輯。IDEA 運(yùn)行 Tomcat 時(shí)并不是直接把項(xiàng)目文件夾丟給 Tomcat而是先打包成一個(gè) Artifact可以理解為編譯后的 Web 項(xiàng)目快照再放到 Tomcat 的容器里執(zhí)行。所以 Artifact 配置錯(cuò)了即使 Tomcat 正常啟動(dòng)頁(yè)面依然訪問(wèn)不到。Application context 填 /hotel意味著訪問(wèn)路徑是 http://localhost:8080/hotel/如果留空則是根路徑。我習(xí)慣填上項(xiàng)目名因?yàn)榇疝q演示時(shí) URL 更直觀。3.3 JDBC 連接的敏感參數(shù)配置項(xiàng)目里的數(shù)據(jù)庫(kù)連接方式是 JDBC通常在 db.properties 文件或工具類里配置連接參數(shù)。打開配置重點(diǎn)檢查四個(gè)參數(shù)驅(qū)動(dòng)類、連接地址、用戶名、密碼。// 數(shù)據(jù)庫(kù)連接工具類核心代碼 public class DBUtil { private static final String DRIVER com.mysql.cj.jdbc.Driver; private static final String URL jdbc:mysql://localhost:3306/hotel_db ?useSSLfalseserverTimezoneAsia/Shanghai; private static final String USER root; private static final String PASSWORD 123456; public static Connection getConnection() throws SQLException { try { Class.forName(DRIVER); } catch (ClassNotFoundException e) { e.printStackTrace(); } return DriverManager.getConnection(URL, USER, PASSWORD); } }注意 URL 里的兩個(gè)參數(shù)useSSLfalse 是必須加的因?yàn)?MySQL 8.0 默認(rèn)開啟 SSL 連接而 Tomcat 本地調(diào)試時(shí)的 SSL 握手經(jīng)常出問(wèn)題報(bào)錯(cuò)信息很迷惑實(shí)際原因就是證書校驗(yàn)失敗serverTimezoneAsia/Shanghai 則是解決日期時(shí)間類的時(shí)區(qū)偏移問(wèn)題不加這個(gè)參數(shù)從數(shù)據(jù)庫(kù)取出 Date 類型時(shí)會(huì)出現(xiàn) 8 小時(shí)誤差導(dǎo)致入住日期和退房日期的天數(shù)計(jì)算全部錯(cuò)位。密碼部分如果你是本地 root 用戶且沒改過(guò)密碼保持 123456 或電腦上實(shí)際的 root 密碼即可。數(shù)據(jù)庫(kù)連接的成功驗(yàn)證方式很簡(jiǎn)單——運(yùn)行一個(gè) main 方法調(diào)用 getConnection()不拋異常就說(shuō)明通了。4. 核心代碼逐段拆解登錄、日期遍歷與訂單查詢的實(shí)現(xiàn)細(xì)節(jié)4.1 LoginServlet 的登錄校驗(yàn)與 Session 管理登錄功能是每個(gè)期末作業(yè)的標(biāo)配但細(xì)節(jié)差異很大。這個(gè)項(xiàng)目的 LoginServlet 寫法非常規(guī)范接收前端 JSP 表單提交的 username 和 password調(diào)用 Query 類去數(shù)據(jù)庫(kù)比對(duì)成功后把用戶信息存入 Session再跳轉(zhuǎn)到主頁(yè)面失敗則返回登錄頁(yè)并附帶錯(cuò)誤提示。WebServlet(/login) public class LoginServlet extends HttpServlet { protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding(UTF-8); String username request.getParameter(username); String password request.getParameter(password); // 調(diào)用查詢層驗(yàn)證賬號(hào)密碼 Query query new Query(); Admin admin query.findAdminByUsernameAndPassword(username, password); if (admin ! null) { HttpSession session request.getSession(); session.setAttribute(currentAdmin, admin); response.sendRedirect(request.getContextPath() /index.jsp); } else { request.setAttribute(errorMsg, 賬號(hào)或密碼錯(cuò)誤); request.getRequestDispatcher(/login.jsp).forward(request, response); } } }兩個(gè)關(guān)鍵細(xì)節(jié)doPost 里的 setCharacterEncoding(UTF-8) 必須放在讀取參數(shù)之前否則表單提交的中文用戶名會(huì)亂碼登錄成功用 sendRedirect 而不是 forward 跳轉(zhuǎn)是為了避免用戶按下 F5 刷新時(shí)重復(fù)提交登錄請(qǐng)求。Session 中存的是 Admin 對(duì)象而不是用戶名這樣主頁(yè)面可以直接從 session 里取出管理員詳細(xì)信息進(jìn)行展示不用再次查詢數(shù)據(jù)庫(kù)。實(shí)際測(cè)試中發(fā)現(xiàn)如果登錄失敗用 forward 返回 login.jsp頁(yè)面上給出的錯(cuò)誤提示能正常顯示這個(gè)設(shè)計(jì)我很喜歡——錯(cuò)誤提示通過(guò) request 域傳遞跳轉(zhuǎn)一次就能看到。// 查詢層方法注意密碼比對(duì)方式 public Admin findAdminByUsernameAndPassword(String username, String password) { Connection conn null; PreparedStatement ps null; ResultSet rs null; try { conn DBUtil.getConnection(); String sql SELECT * FROM t_admin WHERE username? AND password?; ps conn.prepareStatement(sql); ps.setString(1, username); ps.setString(2, password); rs ps.executeQuery(); if (rs.next()) { Admin admin new Admin(); admin.setId(rs.getInt(id)); admin.setUsername(rs.getString(username)); return admin; } } catch (SQLException e) { e.printStackTrace(); } finally { closeResources(rs, ps, conn); } return null; }這里強(qiáng)烈建議使用 PreparedStatement 而不是拼接 SQL 字符串——傳入的用戶名如果包含單引號(hào)拼接 SQL 會(huì)導(dǎo)致語(yǔ)法錯(cuò)誤甚至注入風(fēng)險(xiǎn)。PreparedStatement 的預(yù)編譯機(jī)制自動(dòng)處理了引號(hào)轉(zhuǎn)義是課程設(shè)計(jì)答辯時(shí)老師一定會(huì)問(wèn)的考點(diǎn)。finally 塊里的資源關(guān)閉是另一道加分項(xiàng)很多沒經(jīng)驗(yàn)的代碼會(huì)漏掉連接關(guān)閉長(zhǎng)時(shí)間運(yùn)行后連接池耗盡整個(gè)系統(tǒng)卡死。4.2 TraverseDate 的日期遍歷與房費(fèi)計(jì)算邏輯住宿天數(shù)的計(jì)算是賓館管理系統(tǒng)的核心算法。這個(gè)項(xiàng)目的 TraverseDate 類把邏輯封裝得非常干凈輸入入住日期和退房日期遍歷每一天統(tǒng)計(jì)天數(shù)并計(jì)算總房費(fèi)。// 日期計(jì)算核心邏輯簡(jiǎn)化版 public class TraverseDate { private static final long DAY_IN_MILLIS 24 * 60 * 60 * 1000; public int calculateDays(Date checkinDate, Date checkoutDate) { // 注意日期需要?dú)w一化到零點(diǎn)否則時(shí)間差可能丟失一天 Calendar checkin Calendar.getInstance(); checkin.setTime(checkinDate); checkin.set(Calendar.HOUR_OF_DAY, 0); checkin.set(Calendar.MINUTE, 0); checkin.set(Calendar.SECOND, 0); Calendar checkout Calendar.getInstance(); checkout.setTime(checkoutDate); checkout.set(Calendar.HOUR_OF_DAY, 0); checkout.set(Calendar.MINUTE, 0); checkout.set(Calendar.SECOND, 0); long diff checkout.getTimeInMillis() - checkin.getTimeInMillis(); return (int) (diff / DAY_IN_MILLIS); } public BigDecimal calculateAmount(Date checkin, Date checkout, BigDecimal pricePerDay) { int days calculateDays(checkin, checkout); return pricePerDay.multiply(BigDecimal.valueOf(days)); } }這個(gè)類實(shí)現(xiàn)里有三個(gè)細(xì)節(jié)值得關(guān)注。第一為什么用 Calendar 歸一化時(shí)間因?yàn)閺臄?shù)據(jù)庫(kù)讀出的 Timestamp 可能帶有時(shí)分秒如果入住是 2023-12-01 10:30退房是 2023-12-03 14:00直接算毫秒差是 48 小時(shí)加 3.5 小時(shí)除以 24 小時(shí)后得到 2 天還多強(qiáng)轉(zhuǎn) int 得到 2看起來(lái)沒問(wèn)題但如果退房是 2023-12-03 09:30毫秒差是 47 小時(shí)除以 24 得到 1.958轉(zhuǎn) int 變成 1 天——這就錯(cuò)了。歸一化到零點(diǎn)后再算差邏輯就穩(wěn)了。第二金額用 BigDecimal 而不是 double因?yàn)榉抠M(fèi)涉及小數(shù)計(jì)算double 的精度損失會(huì)在多天累加時(shí)放大。第三這個(gè)方法故意只算了房費(fèi)不包含服務(wù)費(fèi)服務(wù)費(fèi)用由 ServiceManage 單獨(dú)統(tǒng)計(jì)后與房費(fèi)合并——這一設(shè)計(jì)讓每個(gè)類的職責(zé)更聚焦排查問(wèn)題時(shí)不會(huì)一頭霧水。4.3 Query 與 OrderView 的訂單狀態(tài)流轉(zhuǎn)Query 類承擔(dān)了絕大部分?jǐn)?shù)據(jù)庫(kù)查詢工作而 OrderView 負(fù)責(zé)把查詢結(jié)果封裝成視圖對(duì)象供 JSP 渲染。兩者的分工是Query 查原始數(shù)據(jù)OrderView 做數(shù)據(jù)組裝。查詢訂單時(shí)需要關(guān)聯(lián)房間表獲取房間類型和價(jià)格。// 查詢訂單列表關(guān)聯(lián)房間表 public ListOrderView findOrdersWithRoomInfo(String keyword) { ListOrderView orderViews new ArrayList(); Connection conn null; PreparedStatement ps null; ResultSet rs null; try { conn DBUtil.getConnection(); String sql SELECT o.order_id, o.customer_name, o.checkin_date, o.checkout_date, o.total_amount, o.order_status, r.room_type, r.price FROM t_order o LEFT JOIN t_room r ON o.room_id r.room_id ; if (keyword ! null !keyword.isEmpty()) { sql WHERE o.customer_name LIKE ? OR o.order_id ?; } ps conn.prepareStatement(sql); if (keyword ! null !keyword.isEmpty()) { ps.setString(1, % keyword %); ps.setString(2, keyword); } rs ps.executeQuery(); while (rs.next()) { OrderView view new OrderView(); view.setOrderId(rs.getInt(order_id)); view.setCustomerName(rs.getString(customer_name)); view.setRoomType(rs.getString(room_type)); view.setPrice(rs.getBigDecimal(price)); view.setTotalAmount(rs.getBigDecimal(total_amount)); view.setOrderStatus(rs.getString(order_status)); orderViews.add(view); } } catch (SQLException e) { e.printStackTrace(); } finally { closeResources(rs, ps, conn); } return orderViews; }這段查詢的看點(diǎn)在于條件拼接方式先寫基礎(chǔ) SQL再根據(jù)關(guān)鍵字是否為空決定是否拼接 WHERE 子句。注意 WHERE 后面的 AND 問(wèn)題——如果在 WHERE 后面直接寫 AND而當(dāng)關(guān)鍵字為空時(shí) SQL 變成純粹的基礎(chǔ)查詢多出的 AND 就會(huì)語(yǔ)法報(bào)錯(cuò)。這里的處理是先拼 WHERE 再拼接條件用 LIKE 模糊查詢客戶名稱訂單號(hào)用精確匹配參數(shù)均為動(dòng)態(tài)綁定。不要把用戶輸入直接拼進(jìn) SQL 字符串一是安全二是 LIKE 語(yǔ)句里的百分號(hào)和引號(hào)需要轉(zhuǎn)義PreparedStatement 自動(dòng)處理這些。訂單狀態(tài)流轉(zhuǎn)的邏輯在 ServiceManage 里配合使用——退房操作會(huì)將 order_status 從「已入住」改為「已退房」同時(shí)把房間狀態(tài)改為「空閑」。數(shù)據(jù)庫(kù)里 status 字段的更新邏輯很簡(jiǎn)單就是一條 UPDATE 語(yǔ)句但要注意事務(wù)邊界退房操作需要同時(shí)更新訂單表和房間表兩張表的更新必須在一個(gè)事務(wù)中完成否則會(huì)出現(xiàn)訂單已退房但房間仍顯示占用的情況。代碼里通過(guò) Connection 的 setAutoCommit(false) 配合 commit/rollback 實(shí)現(xiàn)事務(wù)控制這是一個(gè)高級(jí)加分點(diǎn)答辯時(shí)可以主動(dòng)提。5. 部署避坑實(shí)錄5 個(gè)讓課程設(shè)計(jì)翻車的常見問(wèn)題與排查5.1 頁(yè)面 404Tomcat 找不到項(xiàng)目資源現(xiàn)象Tomcat 啟動(dòng)成功瀏覽器訪問(wèn) http://localhost:8080/hotel/index.jsp 卻顯示 404。 原因最常見的是 Artifact 沒有正確部署到 Tomcat或者 Application context 配置的和訪問(wèn)路徑不一致。還有一種是 webapp 目錄結(jié)構(gòu)不對(duì)IDEA 新建項(xiàng)目時(shí)如果沒有正確標(biāo)記 webapp 目錄JSP 文件根本不會(huì)被打進(jìn) Artifact。 解決打開 File - Project Structure - Artifacts檢查 Output Layout 里是否包含了 webapp 目錄下的全部文件再打開 Run/Debug Configurations確認(rèn) Deployment 里添加了 Artifact 且 Application context 是 /hotel。如果都沒問(wèn)題清掉 out 目錄和 target 目錄后重啟 Tomcat。5.2 中文亂碼從表單到數(shù)據(jù)庫(kù)全鏈路編碼不一致現(xiàn)象JSP 頁(yè)面輸入中文「張三」存入 MySQL 后變成「?? ??‰」。 原因三處編碼不一致——JSP 頁(yè)面編碼、Servlet 請(qǐng)求編碼、數(shù)據(jù)庫(kù)表字符集。任何一個(gè)環(huán)節(jié)不是 UTF-8中文就保不住。 解決JSP 文件頂部設(shè)置 pageEncoding 為 UTF-8每個(gè) Servlet 的 doPost 開頭寫 request.setCharacterEncoding(UTF-8)連接數(shù)據(jù)庫(kù) URL 加 useUnicodetruecharacterEncodingUTF-8。我遇到過(guò)一個(gè)奇怪場(chǎng)景數(shù)據(jù)庫(kù)表已經(jīng)建好了服了navicat 客戶端顯示正常但程序?qū)懭雭y碼——最后發(fā)現(xiàn)是 MySQL 連接配置沒加 characterEncoding 參數(shù)。從那以后建任何項(xiàng)目我都會(huì)先驗(yàn)證中文寫入再往下走。5.3 MySQL 驅(qū)動(dòng)連接報(bào)錯(cuò)Communications link failure現(xiàn)象Tomcat 啟動(dòng)后訪問(wèn)查詢功能報(bào) Communications link failure 或者 SSL 異常。 原因MySQL 8.0 默認(rèn)開啟 SSL且驅(qū)動(dòng)版本和數(shù)據(jù)庫(kù)版本不匹配。很多人從網(wǎng)上下載的 mysql-connector-java 是舊版 5.x連 8.0 數(shù)據(jù)庫(kù)時(shí)協(xié)議不兼容。 解決使用 mysql-connector-j 8.x 版本驅(qū)動(dòng)連接 URL 加上 useSSLfalse 和 serverTimezoneAsia/Shanghai。檢查驅(qū)動(dòng) JAR 是否放在 WEB-INF/lib 目錄而不是僅加到 IDEA 的依賴列表——運(yùn)行時(shí) Tomcat 只認(rèn) WEB-INF/lib 里的 JARIDEA 編譯通過(guò)不代表 Tomcat 運(yùn)行時(shí)不報(bào)錯(cuò)。5.4 訂單日期計(jì)算差一天時(shí)區(qū)導(dǎo)致的 Date 陷阱現(xiàn)象入住 2024-06-01退房 2024-06-03頁(yè)面顯示住宿 1 天而不是 2 天。 原因數(shù)據(jù)庫(kù)取出的 Date 類型對(duì)象經(jīng)過(guò) JDBC 轉(zhuǎn)換后帶了當(dāng)前虛擬機(jī)的時(shí)區(qū)信息如果 MySQL 連接 URL 沒指定 serverTimezoneJDBC 用默認(rèn)時(shí)區(qū)解析 DATETIME導(dǎo)致時(shí)分秒偏移歸一化失敗后日期差計(jì)算錯(cuò)誤。 解決連接 URL 必須加 serverTimezoneAsia/Shanghai計(jì)算天數(shù)前用 Calendar 歸一化到零點(diǎn)如果時(shí)間緊張最簡(jiǎn)單的驗(yàn)證方式是寫一段測(cè)試代碼故意設(shè)置為 00:00 和 23:59 的時(shí)間看計(jì)算結(jié)果是否符合預(yù)期。這個(gè)坑最容易在答辯前夜爆發(fā)因?yàn)檠菔緯r(shí)日期往往是當(dāng)天的而計(jì)算邏輯對(duì)跨天場(chǎng)景敏感。5.5 SQL 語(yǔ)法異常漏掉空格或者字段名打錯(cuò)現(xiàn)象運(yùn)行時(shí)日志打印 SQLException提示 Unknown column xx in field list 或 You have an error in your SQL syntax。 原因拼接 SQL 字符串時(shí)變量沒有加空格比如 WHERE customer_name LIKE ? AND o.room_id 前后拼接處缺少空格SQL 變成了 WHERE customer_name LIKE ?AND o.room_id。還有一種情況是字段名寫錯(cuò)尤其是有下劃線的字段如 order_status 容易寫成 orderstatus數(shù)據(jù)庫(kù)根本不存在這個(gè)字段報(bào)錯(cuò)信息比較直觀但容易被忽略。 解決用 IDEA 的 SQL 方言檢查功能或者在本地用 Navicat 預(yù)執(zhí)行一遍 SQL 確認(rèn)語(yǔ)法拼接字符串時(shí)每行結(jié)尾保留空格字段名從建表語(yǔ)句里復(fù)制而不是手敲。如果你在 Query 類里用了 StringBuilder 拼 SQL養(yǎng)成習(xí)慣每行結(jié)尾加空格不要依賴下一行的開頭空格。6. 答辯前必做的本地驗(yàn)收用一張檢查清單驗(yàn)證系統(tǒng)完整性期末作業(yè)最慘的不是寫完而是答辯現(xiàn)場(chǎng)演示時(shí)鼠標(biāo)一點(diǎn)就崩。我在這類項(xiàng)目上交過(guò)太多次學(xué)費(fèi)后來(lái)總結(jié)了一個(gè)強(qiáng)制驗(yàn)收流程每次演示前一天必須完整走一遍。先把 Tomcat 啟動(dòng)起來(lái)然后按以下順序逐項(xiàng)驗(yàn)證不要跳步。序號(hào)驗(yàn)證項(xiàng)操作與預(yù)期結(jié)果相關(guān)類1數(shù)據(jù)庫(kù)連接啟動(dòng)后首頁(yè)打開無(wú)報(bào)錯(cuò)DBUtil2錯(cuò)誤密碼登錄提示「賬號(hào)或密碼錯(cuò)誤」并留在登錄頁(yè)LoginServlet3正確密碼登錄跳轉(zhuǎn) index.jsp頁(yè)面右上角顯示管理員名LoginServlet4日期計(jì)算開單選擇 6 月 1 日入住、6 月 3 日退房金額為 2 晚費(fèi)用TraverseDate5訂單查詢按客戶名模糊搜索結(jié)果列表正確展示Query/OrderView6退房操作訂單狀態(tài)變?yōu)橐淹朔糠块g狀態(tài)變?yōu)榭臻eServiceManage7數(shù)據(jù)持久化重啟 Tomcat記錄仍在MySQL這個(gè)清單的妙處在于每項(xiàng)都對(duì)應(yīng)一個(gè)核心類答辯時(shí)如果老師問(wèn)「這項(xiàng)目有哪些功能」你直接背這張表的第 2 到第 6 行每個(gè)功能都能說(shuō)清對(duì)應(yīng)代碼在哪個(gè)類里、核心邏輯是什么。我一般會(huì)在驗(yàn)收時(shí)把 Tomcat 的控制臺(tái)日志打開每一步操作看一眼有沒有報(bào)異常很多隱藏的 SQLException 頁(yè)面不顯示日志里早就有記錄了。關(guān)于文檔說(shuō)明資源里帶的文檔講清楚了建表語(yǔ)句、默認(rèn)賬號(hào)密碼和部署步驟。建議你拿到之后先按文檔跑一遍成功后再自己改功能加功能——比如給訂單表加一個(gè)備注字段練習(xí)從數(shù)據(jù)庫(kù)到 Servlet 到 JSP 的完整鏈路改造。這套項(xiàng)目最好的一點(diǎn)是可擴(kuò)展性極強(qiáng)房間表加一個(gè)床位字段訂單表加一個(gè)押金字段都能順著原有代碼結(jié)構(gòu)快速實(shí)現(xiàn)。從那以后我每次拿到類似項(xiàng)目都會(huì)強(qiáng)制走一遍「先跑基礎(chǔ)流程 → 再改一項(xiàng)小功能 → 最后驗(yàn)證舊功能不被破壞」的三步流程。這套方法論救過(guò)我很多次希望幫到你。本文還有配套的精品資源點(diǎn)擊獲取