據(jù)庫(kù)系統(tǒng)應(yīng)用開(kāi)發(fā)實(shí)例:JSP/Servlet+JDBC+MySQL源碼視頻解析)
簡(jiǎn)介Java Web數(shù)據(jù)庫(kù)系統(tǒng)應(yīng)用開(kāi)發(fā)與實(shí)例是一份面向Web開(kāi)發(fā)者的源碼與視頻學(xué)習(xí)包內(nèi)容圍繞Java Web和數(shù)據(jù)庫(kù)系統(tǒng)開(kāi)發(fā)展開(kāi)覆蓋Servlet、JSP、JDBC、Spring MVC/Struts以及SQL、事務(wù)、存儲(chǔ)過(guò)程和Hibernate/MyBatis等ORM映射技術(shù)。整個(gè)包共884個(gè)文件約222.5MB核心包括163個(gè)Java源碼、140個(gè)JSP頁(yè)面、181個(gè)編譯后Class文件、17個(gè)SQL腳本和48個(gè)AVI操作演示視頻另有JAR包、XML配置、JS/CSS等輔助資源便于對(duì)照學(xué)習(xí)與二次調(diào)試。已有470人學(xué)習(xí)下載適合希望從基礎(chǔ)到進(jìn)階掌握J(rèn)ava Web與數(shù)據(jù)庫(kù)整合開(kāi)發(fā)的初學(xué)者也適合有一定經(jīng)驗(yàn)的開(kāi)發(fā)者查漏補(bǔ)缺。通過(guò)源碼閱讀和視頻演示可直觀(guān)理解系統(tǒng)演示、共通Servlet封裝、第一個(gè)JSP應(yīng)用、報(bào)表打印等關(guān)鍵場(chǎng)景快速搭建數(shù)據(jù)庫(kù)驅(qū)動(dòng)的Web應(yīng)用并優(yōu)化連接、認(rèn)證與并發(fā)處理。1. Java Web數(shù)據(jù)庫(kù)系統(tǒng)應(yīng)用開(kāi)發(fā)與實(shí)例源碼、視頻和課設(shè)的完整閉環(huán)如果你正在準(zhǔn)備Java Web數(shù)據(jù)庫(kù)方向的課設(shè)最讓人心態(tài)失衡的事往往不是寫(xiě)代碼本身而是找不到一個(gè)“能跑起來(lái)”的參考項(xiàng)目。網(wǎng)上的Demo不少可要么頁(yè)面是靜態(tài)的要么SQL腳本導(dǎo)入就報(bào)錯(cuò)。這套《Java Web數(shù)據(jù)庫(kù)系統(tǒng)應(yīng)用開(kāi)發(fā)與實(shí)例》的源碼與視頻資源解決的就是這個(gè)痛點(diǎn)它以一個(gè)賬戶(hù)信息管理Demo為載體把數(shù)據(jù)庫(kù)建表、JDBC連接、用戶(hù)登錄、增刪改查完整走了一遍。配套視頻按模塊拆開(kāi)講每一步都有對(duì)應(yīng)的代碼位置。適合兩類(lèi)人一是新手想照著視頻從零搭環(huán)境二是已經(jīng)寫(xiě)過(guò)一些Java Web代碼的開(kāi)發(fā)者想直接扒一套規(guī)范的分層代碼來(lái)改。2. 技術(shù)選型與工程結(jié)構(gòu)JSP/Servlet、JDBC、MySQL 怎么湊成一套不翻車(chē)的課設(shè)2.1 為什么還在用 JSP/Servlet課設(shè)場(chǎng)景下的務(wù)實(shí)選擇這套資源沒(méi)有追新技術(shù)棧相當(dāng)克制前端用JSP后端用Servlet數(shù)據(jù)庫(kù)訪(fǎng)問(wèn)用JDBC數(shù)據(jù)庫(kù)用MySQL容器用Tomcat。第一反應(yīng)當(dāng)然是現(xiàn)在不都用Spring Boot嗎但如果你打開(kāi)課程大綱會(huì)發(fā)現(xiàn)很多數(shù)據(jù)庫(kù)系統(tǒng)課程的核心要求仍然是“掌握Servlet生命周期”“使用JDBC完成數(shù)據(jù)庫(kù)增刪改查”。用Spring Boot超綱不說(shuō)演示時(shí)被問(wèn)到“Servlet里為什么不是service()方法”反而容易翻車(chē)。從這個(gè)角度看JSP/Servlet恰恰是最穩(wěn)妥的課設(shè)選擇。另一個(gè)維度是學(xué)習(xí)路徑足夠短。JSP負(fù)責(zé)頁(yè)面展示Servlet負(fù)責(zé)接收請(qǐng)求JDBC負(fù)責(zé)和MySQL交互整條鏈路可以在配套視頻里一步步看到。項(xiàng)目?jī)?nèi)部并不是把所有邏輯堆在JSP里而是拆成了bean、dao、service、servlet、util幾個(gè)包。視頻的順序也按這個(gè)依賴(lài)關(guān)系排先裝環(huán)境再寫(xiě)SQL腳本接著封裝DBUtil最后做Servlet和JSP。跟著視頻走實(shí)際就是按技術(shù)棧的依賴(lài)順序重新搭一遍。如果你之前只寫(xiě)過(guò)單文件Servlet這套結(jié)構(gòu)正好幫你理解“類(lèi)多了以后怎么分工”。2.2 WebRoot、src、sql 目錄先分清三塊再動(dòng)手項(xiàng)目的目錄結(jié)構(gòu)如下ProjectName/ ├─ src/ │ ├─ com/xxx/ │ │ ├─ bean/User.java │ │ ├─ dao/UserDao.java │ │ ├─ service/UserService.java │ │ ├─ servlet/LoginServlet.java │ │ └─ util/DBUtil.java ├─ WebRoot/ │ ├─ css/ │ ├─ WEB-INF/ │ │ └─ web.xml │ ├─ login.jsp │ ├─ list.jsp │ └─ index.jsp ├─ sql/ │ └─ init.sql └─ README.txtsrc下主要看五個(gè)包bean里的User對(duì)應(yīng)數(shù)據(jù)庫(kù)表字段名盡量和表列名保持一致否則ResultSet取值時(shí)容易踩坑dao就是數(shù)據(jù)訪(fǎng)問(wèn)層只負(fù)責(zé)拼SQL、執(zhí)行SQL、把結(jié)果封裝成對(duì)象service是業(yè)務(wù)層比如登錄時(shí)候的密碼校驗(yàn)、刪除前的關(guān)聯(lián)檢查都放這里servlet負(fù)責(zé)接收瀏覽器參數(shù)調(diào)用service最后跳轉(zhuǎn)或轉(zhuǎn)發(fā)util里放DBUtil這類(lèi)公共工具。WebRoot是Web應(yīng)用的根目錄部署到Tomcat后訪(fǎng)問(wèn)路徑就是WebRoot下的文件路徑web.xml用來(lái)配置Servlet映射和歡迎頁(yè)。sql/init.sql是全項(xiàng)目最關(guān)鍵的文件。很多課設(shè)翻車(chē)的第一個(gè)原因不是代碼寫(xiě)錯(cuò)而是沒(méi)先執(zhí)行init.sql。你直接啟動(dòng)項(xiàng)目DBUtil能連上MySQL但登錄查詢(xún)會(huì)報(bào)“Table doesnt exist”。所以拿到資源包以后先看sql目錄把腳本導(dǎo)入數(shù)據(jù)庫(kù)再用工具確認(rèn)表和代碼里的字段對(duì)得上。README.txt一般會(huì)寫(xiě)清楚MySQL用戶(hù)名密碼和初始化步驟我第一次跑的時(shí)候沒(méi)看它直接改代碼里的密碼折騰了半小時(shí)才發(fā)現(xiàn)只是數(shù)據(jù)庫(kù)密碼寫(xiě)錯(cuò)了。這套結(jié)構(gòu)是典型的“傳統(tǒng)Web項(xiàng)目”不是Maven項(xiàng)目。構(gòu)建時(shí)要把src編譯輸出到WEB-INF/classes把mysql-connector-java的jar包放到WEB-INF/lib。如果你看到的是Maven結(jié)構(gòu)那就在pom.xml里寫(xiě)依賴(lài)。判斷方式很簡(jiǎn)單看工程目錄下有沒(méi)有pom.xml有就是Maven沒(méi)有就是傳統(tǒng)Web項(xiàng)目。2.3 JDBC 工具類(lèi)把 DriverManager 封裝成單例省掉一半重復(fù)代碼每個(gè)DAO都要拿連接、關(guān)連接如果不封裝寫(xiě)出來(lái)的代碼會(huì)有大量重復(fù)。所以項(xiàng)目里通常會(huì)有這么個(gè)DBUtil核心代碼大致如下package com.xxx.util; import java.sql.Connection; import java.sql.DriverManager; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; public class DBUtil { private static final String URL jdbc:mysql://localhost:3306/account_db?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai; private static final String USER root; private static final String PASSWORD root; static { try { Class.forName(com.mysql.cj.jdbc.Driver); } catch (ClassNotFoundException e) { e.printStackTrace(); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } public static void close(Connection conn, Statement stmt, ResultSet rs) { if (rs ! null) { try { rs.close(); } catch (SQLException ignored) {} } if (stmt ! null) { try { stmt.close(); } catch (SQLException ignored) {} } if (conn ! null) { try { conn.close(); } catch (SQLException ignored) {} } } }URL里的參數(shù)值得逐個(gè)記一下。useUnicodetrue和characterEncodingutf8是為了讓中文能以UTF-8寫(xiě)入useSSLfalse是因?yàn)楸镜亻_(kāi)發(fā)不加密不加的話(huà)MySQL 8.0會(huì)一直報(bào)警告serverTimezoneAsia/Shanghai是MySQL 8.0之后才需要的時(shí)區(qū)參數(shù)少了它會(huì)在獲取連接時(shí)報(bào)時(shí)間差錯(cuò)誤。驅(qū)動(dòng)類(lèi)名也有版本講究mysql-connector 8.x對(duì)應(yīng)com.mysql.cj.jdbc.Driver8.0之前是com.mysql.jdbc.Driver。視頻里如果第一次用5.x后來(lái)?yè)Q8.0不少人就是在這里栽跟頭。close方法先關(guān)ResultSet再關(guān)Statement最后關(guān)Connection。這個(gè)順序不能亂因?yàn)镽esultSet依賴(lài)StatementStatement又依賴(lài)Connection。我見(jiàn)過(guò)有人先關(guān)Connection再讀ResultSet的時(shí)候直接空指針還以為是自己SQL寫(xiě)錯(cuò)了。這套DBUtil是教學(xué)項(xiàng)目里最常見(jiàn)的寫(xiě)法。如果以后要上生產(chǎn)建議把URL、用戶(hù)名、密碼抽到db.properties里再用ResourceBundle讀取改配置不用改代碼。課設(shè)沒(méi)這個(gè)要求但作為二次開(kāi)發(fā)習(xí)慣提前做沒(méi)壞處。3. 核心模塊拆解一個(gè)登錄請(qǐng)求從表單到 Session 再到數(shù)據(jù)庫(kù)的完整鏈路3.1 登錄模塊表單提交后 Servlet 里發(fā)生了什么登錄邏輯看起來(lái)簡(jiǎn)單卻是很多問(wèn)題的集中地。先看LoginServlet的doPostWebServlet(/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); UserService userService new UserService(); User user userService.login(username, password); if (user ! null) { HttpSession session request.getSession(); session.setAttribute(loginUser, user); response.sendRedirect(request.getContextPath() /list); } else { request.setAttribute(errorMsg, 用戶(hù)名或密碼錯(cuò)誤); request.getRequestDispatcher(/login.jsp).forward(request, response); } } }request.setCharacterEncoding(UTF-8)這一行必須在getParameter之前執(zhí)行。如果放在之后POST請(qǐng)求里的中文參數(shù)會(huì)按容器默認(rèn)編碼解析常見(jiàn)表現(xiàn)是用戶(hù)名里的中文變成亂碼。登錄成功以后把User對(duì)象放進(jìn)Session后續(xù)JSP頁(yè)面才能通過(guò)session里的loginUser判斷“是否已登錄”。這里用的是sendRedirect而不是forward好處是瀏覽器地址欄變成/list用戶(hù)按F5刷新時(shí)不會(huì)重復(fù)提交登錄表單。登錄失敗則用forward把錯(cuò)誤提示帶回login.jsprequest里的errorMsg能在頁(yè)面上顯示。很多人會(huì)問(wèn)為什么Servlet里不直接寫(xiě)SQL因?yàn)榇a一旦寫(xiě)成Servlet里new一個(gè)UserDao再查庫(kù)后續(xù)要加“連續(xù)輸錯(cuò)三次鎖定賬號(hào)”這類(lèi)規(guī)則時(shí)就得改Servlet。項(xiàng)目里用UserService包了一層登錄這種業(yè)務(wù)邏輯放在service里。UserService內(nèi)部再調(diào)用UserDao這樣Servlet只負(fù)責(zé)參數(shù)解析和頁(yè)面跳轉(zhuǎn)職責(zé)很干凈。密碼字段在視頻里是明文存儲(chǔ)這是教學(xué)項(xiàng)目為了方便演示。如果你要做成作品展示至少要改成MD5加鹽或者直接用BCrypt。我一般會(huì)在service里加一層“從庫(kù)里查出用戶(hù)對(duì)比密碼”的邏輯而不是讓SQL直接寫(xiě)where username? and password?因?yàn)楹笳邲](méi)法在代碼里做加密和登錄次數(shù)限制。3.2 用戶(hù)列表與分頁(yè)LIMIT 的參數(shù)不能靠字符串拼接列表頁(yè)是CRUD里最常改的部分。分頁(yè)查詢(xún)的UserDao實(shí)現(xiàn)大致是這個(gè)樣子public ListUser findPage(int pageNum, int pageSize) { ListUser list new ArrayList(); String sql SELECT id, username, nickname, email FROM user ORDER BY id LIMIT ?, ?; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setInt(1, (pageNum - 1) * pageSize); ps.setInt(2, pageSize); try (ResultSet rs ps.executeQuery()) { while (rs.next()) { User user new User(); user.setId(rs.getInt(id)); user.setUsername(rs.getString(username)); list.add(user); } } } catch (SQLException e) { e.printStackTrace(); } return list; }LIMIT ? ?是MySQL分頁(yè)的標(biāo)準(zhǔn)寫(xiě)法第一個(gè)參數(shù)是偏移量第二個(gè)參數(shù)是每頁(yè)條數(shù)。pageNum從1開(kāi)始所以偏移量等于(pageNum - 1) * pageSize。比如第3頁(yè)每頁(yè)10條就是從第21條開(kāi)始取10條。用PreparedStatement的setInt傳參而不是拼SQL是因?yàn)檫@些參數(shù)來(lái)自頁(yè)面請(qǐng)求一旦拼SQL就存在注入風(fēng)險(xiǎn)。你在瀏覽器把pageSize改成“10 or 11”如果代碼是字符串拼接后面的查詢(xún)條件就會(huì)被改掉。這段代碼沒(méi)有寫(xiě)COUNT查詢(xún)但列表頁(yè)一般要顯示“共幾頁(yè)”所以還需要一個(gè)配套方法public int count() { String sql SELECT COUNT(*) FROM user; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql); ResultSet rs ps.executeQuery()) { if (rs.next()) { return rs.getInt(1); } } catch (SQLException e) { e.printStackTrace(); } return 0; }總頁(yè)數(shù)的計(jì)算方式是(total pageSize - 1) / pageSize。比如總數(shù)是21每頁(yè)10這樣算出的是3頁(yè)而不是2頁(yè)。視頻里通常會(huì)單獨(dú)講這個(gè)細(xì)節(jié)因?yàn)樗钊菀自诹斜眄?yè)翻頁(yè)時(shí)出現(xiàn)最后一頁(yè)少一條的“玄學(xué)”問(wèn)題。3.3 刪除用戶(hù)事務(wù)邊界和子表刪除順序刪除功能的典型場(chǎng)景是用戶(hù)主表一條記錄用戶(hù)角色關(guān)聯(lián)表里還有多條記錄。如果只刪主表外鍵約束會(huì)報(bào)錯(cuò)如果只刪關(guān)聯(lián)表主表留下孤兒數(shù)據(jù)。項(xiàng)目里的刪除邏輯通常會(huì)這樣寫(xiě)public boolean deleteUser(int userId) { Connection conn null; PreparedStatement ps null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); String deleteRole DELETE FROM user_role WHERE user_id ?; ps conn.prepareStatement(deleteRole); ps.setInt(1, userId); ps.executeUpdate(); String deleteUser DELETE FROM user WHERE id ?; ps conn.prepareStatement(deleteUser); ps.setInt(1, userId); ps.executeUpdate(); conn.commit(); return true; } catch (SQLException e) { if (conn ! null) { try { conn.rollback(); } catch (SQLException ex) { e.printStackTrace(); } } e.printStackTrace(); return false; } finally { if (conn ! null) { try { conn.setAutoCommit(true); } catch (SQLException ignored) {} } DBUtil.close(conn, ps, null); } }核心點(diǎn)是setAutoCommit(false)。一旦關(guān)閉自動(dòng)提交后面兩條DELETE要么一起成功要么一起回滾。如果第一條成功第二條失敗commit不會(huì)執(zhí)行rollback會(huì)把第一次刪除也撤銷(xiāo)避免出現(xiàn)“關(guān)聯(lián)表刪了主表還在”的臟數(shù)據(jù)。事務(wù)只包寫(xiě)操作查詢(xún)不需要包因?yàn)椴樵?xún)不存在一致性問(wèn)題。另一個(gè)細(xì)節(jié)是刪除順序。如果user_role表有外鍵指向user表必須先刪user_role再刪user否則MySQL會(huì)報(bào)外鍵約束失敗。有些項(xiàng)目為了省事把外鍵去掉這樣先刪誰(shuí)都能跑但數(shù)據(jù)完整性就丟了。我建議保留外鍵讓數(shù)據(jù)庫(kù)幫你兜底演示時(shí)還可以順勢(shì)講一句“外鍵約束保證了引用的有效性”這對(duì)課設(shè)評(píng)分是加分項(xiàng)。更新操作和刪除類(lèi)似但要注意executeUpdate的返回值。返回0表示影響行數(shù)為0常見(jiàn)原因是數(shù)據(jù)已經(jīng)被別人刪了或者你提交的id根本不存在。前端提示“更新失敗”總比悄無(wú)聲息成功要好。4. 從導(dǎo)入到跑通Tomcat和MySQL下的部署步驟與配置檢查4.1 環(huán)境準(zhǔn)備JDK、Tomcat、MySQL 版本怎么選視頻里從頭講環(huán)境搭建但你不需要完全復(fù)刻版本。JDK 1.8、Tomcat 8/9、MySQL 5.7或8.0這是教學(xué)項(xiàng)目最保險(xiǎn)的組合。如果你的機(jī)器已經(jīng)裝了JDK 11甚至17傳統(tǒng)JSP項(xiàng)目在Tomcat 9上也能跑但要注意Tomcat 10及以上把包名換成了jakarta.*老項(xiàng)目的javax.*代碼直接編譯不過(guò)。為了少折騰我建議直接用JDK 8和Tomcat 8.5。版本對(duì)照可以看這張表組件推薦版本注意點(diǎn)JDK1.8Servlet編譯兼容性最好Tomcat8.5 / 9.0WebServlet注解可用MySQL5.7 / 8.08.0需要serverTimezone參數(shù)mysql-connector-java5.1.x / 8.0.x驅(qū)動(dòng)類(lèi)名不一樣MySQL 5.7的好處是省心驅(qū)動(dòng)類(lèi)名用com.mysql.jdbc.Driver不需要時(shí)區(qū)參數(shù)。MySQL 8.0性能更好但sql_mode和排序規(guī)則更容易出幺蛾子。視頻里演示用的具體版本我不重復(fù)原則是跟著視頻版本走別自己混搭最新版。尤其是mysql-connector 8.0.x對(duì)MySQL 5.7也能連反過(guò)來(lái)5.1.x連8.0會(huì)報(bào)SSL握手失敗這里面坑很多。4.2 把工程導(dǎo)入 IDE傳統(tǒng)Web項(xiàng)目與 WEB-INF 檢查拿到源碼后先看是不是Maven工程。傳統(tǒng)Web項(xiàng)目用Eclipse導(dǎo)入File Import Existing Projects into WorkspaceIDEA則直接Open目錄然后在Project Structure里設(shè)置SDK和Web Facet。導(dǎo)入后最重要的一步是檢查WEB-INF/lib下有沒(méi)有mysql-connector的jar包。沒(méi)有的話(huà)啟動(dòng)后跑一次登錄就會(huì)報(bào)ClassNotFoundException。IDEA里傳統(tǒng)Web項(xiàng)目啟動(dòng)前還要設(shè)置Artifacts。常見(jiàn)翻車(chē)現(xiàn)場(chǎng)是Tomcat啟動(dòng)成功瀏覽器訪(fǎng)問(wèn)login.jsp能打開(kāi)但一點(diǎn)登錄就404控制臺(tái)提示Servlet沒(méi)找到。原因通常是.class文件沒(méi)有編譯輸出到WEB-INF/classes或者WebServlet注解沒(méi)有被掃描。解決方法是重新Build Artifacts并在Run Configuration的Deployment里確認(rèn)war exploded已經(jīng)加進(jìn)去了。如果是Maven工程導(dǎo)入后會(huì)自動(dòng)下載依賴(lài)。等待進(jìn)度條走完再啟動(dòng)否則JSP頁(yè)面里用了JSTL標(biāo)簽時(shí)會(huì)報(bào)“Cannot find the tag library descriptor for core”。JSTL需要jstl和standard兩個(gè)jar缺一不可。部署前建議按這個(gè)順序過(guò)一遍項(xiàng)目編譯無(wú)紅叉、WEB-INF/lib能看到驅(qū)動(dòng)、Tomcat啟動(dòng)無(wú)異常、訪(fǎng)問(wèn)http://localhost:8080/項(xiàng)目名/login.jsp能正常顯示。4.3 執(zhí)行 init.sql初始化數(shù)據(jù)庫(kù)的三步檢查初始化數(shù)據(jù)庫(kù)不是“雙擊執(zhí)行”就完事。第一步確認(rèn)腳本里是否已經(jīng)包含CREATE DATABASE。如果包含你直接執(zhí)行就行如果不包含需要手動(dòng)創(chuàng)建數(shù)據(jù)庫(kù)再u(mài)se進(jìn)入。第二步檢查腳本里的表名和UserDao里的表名是否一致。我見(jiàn)過(guò)一個(gè)項(xiàng)目代碼里寫(xiě)的表名是user腳本里建的表名是users啟動(dòng)不報(bào)錯(cuò)但查詢(xún)永遠(yuǎn)返回空。第三步在DBUtil或db.properties里核對(duì)用戶(hù)名、密碼特別是密碼里有特殊字符時(shí)URL里偶爾還需要做URL編碼。還有一條實(shí)戰(zhàn)經(jīng)驗(yàn)數(shù)據(jù)庫(kù)連不上時(shí)先別懷疑代碼。在終端用mysql -uroot -p試一下能不能登錄能登錄說(shuō)明MySQL服務(wù)正常。如果終端能連而項(xiàng)目連不上就檢查URL里的端口是否被改過(guò)。MySQL默認(rèn)3306有些機(jī)器裝了多版本MySQL端口改成了3307URL沒(méi)同步就會(huì)一直報(bào)Connection refused。這些都不算代碼邏輯問(wèn)題但占了我?guī)腿伺挪閱?wèn)題的七成時(shí)間。初始化完成后用一條最簡(jiǎn)單查詢(xún)驗(yàn)證SELECT * FROM user; 如果能查出數(shù)據(jù)再啟動(dòng)項(xiàng)目。直接跑項(xiàng)目容易把“SQL腳本沒(méi)執(zhí)行”和“代碼有bug”混在一起到最后你根本不知道是哪里出了問(wèn)題。5. 常見(jiàn)問(wèn)題與避坑404、500、中文亂碼的排查筆記這套源碼視頻在本地跑起來(lái)的過(guò)程里下面幾個(gè)問(wèn)題是出現(xiàn)頻率最高的。我按“現(xiàn)象、原因、解決”三條寫(xiě)方便你直接對(duì)號(hào)入座。5.1 訪(fǎng)問(wèn) JSP 一直 404頁(yè)面像沒(méi)部署現(xiàn)象Tomcat能啟動(dòng)瀏覽器打開(kāi)login.jsp卻顯示404或者訪(fǎng)問(wèn)一個(gè)Servlet地址下載了JSP源碼。原因項(xiàng)目沒(méi)有真正部署到Tomcat或者部署路徑和訪(fǎng)問(wèn)路徑不一致。解決先確認(rèn)IDE里Run Configuration的Deployment中已經(jīng)添加了項(xiàng)目的war或war exploded。Application context建議設(shè)為/項(xiàng)目名訪(fǎng)問(wèn)時(shí)用帶上下文路徑的地址。Eclipse用戶(hù)右鍵項(xiàng)目Run As Run on Server不要直接雙擊jsp文件。Tomcat的webapps目錄下也應(yīng)該看到對(duì)應(yīng)的項(xiàng)目文件夾如果沒(méi)有說(shuō)明部署沒(méi)生效。這類(lèi)問(wèn)題我遇到最多的是IDEA多模塊工程module沒(méi)被加到Artifacts里導(dǎo)致頁(yè)面前端資源能找到但Servlet的class文件不在WEB-INF/classes下。檢查方法是看Tomcat日志里有沒(méi)有打印“Deploying web application directory”有的話(huà)再確認(rèn)WEB-INF/classes里有沒(méi)有LoginServlet.class。5.2 ClassNotFoundException驅(qū)動(dòng)類(lèi)名按版本選現(xiàn)象訪(fǎng)問(wèn)數(shù)據(jù)庫(kù)相關(guān)頁(yè)面控制臺(tái)報(bào)ClassNotFoundException后面跟著com.mysql.jdbc.Driver或com.mysql.cj.jdbc.Driver。原因一是驅(qū)動(dòng)jar沒(méi)有放到WEB-INF/lib二是驅(qū)動(dòng)類(lèi)名和驅(qū)動(dòng)版本不匹配。解決先確認(rèn)mysql-connector-java的版本。8.0.x必須用com.mysql.cj.jdbc.Driver5.1.x用com.mysql.jdbc.Driver。然后把jar放到WEB-INF/lib下重新部署不要放在IDE的external libraries里因?yàn)門(mén)omcat運(yùn)行時(shí)不會(huì)去讀IDE的類(lèi)路徑。Maven項(xiàng)目則要檢查pom.xml里的驅(qū)動(dòng)依賴(lài)scope如果寫(xiě)成了provided或者testTomcat運(yùn)行時(shí)會(huì)找不到類(lèi)。順便說(shuō)一句驅(qū)動(dòng)jar包版本不要亂用最新的8.4、8.5項(xiàng)目里視頻用的是8.0.x系列就保持一致。打包部署的環(huán)境最怕“我更新了一下依賴(lài)然后突然壞了”。5.3 中文亂碼從頁(yè)面到數(shù)據(jù)庫(kù)要統(tǒng)一三處編碼現(xiàn)象表單輸入中文插入MySQL后變成問(wèn)號(hào)或者頁(yè)面顯示“用戶(hù)名”變成“?”開(kāi)頭的一串亂碼。原因JSP頁(yè)面編碼、Servlet請(qǐng)求編碼、JDBC連接編碼三處不一致。解決JSP第一行寫(xiě)pageEncodingUTF-8Servlet在doPost開(kāi)頭第一行寫(xiě)request.setCharacterEncoding(UTF-8)JDBC URL加characterEncodingutf8MySQL建表時(shí)指定DEFAULT CHARACTER SET utf8mb4。三個(gè)地方缺一個(gè)都會(huì)出問(wèn)題但最常見(jiàn)的是漏了Servlet里的setCharacterEncoding因?yàn)轫?yè)面和數(shù)據(jù)庫(kù)都改好了反而這個(gè)不起眼的一行被忽略。Tomcat 8.5之后對(duì)GET請(qǐng)求的參數(shù)默認(rèn)按UTF-8解碼所以很多人在GET請(qǐng)求里不寫(xiě)setCharacterEncoding也沒(méi)事。但POST請(qǐng)求還是建議顯式設(shè)置因?yàn)閾Q成其他容器或Tomcat版本后行為可能不同。課設(shè)演示時(shí)最好三種請(qǐng)求都試一遍避免現(xiàn)場(chǎng)翻車(chē)。5.4 數(shù)據(jù)庫(kù)連接耗盡操作幾次后卡死現(xiàn)象項(xiàng)目剛啟動(dòng)能正常增刪改查翻幾頁(yè)之后越來(lái)越卡最后請(qǐng)求一直轉(zhuǎn)圈控制臺(tái)報(bào)Too many connections或者Connection refused。原因代碼里getConnection后沒(méi)有close連接泄漏MySQL連接數(shù)被占滿(mǎn)。解決在DAO每個(gè)方法里用try-with-resources或者finally里調(diào)用DBUtil.close。關(guān)閉順序按ResultSet、Statement、Connection來(lái)。如果用了連接池還要檢查池的最大連接數(shù)和空閑超時(shí)。排查時(shí)在MySQL執(zhí)行SHOW PROCESSLIST能看到大量Sleep的連接基本就能實(shí)錘。這里有個(gè)容易誤傷的點(diǎn)JDBC的Connection.close不是直接把進(jìn)程殺死而是把連接放回連接池或標(biāo)記為關(guān)閉。如果你在finally里關(guān)閉后后續(xù)又去操作ResultSet會(huì)報(bào)ResultSet closed。所以取值范圍只在try塊內(nèi)取完再關(guān)不要把rs拿到方法外面去用。5.5 視頻里能跑我這兒就是不行版本和緩存現(xiàn)象每一步都跟著視頻做軟件版本也一樣但啟動(dòng)后報(bào)錯(cuò)信息五花八門(mén)。原因多半是驅(qū)動(dòng)jar版本不一致、數(shù)據(jù)庫(kù)端口不對(duì)或者瀏覽器緩存了舊的JSP。解決先暫停視頻逐行對(duì)照自己的配置文件和代碼不要憑記憶寫(xiě)URL。再確認(rèn)MySQL服務(wù)真的啟動(dòng)了Windows下服務(wù)列表可能顯示“已停止”。瀏覽器按CtrlF5強(qiáng)制刷新排除舊頁(yè)面緩存。最后還不行就刪除項(xiàng)目重新導(dǎo)入一次IDEA緩存導(dǎo)致的資源沒(méi)刷新問(wèn)題重導(dǎo)往往能解決。這一條看著像玄學(xué)其實(shí)背后是“環(huán)境狀態(tài)不確定”。我處理這類(lèi)問(wèn)題時(shí)會(huì)寫(xiě)一個(gè)環(huán)境檢查清單JDK版本、Tomcat版本、MySQL版本、驅(qū)動(dòng)版本、端口、字符集。逐個(gè)打勾不猜。很多時(shí)候你盯著代碼看半天最后發(fā)現(xiàn)Tomcat的CATALINA_BASE指到了另一個(gè)安裝目錄項(xiàng)目根本沒(méi)部署到你正在修改的那個(gè)Tomcat上。6. 二次開(kāi)發(fā)指南把單用戶(hù)登錄改造成多角色權(quán)限控制6.1 改造思路登錄后把角色信息放進(jìn) Session這套Demo的Session里只存了User對(duì)象所有登錄用戶(hù)看到的功能都一樣。要做多角色權(quán)限第一步是擴(kuò)展表結(jié)構(gòu)user表加一個(gè)role_id字段再建role表保存角色名比如admin和user。登錄時(shí)除了查User還要通過(guò)role_id查出角色名稱(chēng)。// 登錄成功后額外查詢(xún)角色并放入 Session Role role roleService.findByUserId(user.getId()); session.setAttribute(currentRole, role.getName());第二步在JSP頁(yè)面判斷角色決定是否顯示某個(gè)入口比如管理員才能看到“用戶(hù)管理”鏈接。但頁(yè)面判斷只能算展示層控制真正的控制要在Servlet入口做。6.2 驗(yàn)證方法用普通賬號(hào)試一次管理員頁(yè)面最簡(jiǎn)單可演示的權(quán)限控制是在管理員相關(guān)Servlet的doGet或doPost開(kāi)頭加幾行HttpSession session request.getSession(); if (!admin.equals(session.getAttribute(currentRole))) { response.sendRedirect(request.getContextPath() /login.jsp); return; }這樣輸入普通賬號(hào)登錄后直接訪(fǎng)問(wèn)管理員的Servlet地址會(huì)被彈回登錄頁(yè)瀏覽器地址欄也能看到變化。演示時(shí)先截一張普通賬號(hào)被攔截的圖再換成管理員賬號(hào)訪(fǎng)問(wèn)成功就是這個(gè)功能最好的說(shuō)明。如果需要更完整的攔截再考慮寫(xiě)一個(gè)Filter統(tǒng)一檢查除login之外的路徑。我在第一次給類(lèi)似項(xiàng)目加權(quán)限時(shí)把角色判斷直接塞進(jìn)了JSP頁(yè)面里結(jié)果別人改個(gè)頁(yè)面就能繞過(guò)。后來(lái)我強(qiáng)制要求所有權(quán)限校驗(yàn)都放在Servlet入口完成JSP只負(fù)責(zé)展示。從那以后我每次做Java Web數(shù)據(jù)庫(kù)項(xiàng)目的二次開(kāi)發(fā)都會(huì)先畫(huà)一遍“請(qǐng)求從哪里進(jìn)、角色在哪里校驗(yàn)、失敗回哪里”的路徑圖再動(dòng)手。希望幫到你。本文還有配套的精品資源點(diǎn)擊獲取