物業(yè)管理系統(tǒng)實(shí)戰(zhàn)解析)
很多人做JavaWeb課程設(shè)計(jì)或畢業(yè)設(shè)計(jì)時(shí)第一反應(yīng)就是去網(wǎng)上找個(gè)Spring Boot Vue的模板改改數(shù)據(jù)庫(kù)交差完事。但真到了答辯或復(fù)試一問事務(wù)怎么控制的一問Servlet生命周期直接卡殼。我前陣子帶著一個(gè)學(xué)弟把這個(gè)“基于JavaWeb和MySQL的JSPServlet智能小區(qū)物業(yè)管理系統(tǒng)”從頭到尾完整擼了一遍用的就是最傳統(tǒng)的JSP Servlet Bootstrap JavaScript MySQL那套組合。做完之后我最大的感受是這個(gè)項(xiàng)目看起來“老”但它恰恰是理解JavaWeb底層運(yùn)行邏輯的最佳載體。這套系統(tǒng)不是那種花里胡哨的演示項(xiàng)目它覆蓋了小區(qū)物業(yè)管理的真實(shí)業(yè)務(wù)場(chǎng)景業(yè)主信息維護(hù)、房屋信息綁定、物業(yè)費(fèi)賬單生成與繳費(fèi)、報(bào)修工單流轉(zhuǎn)、投訴建議處理、公告發(fā)布以及最關(guān)鍵的登錄認(rèn)證和權(quán)限控制。技術(shù)棧雖然基礎(chǔ)但五臟俱全。如果你是正在準(zhǔn)備JavaWeb期末項(xiàng)目、畢業(yè)設(shè)計(jì)或者想徹底搞懂Servlet JSP MySQL之間到底怎么協(xié)作的這篇內(nèi)容會(huì)從需求拆解、數(shù)據(jù)庫(kù)設(shè)計(jì)、功能實(shí)現(xiàn)到部署踩坑按我實(shí)際動(dòng)手的思路給你完整過一遍。很多細(xì)節(jié)是網(wǎng)上教程不會(huì)寫的。1. 需求拆解與整體功能設(shè)計(jì)1.1 角色權(quán)限是這類系統(tǒng)的樞紐我以為最開始考慮的不應(yīng)該是“要建幾張表”而是系統(tǒng)給誰用。小區(qū)物業(yè)系統(tǒng)表面上功能很多但剝開看其實(shí)只有三類人拿著鑰匙的業(yè)主、干活的物業(yè)人員、管全局的超級(jí)管理員。業(yè)主端注冊(cè)登錄、綁定房屋、查看自己的賬單、在線繳費(fèi)、提交報(bào)修、投訴、看公告。物業(yè)端處理業(yè)主提交的報(bào)修單、回復(fù)投訴、發(fā)布公告、維護(hù)業(yè)主和房屋信息。管理員端人員管理、角色分配、查看全局?jǐn)?shù)據(jù)比如繳費(fèi)率、報(bào)修量、系統(tǒng)參數(shù)配置。權(quán)限控制如果從一開始沒想清楚后面寫Servlet過濾器的時(shí)候會(huì)非常痛苦。這套系統(tǒng)我采用的是基于“角色標(biāo)識(shí)”的Session控制方案用戶登錄成功后把 user 對(duì)象和 roleId 塞進(jìn) Session后續(xù)每個(gè)需要權(quán)限的Servlet或JSP頁面都先過一遍過濾器。權(quán)限控制的核心不是“頁面隱藏”而是“后端攔截”。很多新手以為只要在頁面上根據(jù)角色隱藏按鈕就夠了實(shí)際上直接訪問Servlet URL是完全繞開頁面的。所以我在項(xiàng)目里寫了一個(gè)全局Filter專門攔截所有 /admin/* 和 /property/* 路徑校驗(yàn)Session里有沒有對(duì)應(yīng)角色的標(biāo)記。1.2 功能模塊劃分要有邊界按業(yè)務(wù)邊界把功能切成四塊后面寫代碼、分工才不亂基礎(chǔ)信息管理樓棟、單元、房屋、業(yè)主檔案這是整個(gè)系統(tǒng)運(yùn)行的地基。收費(fèi)管理賬單生成、繳費(fèi)登記、欠費(fèi)查詢、收費(fèi)統(tǒng)計(jì)這個(gè)模塊最容易暴露SQL和事務(wù)設(shè)計(jì)問題。工單管理報(bào)修單創(chuàng)建、派單、處理進(jìn)度回填、投訴建議處理。運(yùn)營(yíng)支撐系統(tǒng)公告、管理員賬號(hào)管理、登錄日志。邊界劃分的意義在于它是一個(gè)小程序轉(zhuǎn)型為大系統(tǒng)必經(jīng)之路。如果在JSPServlet階段你就有了模塊化意識(shí)以后理解微服務(wù)里的服務(wù)劃分會(huì)自然得多。2. 技術(shù)選型為什么還是JSP Servlet2.1 不是非要守舊而是“裸奔”一次才能理解框架現(xiàn)在做JavaWeb項(xiàng)目絕大多數(shù)教程開場(chǎng)就上Spring Boot。Spring Boot太方便了一個(gè)注解搞定請(qǐng)求映射一個(gè)注解搞定依賴注入。但代價(jià)是你幾乎看不到HTTP請(qǐng)求從Tomcat進(jìn)來之后是如何一步一步走到數(shù)據(jù)庫(kù)再返回頁面的。這套系統(tǒng)堅(jiān)持用JSP Servlet目的就是把這個(gè)鏈路完完整整暴露出來。我讓學(xué)弟自己寫Servlet類自己配置 web.xml自己封裝request.setAttribute和請(qǐng)求轉(zhuǎn)發(fā)。他做完之后說了一句讓我印象很深的話“原來Spring MVC里那些Controller本質(zhì)上就是一個(gè)Servlet殼子?!边@個(gè)理解很值錢。你面試時(shí)如果能把 Servlet 生命周期init - service - destroy、請(qǐng)求到響應(yīng)的流轉(zhuǎn)、JSP本質(zhì)是Servlet這三個(gè)點(diǎn)接上就已經(jīng)碾壓了一堆只會(huì)背框架注解的候選人。2.2 前端增強(qiáng)Bootstrap負(fù)責(zé)“能看”JavaScript負(fù)責(zé)“好用”這套系統(tǒng)的頁面層我采用的是JSP負(fù)責(zé)動(dòng)態(tài)渲染后端數(shù)據(jù)Bootstrap負(fù)責(zé)布柵格和響應(yīng)式布局JavaScript負(fù)責(zé)交互校驗(yàn)和異步請(qǐng)求。Bootstrap在2024年之后雖然熱度不如Tailwind CSS但在課程設(shè)計(jì)、后臺(tái)管理系統(tǒng)這種傳統(tǒng)場(chǎng)景下它依然是最穩(wěn)的選擇。因?yàn)樗幸惶壮墒斓慕M件庫(kù)和柵格系統(tǒng)你不用自己寫CSS只需要把骨架搭好抄Bootstrap官方文檔里的組件結(jié)構(gòu)頁面就不會(huì)丑到哪里去。JavaScript在這套里的角色遠(yuǎn)不止是表單校驗(yàn)。重點(diǎn)用在了以下幾個(gè)地方前后端聯(lián)調(diào)的Ajax請(qǐng)求特別是繳費(fèi)模塊和報(bào)修狀態(tài)輪詢。前端二次校驗(yàn)比如手機(jī)號(hào)格式、金額合法性、必填項(xiàng)控制。操作確認(rèn)彈窗比如刪除業(yè)主前的二次確認(rèn)。動(dòng)態(tài)渲染聯(lián)動(dòng)下拉框比如選擇了樓棟之后單元下拉框要自動(dòng)刷新。值得多說一句JavaScript 判斷數(shù)據(jù)類型是這門基本功在寫表單校驗(yàn)時(shí)天天用。你可以用 typeof 判斷原始類型用 Object.prototype.toString.call() 判斷引用類型。例如判斷某個(gè)值到底是不是數(shù)組Array.isArray 在部分兼容場(chǎng)景下不夠保險(xiǎn)最穩(wěn)妥的寫法是function isArray(value) { return Object.prototype.toString.call(value) [object Array]; }2.3 JDK Tomcat MySQL版本組合我建議的版本組合是組件版本推薦理由JDK1.8 或 8u202兼容性最廣教程多Tomcat99%適配Tomcat9.xServlet 4.0規(guī)范支持Java EE 8MySQL5.7 或 8.05.7部署簡(jiǎn)單8.0功能新IDEA2023.3及以上內(nèi)置Tomcat集成配置順手如果你在Windows 10上折騰MySQL安裝最忌諱的是下載8.x解壓版卻忘了初始化data目錄。8.x解壓版必須執(zhí)行 mysqld --initialize-insecure 才能生成root空密碼實(shí)例5.7用msi安裝包會(huì)更省心。我個(gè)人建議初學(xué)做課程設(shè)計(jì)直接用5.7的msi版一路Next設(shè)置好root密碼就行少踩很多坑。3. 數(shù)據(jù)庫(kù)設(shè)計(jì)表結(jié)構(gòu)決定系統(tǒng)上限3.1 核心表清單及關(guān)系物業(yè)系統(tǒng)的數(shù)據(jù)模型并不復(fù)雜關(guān)鍵是理清幾個(gè)核心實(shí)體的外鍵關(guān)系。我設(shè)計(jì)的核心表如下我知道你急但你先別急著建表先看關(guān)系設(shè)計(jì)表名用途核心字段tb_user所有登錄賬號(hào)id, username, password, role_id, phonetb_owner業(yè)主信息id, user_id, name, id_card, phone, building_id, house_idtb_building樓棟id, name, address, areatb_house房屋id, building_id, unit, floor, room_num, area, statustb_fee_item收費(fèi)標(biāo)準(zhǔn)id, name, unit_price, fee_typetb_fee_bill物業(yè)繳費(fèi)賬單id, owner_id, item_id, amount, status, deadlinetb_repair報(bào)修工單id, owner_id, repair_type, detail, img, status, create_timetb_complaint投訴建議id, owner_id, content, reply, status, create_timetb_notice公告id, title, content, admin_id, publish_time外鍵關(guān)系說白了就是一句話業(yè)主指向用戶和房屋賬單和工單都指向業(yè)主公告指向管理員。3.2 關(guān)鍵設(shè)計(jì)細(xì)節(jié)賬單生成邏輯物業(yè)繳費(fèi)不能是手動(dòng)一條一條錄的那樣會(huì)累死。我的做法是做一個(gè)“自動(dòng)生成月度賬單”的定時(shí)任務(wù)思路每月1號(hào)掃描 tb_owner 表的房屋面積結(jié)合收費(fèi)標(biāo)準(zhǔn)表里的單價(jià)自動(dòng)計(jì)算物業(yè)費(fèi)并插入 tb_fee_bill 表。對(duì)應(yīng)的核心SQL邏輯大致如下-- 把關(guān)聯(lián)查詢出來的結(jié)果插入賬單表這是典型的INSERT ... SELECT寫法 INSERT INTO tb_fee_bill (owner_id, item_id, amount, status, deadline, create_time) SELECT o.id, fi.id, ROUND(h.area * fi.unit_price, 2), unpaid, DATE_ADD(CURDATE(), INTERVAL 1 MONTH), NOW() FROM tb_owner o JOIN tb_house h ON o.house_id h.id JOIN tb_fee_item fi ON fi.fee_type property WHERE o.status active;這種寫法的好處是把業(yè)務(wù)規(guī)則下沉到SQL層而不是在Java代碼里循環(huán)幾百次再一條條insert那性能會(huì)很差。這張賬單表的值班就是“對(duì)賬依據(jù)”業(yè)主繳費(fèi)后狀態(tài)改為 paid同時(shí)記錄繳費(fèi)時(shí)間。3.3 事務(wù)處理不能忘繳費(fèi)不是單純update一條賬單狀態(tài)它往往伴隨資金流水表的插入。這兩條SQL必須在一個(gè)事務(wù)里。自己在Servlet里控制事務(wù)的寫法比較裸但能讓你看清本質(zhì)Connection conn null; try { conn DBUtils.getConnection(); conn.setAutoCommit(false); // 關(guān)閉自動(dòng)提交開啟事務(wù) // 更新賬單狀態(tài)為已繳 String sql1 UPDATE tb_fee_bill SET statuspaid, pay_timeNOW() WHERE id?; PreparedStatement ps1 conn.prepareStatement(sql1); ps1.setInt(1, billId); ps1.executeUpdate(); // 插入收支流水 String sql2 INSERT INTO tb_pay_log (bill_id, amount, pay_type, create_time) VALUES (?, ?, alipay, NOW()); PreparedStatement ps2 conn.prepareStatement(sql2); ps2.setInt(1, billId); ps2.setBigDecimal(2, amount); ps2.executeUpdate(); conn.commit(); // 都成功才提交 } catch (Exception e) { conn.rollback(); // 任何異常整體回滾 e.printStackTrace(); } finally { if (conn ! null) conn.close(); }MySQL默認(rèn)存儲(chǔ)引擎在5.5之后就是InnoDB支持事務(wù)。但如果你在建表的時(shí)候沒注意把表定義成了MyISAM那事務(wù)是不生效的。建表語句一定要帶 ENGINEInnoDB這是排查“為什么回滾無效”的第一根線。4. 項(xiàng)目骨架搭建從零擼一個(gè)JavaWeb工程的步驟4.1 IDEA創(chuàng)建Web工程的正確姿勢(shì)如果你用IDEA 2023不要選錯(cuò)項(xiàng)目類型。正確路徑是File - New Project - Jakarta EE然后勾選 Web ApplicationApplication Server 選你已經(jīng)配置好的Tomcat。如果你建出來發(fā)現(xiàn)沒有WEB-INF目錄直接右鍵項(xiàng)目 - Add Framework Support - 勾選 Web Application。這個(gè)步驟很基礎(chǔ)但我見過太多人在這里翻車。翻車表現(xiàn)是明明建了項(xiàng)目運(yùn)行的時(shí)候卻提示“Artifact未部署”或“404”。原因就是IDEA只在項(xiàng)目文件里生成了一個(gè)空殼沒有生成Web目錄結(jié)構(gòu)。你需要在 web/WEB-INF 下自己補(bǔ) web.xmlTomcat 9 不強(qiáng)制要web.xml但學(xué)了Servlet規(guī)范手動(dòng)配置一次對(duì)理解URL映射會(huì)非常直觀。4.2 web.xml與Servlet注解的取舍當(dāng)前Servlet 3.0 支持用 WebServlet 注解代替 web.xml 配置更簡(jiǎn)潔。但作為學(xué)習(xí)向的項(xiàng)目我還是建議兩個(gè)都用一用。注解方式寫起來是這樣WebServlet(/login) public class LoginServlet extends HttpServlet { Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { // 處理登錄邏輯 } }用web.xml配置的方式更傳統(tǒng)但是能把“Servlet類 URL映射”這層關(guān)系展示得極為清楚。其實(shí)項(xiàng)目里真正要注意的是一個(gè)Servlet只負(fù)責(zé)一類請(qǐng)求不要寫一個(gè)超級(jí)Servlet然后url-pattern配成 /*那樣后期維護(hù)會(huì)非常痛苦。4.3 分層思想即使沒有框架代碼也要分包這套系統(tǒng)即便沒有用到Spring我也強(qiáng)行把它分成四個(gè)包entity實(shí)體類對(duì)應(yīng)數(shù)據(jù)庫(kù)表。dao數(shù)據(jù)訪問層只負(fù)責(zé)SQL增刪改查。service業(yè)務(wù)邏輯層處理事務(wù)、判斷、計(jì)算。servlet控制層接收請(qǐng)求調(diào)用service轉(zhuǎn)發(fā)結(jié)果。分層的目的不是多余而是讓你在項(xiàng)目里面對(duì)一個(gè)“修改報(bào)修狀態(tài)”的功能時(shí)知道應(yīng)該去哪一層改代碼。如果所有JDBC代碼全寫在Servlet里面兩千行代碼的Servlet會(huì)讓你懷疑人生。5. 核心功能模塊的實(shí)現(xiàn)解析5.1 登錄鑒權(quán)與過濾器登錄邏輯是每一套管理系統(tǒng)的門神。用戶輸入用戶名密碼后Servlet調(diào)用service層的 login 方法查詢用戶表校驗(yàn)密碼同時(shí)查出角色id存Session。登錄成功之后下一步就是做“已登錄保護(hù)”。寫一個(gè)過濾器攔截未登錄用戶訪問受保護(hù)資源。WebFilter(/auth/*) public class AuthFilter implements Filter { public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request (HttpServletRequest) req; HttpServletResponse response (HttpServletResponse) resp; HttpSession session request.getSession(false); if (session null || session.getAttribute(user) null) { response.sendRedirect(request.getContextPath() /login.jsp); return; } chain.doFilter(req, resp); } }注意這里用的是 request.getSession(false)而不是 getSession()。前者拿不到Session就返回null后者拿不到會(huì)強(qiáng)行創(chuàng)建一個(gè)新Session這會(huì)導(dǎo)致“明明未登錄卻因?yàn)樾聞?chuàng)建的Session繞過了校驗(yàn)”的嚴(yán)重安全漏洞。5.2 報(bào)修工單核心狀態(tài)機(jī)報(bào)修模塊是系統(tǒng)中業(yè)務(wù)狀態(tài)最多的地方。它的狀態(tài)流轉(zhuǎn)是業(yè)主提交工單 - 狀態(tài)為“待分配”物業(yè)主管受理 - 狀態(tài)變?yōu)椤疤幚碇小蓖瑫r(shí)指定維修人員維修人員回填結(jié)果 - 狀態(tài)變?yōu)椤耙淹瓿伞睒I(yè)主確認(rèn)/超時(shí)自動(dòng)確認(rèn) - 狀態(tài)變?yōu)椤耙呀Y(jié)束”狀態(tài)用代碼里定義常量表示千萬不要在數(shù)據(jù)庫(kù)里存中文狀態(tài)值。數(shù)據(jù)庫(kù)只存狀態(tài)碼比如 0:待分配, 1:處理中, 2:已完成, 3:已結(jié)束頁面顯示時(shí)再映射成中文。這樣后期如果要擴(kuò)展?fàn)顟B(tài)不需要改數(shù)據(jù)庫(kù)字典表還能在業(yè)務(wù)代碼里做狀態(tài)合法性校驗(yàn)避免出現(xiàn)“直接從待分配跳轉(zhuǎn)到已結(jié)束”的非法流轉(zhuǎn)。5.3 Bootstrap頁面布局實(shí)戰(zhàn)使用Bootstrap最忌諱的是把整個(gè)頁面代碼全塞在一個(gè)JSP里。我的做法是做一個(gè)公共的 header.jsp 和 sidebar.jsp用 include 動(dòng)作引入。% page contentTypetext/html;charsetUTF-8 languagejava % div classcontainer-fluid div classrow div classcol-md-3 bg-dark text-white p-3 !-- 側(cè)邊欄菜單項(xiàng)根據(jù)角色動(dòng)態(tài)顯示 -- /div div classcol-md-9 p-4 jsp:include pagecontent.jsp / /div /div /divBootstrap的柵格系統(tǒng)本質(zhì)上是12列布局你只要記住 col-md-3 占四分之一的寬度col-md-9 占四分之三搭配起來就能做出一個(gè)經(jīng)典的后臺(tái)管理布局。表格直接套 table table-bordered table-hover表單套 form-control整體效果就已經(jīng)像模像樣了。5.4 JavaScript聯(lián)動(dòng)與數(shù)據(jù)校驗(yàn)頁面交互最常用到的場(chǎng)景是選擇樓棟后單元下拉框自動(dòng)加載該樓棟下的單元列表。這個(gè)必須用Ajax異步請(qǐng)求后端Servlet拿到數(shù)據(jù)再動(dòng)態(tài)渲染下拉項(xiàng)。// 樓棟下拉框change事件 $(#buildingId).change(function () { var bid $(this).val(); $.ajax({ url: /property/loadUnits, type: GET, data: { buildingId: bid }, dataType: json, success: function (res) { var unitSelect $(#unitId); unitSelect.empty(); unitSelect.append(option value請(qǐng)選擇單元/option); res.forEach(function (item) { unitSelect.append(option value item.id item.name /option); }); } }); });這里有個(gè)很容易忽略的細(xì)節(jié)返回JSON之前Servlet一定設(shè)了 response.setContentType(application/json;charsetutf-8)否則前端拿到的漢字會(huì)是亂碼。這就是瀏覽器頁面編碼和后端響應(yīng)編碼不一致導(dǎo)致的經(jīng)典坑放以前你可能會(huì)懷疑是數(shù)據(jù)源亂碼其實(shí)根源就是HTTP響應(yīng)頭沒設(shè)置對(duì)。6. 環(huán)境配置三步走JDK Tomcat MySQL6.1 Tomcat配置的關(guān)鍵不是配了就能跑很多人以為IDEA里集成了Tomcat就沒問題了。實(shí)際上你還需要在Project Structure里加一個(gè) ArtifactWeb構(gòu)建產(chǎn)物然后把這個(gè)Artifact部署到Tomcat的運(yùn)行配置里。如果沒加Artifact啟動(dòng)Tomcat會(huì)直接報(bào)錯(cuò)找不到要部署的包。IDEA 2023版本的配置路徑是Run - Edit Configurations - Tomcat Server - Local - Deployment - Add - 選擇你的項(xiàng)目 exploded。注意 artifact 名字里如果帶 .war 和后面的 exploded 是兩個(gè)東西exploded是解壓目錄方式開發(fā)期調(diào)試用這個(gè)即可。6.2 MySQL連接三大“神坑”我把這個(gè)項(xiàng)目里最常踩到的MySQL連接問題列成了排查速查表。現(xiàn)象根本原因解決方案Could not create connection to database serverMySQL服務(wù)未啟動(dòng)以管理員身份打開服務(wù)面板啟動(dòng)MySQL服務(wù)Access denied for user root密碼不對(duì)或權(quán)限為空用命令行登錄重置密碼Unknown database數(shù)據(jù)庫(kù)名不一致確保連接串里dbName和實(shí)際庫(kù)名一致Public Key Retrieval is not allowedMySQL8.0安全沙箱限制JDBC連接串加 allowPublicKeyRetrievaltrueSSL連接錯(cuò)誤證書校驗(yàn)失敗5.7可以忽略8.0改useSSLfalse并關(guān)掉requireSSL如果你是MySQL 8.0連接串盡量這樣寫jdbc:mysql://localhost:3306/property_db?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrueserverTimezone是必須的不然IDEA會(huì)報(bào)服務(wù)器時(shí)區(qū)不一致的異常。characterEncodingutf8 是必須的不然JSP往MySQL插中文必現(xiàn)亂碼。6.3 數(shù)據(jù)庫(kù)初始化腳本我強(qiáng)烈建議不要用手工敲表的方式去建庫(kù)太容易缺字段。把建表腳本和初始化數(shù)據(jù)寫進(jìn)一個(gè) init.sql 文件用Navicat或命令行一次性執(zhí)行。腳本開頭加一句DROP DATABASE IF EXISTS property_db; CREATE DATABASE property_db DEFAULT CHARACTER SET utf8mb4; USE property_db;字符集用 utf8mb4 而不是 utf8因?yàn)镸ySQL的utf8最多只能存三個(gè)字節(jié)遇到生僻字或emoji會(huì)寫入失敗。utf8mb4是MySQL的完整版兼容utf8且支持四個(gè)字節(jié)這是2014年之后做項(xiàng)目的一個(gè)小常識(shí)。7. 常見運(yùn)行錯(cuò)誤與排查技巧實(shí)錄7.1 “Account/read failed during TUI bootstrap”類環(huán)境問題在IDEA里配置Tomcat時(shí)如果你恰好使用了一些自動(dòng)配置插件或IDEA引導(dǎo)框架有可能會(huì)在啟動(dòng)階段卡在類似“account/read failed during tui bootstrap”的錯(cuò)誤。這個(gè)錯(cuò)誤本質(zhì)上是IDEA讀取用戶配置或插件狀態(tài)失敗不是項(xiàng)目代碼的問題。一個(gè)干凈的做法是File - Invalidate Caches / Restart把IDEA緩存清掉重啟如果還是不行就直接在本地Tomcat的conf/server.xml里面刪掉默認(rèn)的Host下多余Context重新部署。核心思路是讓Tomcat和IDEA的耦合降到最低直接把項(xiàng)目打成war丟進(jìn)Tomcat的webapps目錄里跑繞開IDEA自帶的復(fù)雜集成反而更快。7.2 數(shù)據(jù)庫(kù)連接失敗一個(gè)被忽視的MySQL服務(wù)狀態(tài)陷阱MySQL安裝成功不代表服務(wù)已經(jīng)啟動(dòng)。Windows 10下MySQL8.x默認(rèn)不會(huì)自動(dòng)啟動(dòng)服務(wù)。如果你連接數(shù)據(jù)庫(kù)報(bào)錯(cuò)第一個(gè)動(dòng)作是去“服務(wù)”面板看MySQL80到底有沒有在運(yùn)行而不是去懷疑代碼。命令行手動(dòng)啟動(dòng)的方式net start mysql80裝MySQL 8.0.x時(shí)MSI安裝器會(huì)默認(rèn)把服務(wù)名設(shè)為 MySQL80。如果裝的是5.7服務(wù)名一般叫 MySQL57。記住一點(diǎn)命令行里 services.msc 查到的服務(wù)名必須和 net start 后面寫的一致。7.3 JSP頁面找不到或404學(xué)生做這個(gè)項(xiàng)目最常報(bào)的錯(cuò)之一就是Tomcat都正常啟動(dòng)了但訪問 http://localhost:8080/xxx 返回404。排查路徑按順序走看URL有沒有帶項(xiàng)目訪問路徑contextPath如果你的應(yīng)用部署名是 propertyMS那必須訪問 /propertyMS/login.jsp 而不是 /login.jsp??碬EB-INF里的JSP頁面能不能被直接訪問。WEB-INF是被保護(hù)目錄瀏覽器無法直接訪問其中的資源但你部署的時(shí)候如果把頁面放到WEB-INF里面了就必須通過Servlet轉(zhuǎn)發(fā)才能訪問不可直接用URL打開??创a里的路徑加沒加 request.getContextPath()。在JSP里引用CSS或JS文件路徑必須帶著項(xiàng)目名不然全是404。一個(gè)簡(jiǎn)單的辦法是在JSP頭部寫死% String path request.getContextPath(); String basePath request.getScheme() :// request.getServerName() : request.getServerPort() path /; % base href%basePath%這個(gè)base標(biāo)簽加上后頁面里所有的相對(duì)路徑都會(huì)自動(dòng)以項(xiàng)目根路徑為基準(zhǔn)從此不再出現(xiàn)“圖片顯示不出、CSS加載不了”的弱智錯(cuò)誤。7.4 中文亂碼問題一串到底怎么治中文亂碼在JavaWeb里是個(gè)高發(fā)問題但它其實(shí)是一個(gè)長(zhǎng)長(zhǎng)的鏈路只要中間一個(gè)環(huán)節(jié)斷裂必然亂碼。排查順序JSP頁面第一行是否寫了 % page contentTypetext/html;charsetUTF-8 languagejava %。瀏覽器是否以UTF-8解析沒有的話JSP里加。POST請(qǐng)求取參之前有沒有執(zhí)行 request.setCharacterEncoding(UTF-8)。Servlet響應(yīng)時(shí)有沒有 resp.setContentType(text/html;charsetUTF-8)。數(shù)據(jù)庫(kù)連接串有沒有帶 characterEncodingutf8。對(duì)POST請(qǐng)求setCharacterEncoding 寫在 getParameter 之前才有效寫在后面等于沒寫。對(duì)GET請(qǐng)求Tomcat8默認(rèn)字符集是UTF-8Tomcat7及以下要對(duì)get提交參數(shù)做二次解碼最好在Tomcat的server.xml Connector里配 useBodyEncodingForURItrue。7.5 頁面顯示正常但JavaScript報(bào)錯(cuò)最常見的是JS操作的元素沒渲染出來報(bào)錯(cuò)信息是 Uncaught TypeError: Cannot read properties of null。仔細(xì)檢查一下這個(gè)元素到底是不是放在jsp:include里面了如果那個(gè)include里頁面內(nèi)容為空J(rèn)S在DOM沒加載完成時(shí)執(zhí)行就會(huì)拿不到元素。穩(wěn)妥的操作是把你寫的JS代碼放到頁面最底部或包在 $(document).ready() 里。還有一類是前端Ajax請(qǐng)求返回了HTML而不是JSON。這個(gè)大概率是Servlet跳轉(zhuǎn)了頁面或者異常被Tomcat默認(rèn)錯(cuò)誤頁捕獲導(dǎo)致dataType為json但實(shí)際拿到的是HTML就會(huì)直接報(bào)parsererror。排查方式很簡(jiǎn)單瀏覽器F12打開Network標(biāo)簽看那個(gè)XHR請(qǐng)求的Response原文到底是什么內(nèi)容。8. 從課程設(shè)計(jì)到簡(jiǎn)歷項(xiàng)目的進(jìn)階建議8.1 加一點(diǎn)緩存和日志項(xiàng)目質(zhì)感立刻提升如果項(xiàng)目做完了還有時(shí)間我不建議急著擴(kuò)展新模塊先補(bǔ)兩個(gè)工程化能力一是在查詢繳費(fèi)記錄的地方加一個(gè)簡(jiǎn)單的HashMap緩存以業(yè)主id為key緩存最近10條賬單查詢結(jié)果。哪怕你用最簡(jiǎn)單的ConcurrentHashMap也能體現(xiàn)出緩存意識(shí)面試官問到“JSPServlet項(xiàng)目還有什么優(yōu)化點(diǎn)”時(shí)你能答出SQL索引優(yōu)化和緩存手段這比說“我是Java后端開發(fā)”要有說服力得多。二是在關(guān)鍵操作加日志。不用上Log4j直接在Servlet里用JDK自帶的 java.util.logging 或ServletContext.log打印關(guān)鍵參數(shù)。面試問“你項(xiàng)目里怎么排查問題的”你說“我看Tomcat日志”比說“我打斷點(diǎn)調(diào)試”更像生產(chǎn)環(huán)境實(shí)戰(zhàn)。8.2 前端功能點(diǎn)上做一點(diǎn)“超綱”小設(shè)計(jì)除了基礎(chǔ)CRUD我強(qiáng)烈建議在任意一個(gè)頁面用Bootstrap的 Modal 模態(tài)框做新增/編輯操作而不是跳轉(zhuǎn)到另一個(gè)JSP頁面。這一個(gè)細(xì)節(jié)能讓你的項(xiàng)目在答辯演示時(shí)顯得格外流暢。同時(shí)配合JavaScript的 confirm 彈窗做刪除確認(rèn)或者用 SweetAlert 替換難看的原生alert視覺上直接提升一個(gè)檔次。記住一個(gè)原則用戶體驗(yàn)的細(xì)節(jié)往往比功能本身更讓老師覺得“這個(gè)學(xué)生真的有產(chǎn)品思維”。9. 踩坑小結(jié)做完這個(gè)項(xiàng)目最大的體會(huì)這個(gè)JSP Servlet的小區(qū)物業(yè)系統(tǒng)做完后我最大的感受是它像一道“基礎(chǔ)題”但咬合了JavaWeb日常開發(fā)的所有關(guān)鍵環(huán)節(jié)。比如你天天在Spring Boot里用Transactional根本不知道事務(wù)底層需要手動(dòng)管conn.commit和conn.rollback你天天用MyBatis自動(dòng)生成SQL根本不知道預(yù)編譯PreparedStatement是防SQL注入的第一道防線你天天說前后端分離但對(duì)Bootstrap的柵格布局和Ajax異步加載一知半解。做完這個(gè)項(xiàng)目這些坑基本都趟平了。如果再用這個(gè)項(xiàng)目去面試建議你提前把數(shù)據(jù)庫(kù)建表的幾條核心SQL背熟把過濾器的重定向路徑熟記把前端Ajax請(qǐng)求的URL和Servlet的映射關(guān)系講清楚。面對(duì)“你項(xiàng)目里遇到的最大困難是什么”這個(gè)問題你也不愁沒素材了。最后再分享一個(gè)我個(gè)人的操作習(xí)慣每次項(xiàng)目快收尾時(shí)在本地備份一份完整的源碼和數(shù)據(jù)庫(kù)腳本然后重新裝一臺(tái)干凈環(huán)境從零開始跑一遍部署流程。凡是這個(gè)流程里卡殼的地方就是你項(xiàng)目文檔里最該重點(diǎn)寫清楚的地方。這個(gè)習(xí)慣能讓你的項(xiàng)目交付質(zhì)量明顯高于同期水平。