實戰(zhàn):JSP+Servlet+MySQL全解析)
簡介在Web開發(fā)學習路徑中理解底層原理往往比直接使用框架更為關(guān)鍵。JSP作為Servlet的模板化擴展本質(zhì)是在服務端動態(tài)生成HTML配合Servlet處理請求分發(fā)、JDBC操作數(shù)據(jù)庫形成了一套完整且透明的請求-響應鏈路。這種經(jīng)典組合能清晰展示HTTP會話管理、用戶鑒權(quán)、SQL查詢與數(shù)據(jù)渲染的每個環(huán)節(jié)非常適合課程設計、畢業(yè)設計及初學者練手。從數(shù)據(jù)庫表結(jié)構(gòu)設計、連接池配置到登錄過濾器的實現(xiàn)再到成績統(tǒng)計的聚合查詢與防重復提交的冪等處理每一步都承載著Web工程的核心實踐。無論是構(gòu)建學生信息管理系統(tǒng)還是其他業(yè)務后臺掌握JSPServletMySQL這套技術(shù)棧都能為后續(xù)學習Spring Boot等高級框架打下堅實基礎。本文以一個學生成績綜合管理系統(tǒng)為藍本完整拆解了從需求分析到故障排查的全過程幫助你快速搭建一個穩(wěn)定可演示的Java Web項目。 做課程設計或者畢業(yè)設計的同學大概率繞不開“學生成績管理系統(tǒng)”這類題目尤其是當題目里明明白白寫著 JavaJSP MySQL 的時候。我身邊不少人一看到 JSP 就皺眉覺得這技術(shù)“老掉牙”了但說句實在話對于學習階段的項目、答辯演示、以及理解 Web 開發(fā)的核心鏈路來說JSP Servlet MySQL 這套組合反而是最透明的每一步請求怎么走、數(shù)據(jù)怎么查、頁面怎么渲染你都能看得一清二楚沒有任何黑盒。這個項目雖然掛著“教學系統(tǒng)”的名字但它其實覆蓋了 Web 項目開發(fā)從零到一的全部關(guān)鍵點——數(shù)據(jù)庫設計、JDBC 操作、Servlet 控制層、JSP 展示層、登錄鑒權(quán)、增刪改查、統(tǒng)計報表一個都不少。學生成績綜合管理系統(tǒng)最適合用來打通 Web 開發(fā)任督二脈也適合那些需要快速交付一個穩(wěn)定可演示系統(tǒng)的同學。這篇文章我就以這個項目為藍本把我實際開發(fā)過程中踩過的坑、優(yōu)化過的細節(jié)、以及那些學校文檔里不會寫的東西全部拆開來講。無論你是要用它當課程設計還是想自己動手做一個完整項目練手都能直接照著做。1. 項目整體設計與需求拆解1.1 技術(shù)選型背后的邏輯為什么 JSP 仍然是練手的最佳選擇先回答一個大家都會問的問題現(xiàn)在企業(yè)里都用 Spring Boot Vue 了為什么還要用 JSP我的看法是學習階段的重點不是“用最新的框架”而是“理解原理”。JSP 的本質(zhì)是 Servlet它在服務端把 Java 代碼和 HTML 模板揉在一起然后編譯成 Servlet 執(zhí)行。用 JSP 做項目你被迫去理解 HTTP 請求是怎么到達服務器的、Session 是怎么維持登錄狀態(tài)的、JDBC 是怎么把數(shù)據(jù)庫記錄變成 Java 對象的。這些底層概念換了 Spring Boot 也一樣重要只是框架幫你藏起來了。這個項目我采用的是經(jīng)典的 JSP Servlet JavaBean 三層結(jié)構(gòu)JSP 層負責頁面展示和表單收集比如登錄頁、學生列表頁、成績錄入頁Servlet 層負責接收請求、調(diào)用業(yè)務邏輯、跳轉(zhuǎn)頁面相當于路由器JavaBean / DAO 層負責封裝數(shù)據(jù)和操作數(shù)據(jù)庫JDBC 的代碼都寫在這一層。MySQL 作為數(shù)據(jù)庫是這套組合里最沒有爭議的選擇。MySQL 社區(qū)版免費、安裝方便、SQL 語法標準而且市面上教程極多遇到任何問題基本一搜就有答案。對于一個成績管理系統(tǒng)它的數(shù)據(jù)量級在幾千到幾萬條記錄MySQL 的性能綽綽有余。1.2 功能模塊劃分一個“綜合管理系統(tǒng)”到底要管什么“綜合”兩個字是這個項目的題眼。它不只是簡單的成績錄入而是把學生、教師、課程、成績、用戶權(quán)限全部串起來。我按照角色的視角把功能拆成四塊模塊主要功能涉及角色系統(tǒng)登錄用戶名密碼驗證、Session 管理、退出登錄管理員/教師/學生學生管理學號、姓名、班級、院系的新增/修改/刪除/查詢管理員課程管理課程編號、課程名、學分、授課教師的新增/修改/刪除管理員/教師成績管理成績錄入、修改、審核、按課程/班級/個人查詢教師/管理員統(tǒng)計報表平均分、最高分、最低分、及格率、分數(shù)段分布全部角色不同角色的權(quán)限差異很關(guān)鍵。學生登錄后只能看到自己的成績不能看到別人的教師登錄后能錄入和修改自己授課課程的成績但不能改其他課程管理員擁有全部權(quán)限。這個權(quán)限控制如果用 Spring Security 做配置一大堆反而看不明白用 Session Filter 手動實現(xiàn)你能清楚知道每一個受保護頁面的訪問是經(jīng)過怎樣的校驗這種“笨辦法”對學習反而更友好。1.3 頁面流程設計從登錄到報表的完整鏈路我建議你先把整個系統(tǒng)的頁面跳轉(zhuǎn)圖畫出來再動手寫代碼。這個系統(tǒng)的核心鏈路是打開登錄頁 index.jsp → 提交表單到 LoginServlet → 查詢用戶表驗證身份 → 查詢角色類型 → 跳轉(zhuǎn)對應主頁admin.jsp / teacher.jsp / student.jsp→ 在主頁點“成績管理”進入 ScoreListServlet → JSP 列表頁展示 → 點“錄入”進入 ScoreAddServlet → 錄入成功返回列表頁。這個鏈路每一步都有對應的文件寫完以后你會對“請求-處理-響應”這套模型有非常具象的理解。我見過太多人上來就寫代碼結(jié)果寫到一半發(fā)現(xiàn)缺了一張表、少了一個 Servlet再回頭改數(shù)據(jù)庫非常痛苦。先把流程圖畫清楚后面寫代碼只需要照著填。2. 數(shù)據(jù)庫設計與核心細節(jié)2.1 五張核心表結(jié)構(gòu)設計數(shù)據(jù)庫設計是這個項目的重中之重。很多人建表很隨意導致后面寫 SQL 的時候各種別扭。我的建議是最少建五張表t_user用戶表、t_student學生表、t_teacher教師表、t_course課程表、t_score成績表。先看建表 SQL這是我從實際項目中抽取的簡化版本字段講求實用CREATE DATABASE IF NOT EXISTS student_score_db DEFAULT CHARACTER SET utf8mb4; USE student_score_db; -- 用戶表存登錄賬號密碼和角色 CREATE TABLE t_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(100) NOT NULL, role VARCHAR(20) NOT NULL COMMENT ADMIN/TEACHER/STUDENT, ref_id INT COMMENT 關(guān)聯(lián)學生表或教師表的主鍵學生和教師登錄用 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 學生表 CREATE TABLE t_student ( id INT PRIMARY KEY AUTO_INCREMENT, student_no VARCHAR(20) NOT NULL UNIQUE COMMENT 學號, name VARCHAR(50) NOT NULL, gender VARCHAR(10), class_name VARCHAR(50), major VARCHAR(50), enroll_year VARCHAR(10) COMMENT 入學年份 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 教師表 CREATE TABLE t_teacher ( id INT PRIMARY KEY AUTO_INCREMENT, teacher_no VARCHAR(20) NOT NULL UNIQUE COMMENT 工號, name VARCHAR(50) NOT NULL, title VARCHAR(30) COMMENT 職稱, department VARCHAR(50) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 課程表 CREATE TABLE t_course ( id INT PRIMARY KEY AUTO_INCREMENT, course_no VARCHAR(20) NOT NULL UNIQUE, course_name VARCHAR(100) NOT NULL, credit DECIMAL(3,1) DEFAULT 2.0 COMMENT 學分, teacher_id INT COMMENT 授課教師ID ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 成績表 CREATE TABLE t_score ( id INT PRIMARY KEY AUTO_INCREMENT, student_id INT NOT NULL, course_id INT NOT NULL, score DECIMAL(5,2) COMMENT 成績, semester VARCHAR(20) COMMENT 學期如 2025-2026-1, exam_type VARCHAR(20) DEFAULT 期終 COMMENT 考試類型, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_student_course (student_id, course_id, semester, exam_type) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;這里幾個設計意圖說一下。t_user 表單獨拆出來而不是在學生表和教師表里直接加 username 和 password是為了讓登錄邏輯統(tǒng)一不管什么角色都從 t_user 查查到了再根據(jù) role 去對應表拿詳細信息。ref_id 字段記錄關(guān)聯(lián)的學生或教師主鍵這樣登錄后可以方便地拿到個人信息。t_score 表中我加了一個聯(lián)合唯一鍵uk_student_course (student_id, course_id, semester, exam_type)這非常關(guān)鍵。它的作用是防止同一個學生在同一學期同一門課被重復錄入成績。如果沒有這個約束程序里忘記判斷數(shù)據(jù)庫就可能出現(xiàn)兩條一模一樣的成績記錄統(tǒng)計平均分的時候直接翻車。2.2 外鍵、索引與存儲引擎的選擇關(guān)于外鍵我的建議是在表結(jié)構(gòu)上不加物理外鍵而是使用邏輯關(guān)聯(lián)。這個項目里 t_score.student_id 邏輯上引用 t_student.idt_score.course_id 引用 t_course.id但我不建FOREIGN KEY約束。原因很簡單物理外鍵在刪除數(shù)據(jù)時會有各種限制比如你想刪除一個學生如果他在成績表里有記錄外鍵會阻止刪除或者需要先刪成績這在課程設計答辯演示時非常不靈活。我自己在開發(fā)中更多的做法是在應用層保證數(shù)據(jù)一致性刪除前先查一下有沒有關(guān)聯(lián)數(shù)據(jù)。物理外鍵適合數(shù)據(jù)完整性要求極高、刪除操作極少的生產(chǎn)系統(tǒng)但不適合這種交互頻繁的教學項目。索引方面除了唯一索引我給 t_score 表的 course_id 加了普通索引因為“按課程統(tǒng)計成績”是最常見的查詢ALTER TABLE t_score ADD INDEX idx_course (course_id);2.3 字符集與時區(qū)連接 MySQL 最容易踩的坑建庫的時候定了 utf8mb4連接的時候也得對應上。utf8mb4 和 utf8 的區(qū)別是前者支持 Emoji 表情和生僻字現(xiàn)在建庫無腦用 utf8mb4 就行。JDBC 連接串里要明確指定參數(shù)jdbc:mysql://localhost:3306/student_score_db?useUnicodetruecharacterEncodingutf8mb4serverTimezoneAsia/ShanghaiuseUnicodetruecharacterEncodingutf8mb4是告訴驅(qū)動用 UTF-8 編碼傳輸數(shù)據(jù)serverTimezoneAsia/Shanghai是針對 MySQL 8.x 的時區(qū)設置。如果不加 serverTimezone經(jīng)常會報The server time zone value ?D1ú±ê×?ê±?? is unrecognized這種亂碼報錯。這個我后面在常見問題里再詳細展開。3. 核心代碼實現(xiàn)與實操3.1 項目目錄結(jié)構(gòu)Java Web 項目的經(jīng)典布局用 IDEA 2019 及以上版本創(chuàng)建一個 Web 項目項目的目錄結(jié)構(gòu)長這樣StudentScoreSystem/ ├── src/ │ └── com/score/ │ ├── dao/ │ │ ├── UserDao.java │ │ ├── StudentDao.java │ │ ├── TeacherDao.java │ │ ├── CourseDao.java │ │ └── ScoreDao.java │ ├── entity/ │ │ ├── User.java │ │ ├── Student.java │ │ ├── Teacher.java │ │ ├── Course.java │ │ └── Score.java │ ├── servlet/ │ │ ├── LoginServlet.java │ │ ├── LogoutServlet.java │ │ ├── StudentServlet.java │ │ ├── CourseServlet.java │ │ ├── ScoreQueryServlet.java │ │ └── ScoreAddServlet.java │ └── util/ │ └── DBUtil.java ├── web/ │ ├── index.jsp │ ├── admin/ │ │ ├── admin_home.jsp │ │ ├── student_list.jsp │ │ └── course_list.jsp │ ├── teacher/ │ │ └── teacher_home.jsp │ ├── student/ │ │ └── student_home.jsp │ └── WEB-INF/ │ ├── web.xml │ └── lib/ │ └── mysql-connector-java-8.0.30.jarentity 包里的類就是 JavaBean字段和數(shù)據(jù)庫表一一對應dao 包里的類負責 JDBC 操作servlet 包接收請求調(diào)用 dao再把結(jié)果塞進 request 或 session最后 forward 或 redirect 到 JSP 頁面。分包清晰的好處是后期改了表結(jié)構(gòu)你只需要改 entity 和 daocontrol 層和視圖層基本不用動。3.2 JDBC 連接工具類 DBUtil寫一次到處用JDBC 連接的代碼很固定我把它封裝成一個工具類。這里注意一點連接信息用配置文件管理不要硬編碼在 Java 代碼里。我在 src 目錄下放一個 db.propertiesjdbc.drivercom.mysql.cj.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/student_score_db?useUnicodetruecharacterEncodingutf8mb4serverTimezoneAsia/Shanghai jdbc.usernameroot jdbc.password123456然后 DBUtil 讀取這個配置文件package com.score.util; import java.io.InputStream; import java.sql.Connection; import java.sql.DriverManager; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; import java.util.Properties; public class DBUtil { private static String driver; private static String url; private static String username; private static String password; static { try { InputStream in DBUtil.class.getClassLoader().getResourceAsStream(db.properties); Properties props new Properties(); props.load(in); driver props.getProperty(jdbc.driver); url props.getProperty(jdbc.url); username props.getProperty(jdbc.username); password props.getProperty(jdbc.password); Class.forName(driver); } catch (Exception e) { e.printStackTrace(); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(url, username, password); } public static void close(ResultSet rs, Statement stmt, Connection conn) { try { if (rs ! null) rs.close(); } catch (SQLException e) { e.printStackTrace(); } try { if (stmt ! null) stmt.close(); } catch (SQLException e) { e.printStackTrace(); } try { if (conn ! null) conn.close(); } catch (SQLException e) { e.printStackTrace(); } } }Class.forName(driver)這行代碼如果你用的是 MySQL 8.x 驅(qū)動com.mysql.cj.jdbc.Driver必須要寫MySQL 5.x 的驅(qū)動是 com.mysql.jdbc.Driver不要混。很多同學報ClassNotFoundException就是驅(qū)動類路徑寫錯或者 jar 包沒放到 WEB-INF/lib 下。注意關(guān)資源的順序是 ResultSet → Statement → Connection先開后關(guān)。我見過有人只關(guān)了 Connection不關(guān) Statement 和 ResultSet短時間看不出問題但連接池一緊張就會報連接泄漏。3.3 登錄鑒權(quán)與 Session 管理別把密碼明文存數(shù)據(jù)庫登錄功能是整個系統(tǒng)的門面也是過濾器邏輯的核心。先看 LoginServlet 的核心邏輯package com.score.servlet; import com.score.dao.UserDao; import com.score.entity.User; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.*; import java.io.IOException; 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); UserDao userDao new UserDao(); User user userDao.findByUsernameAndPassword(username, password); if (user ! null) { HttpSession session request.getSession(); session.setAttribute(loginUser, user); session.setMaxInactiveInterval(30 * 60); // 30分鐘無操作自動過期 if (ADMIN.equals(user.getRole())) { response.sendRedirect(request.getContextPath() /admin/admin_home.jsp); } else if (TEACHER.equals(user.getRole())) { response.sendRedirect(request.getContextPath() /teacher/teacher_home.jsp); } else { response.sendRedirect(request.getContextPath() /student/student_home.jsp); } } else { request.setAttribute(msg, 用戶名或密碼錯誤); request.getRequestDispatcher(/index.jsp).forward(request, response); } } }密碼存儲這里我多說兩句。課程設計里很多人直接明文存密碼圖省事但我覺得做項目還是要有基本的安全意識。正確做法是存 MD5 或 SHA-256 的哈希值甚至加鹽。舉一個簡單實現(xiàn)用戶注冊時用DigestUtils.md5Hex(password salt)存進去登錄校驗時把輸入密碼做同樣的哈希再比對。JDK 自帶 MessageDigest不需要引額外依賴import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; public class MD5Util { public static String md5(String source) { try { MessageDigest md MessageDigest.getInstance(MD5); byte[] bytes md.digest(source.getBytes(UTF-8)); StringBuilder sb new StringBuilder(); for (byte b : bytes) { String hex Integer.toHexString(b 0xff); if (hex.length() 1) sb.append(0); sb.append(hex); } return sb.toString(); } catch (Exception e) { throw new RuntimeException(e); } } }登錄之后怎么保護頁面用 Filter 是最標準的做法。LoginFilter 攔截所有 /admin/、/teacher/、/student/* 的請求檢查 session 里有沒有 loginUser沒有就跳回登錄頁package com.score.filter; import javax.servlet.*; import javax.servlet.annotation.WebFilter; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import java.io.IOException; WebFilter(/admin/*) public class AdminFilter 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(loginUser) null) { response.sendRedirect(request.getContextPath() /index.jsp); return; } // 這里還可以繼續(xù)判斷角色不是 ADMIN 就 403 chain.doFilter(req, resp); } }這里有個小細節(jié)request.getSession(false)而不是getSession()。傳 false 表示如果當前沒有 Session 就返回 null而不是創(chuàng)建一個新的。如果你傳 true攻擊者隨便訪問一個受保護頁面就能讓服務器創(chuàng)建 Session浪費內(nèi)存也不是好習慣。3.4 成績錄入與統(tǒng)計報表SQL 是系統(tǒng)的靈魂成績查詢是整個系統(tǒng)最花心思的部分?;拘枨笫前凑n程查詢成績單顯示學號、姓名、班級、成績。這個時候必須用 JOIN 而不是單表查詢。看這條核心 SQLSELECT s.student_no, s.name, s.class_name, c.course_name, sc.score, sc.semester FROM t_score sc JOIN t_student s ON sc.student_id s.id JOIN t_course c ON sc.course_id c.id WHERE c.id ? ORDER BY sc.score DESC;這條 SQL 查出來的結(jié)果能直接映射到 JSP 表格里。在 ScoreQueryServlet 中執(zhí)行查詢遍歷 ResultSet 放到 List再 setAttribute 到 request 轉(zhuǎn)發(fā)給 JSPListMapString, Object list new ArrayList(); while (rs.next()) { MapString, Object row new HashMap(); row.put(student_no, rs.getString(student_no)); row.put(name, rs.getString(name)); row.put(class_name, rs.getString(class_name)); row.put(course_name, rs.getString(course_name)); row.put(score, rs.getBigDecimal(score)); row.put(semester, rs.getString(semester)); list.add(row); } request.setAttribute(scoreList, list);統(tǒng)計報表是課程設計的加分項。我用一條 GROUP BY SQL 算出分數(shù)段分布SELECT SUM(CASE WHEN score 90 THEN 1 ELSE 0 END) AS excellent, SUM(CASE WHEN score 80 AND score 90 THEN 1 ELSE 0 END) AS good, SUM(CASE WHEN score 70 AND score 80 THEN 1 ELSE 0 END) AS medium, SUM(CASE WHEN score 60 AND score 70 THEN 1 ELSE 0 END) AS pass, SUM(CASE WHEN score 60 THEN 1 ELSE 0 END) AS fail, COUNT(*) AS total, ROUND(AVG(score), 2) AS avg_score, MAX(score) AS max_score, MIN(score) AS min_score FROM t_score WHERE course_id ?;CASE WHEN 配合 SUM 的寫法是統(tǒng)計報表的經(jīng)典思路一行 SQL 就能算出所有分段數(shù)據(jù)不用在 Java 里循環(huán)判斷。ROUND 保留兩位小數(shù)讓平均分更美觀。這條 SQL 一寫出來直接在答辯時演示老師一定認可。3.5 成績錄入的冪等處理防止重復提交錄入成績時教師可能手快提交兩次或者同一學生被重復導入。前面我們在表結(jié)構(gòu)上加了聯(lián)合唯一鍵 uk_student_course這里再用INSERT ... ON DUPLICATE KEY UPDATE來兜底INSERT INTO t_score (student_id, course_id, score, semester, exam_type) VALUES (?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE score VALUES(score);這句 SQL 的意思是如果唯一鍵student_id course_id semester exam_type已經(jīng)存在就更新成績不存在就插入。這樣極大簡化了代碼邏輯不用先查一遍判斷是否已存在再決定 insert 還是 update。這個技巧在真實項目中非常常用值得記下來。4. 常見問題與排查技巧實錄4.1 JSP 頁面中文亂碼十個人九個會遇到亂碼問題的根源是編碼不統(tǒng)一。排查思路是順著“請求 → 響應 → 數(shù)據(jù)庫”三層逐一檢查。第一層JSP 頁面編碼。每個 JSP 頂部要寫% page contentTypetext/html;charsetUTF-8 languagejava %第二層Servlet 接收請求的編碼。在 doPost 方法第一行寫request.setCharacterEncoding(UTF-8);第三層數(shù)據(jù)庫連接串前面已經(jīng)提到要帶characterEncodingutf8mb4。只要這三層統(tǒng)一成 UTF-8中文亂碼基本不會出現(xiàn)。如果你發(fā)現(xiàn) JSP 頁面顯示正常但插入數(shù)據(jù)庫后是問號那一定是第三步?jīng)]配置好如果頁面本身出現(xiàn)亂碼那第一步或者第二步的優(yōu)先級搞反了。4.2 數(shù)據(jù)庫連接失敗時區(qū)、驅(qū)動、jar 包三連坑數(shù)據(jù)庫連接失敗是最常見的啟動錯誤我把它整理成了一個排查清單錯誤現(xiàn)象可能原因解決方法ClassNotFoundException驅(qū)動 jar 沒放進 WEB-INF/lib把 mysql-connector-java-x.x.xx.jar 放到 lib 目錄Access denied for user用戶名密碼錯核對 db.propertiesThe server time zone ... unrecognized連接串缺 serverTimezone加上 serverTimezoneAsia/ShanghaiCommunications link failureMySQL 沒啟動或端口不對檢查 MySQL 服務狀態(tài)和 3306 端口檢查端口是否被占用可以用命令netstat -ano | findstr 3306Windows或者lsof -i:3306macOS/Linux。經(jīng)常有同學 MySQL 服務沒啟動就在啟動 Tomcat然后報一堆 Connection refused 的錯先看服務再看代碼。4.3 IDEA 中 Web 項目部署報 404 或 500IDEA 里跑 Web 項目最煩的坑有兩個。第一個是 404。明明代碼沒錯訪問的是/admin/admin_home.jsp卻報 404。八成是 Artifact 配置不對——IDEA 的 Artifact 決定了哪些文件會被打進部署包里。檢查 Project Structure → Artifacts確保Web Resource Directory指向項目的 web 目錄Output Layout里能看到 WEB-INF 和 JSP 文件。修改后一定要 Rebuild 再重啟 Tomcat。第二個是 500 的JasperException或者ClassNotFoundException。這種情況幾乎都是依賴缺失或者編譯輸出目錄不對。確認Project Structure → Modules → Dependencies里是否有 MySQL 驅(qū)動 jar并且 Scope 選擇 Compile。JSP 是在 Tomcat 運行時動態(tài)編譯成 Servlet 的它需要能在 classpath 里找到你的 DAO 和 Entity 類。4.4 成績查詢性能問題幾千條數(shù)據(jù)也卡雖然系統(tǒng)數(shù)據(jù)量不大但我也遇到過查詢變慢的情況原因是 JSP 里嵌套了太多邏輯。舉個例子原本在 score_list.jsp 里為了顯示學生姓名每行都調(diào)用一次 StudentDao 查數(shù)據(jù)庫這就是經(jīng)典的 N1 查詢問題——列表有 50 條成績就要額外執(zhí)行 50 次學生查詢。解決辦法是用 SQL 的 JOIN 一次性把關(guān)聯(lián)數(shù)據(jù)查出來而不是在 JSP 里反復查詢。這也是我前面堅持用 JOIN SQL 的原因。JSP 里只負責遍歷展示絕不在標簽內(nèi)寫 JDBC 代碼。你的 JSP 里如果出現(xiàn)了%加 Java 代碼做數(shù)據(jù)庫操作的痕跡就要注意了——這正是風格老派且難維護的根源。4.5 JSP 導入類失敗或語法高亮不正常有時候 IDEA 里 JSP 文件一片紅色導入的類報錯。這通常是項目沒有正確關(guān)聯(lián) Tomcat 和 JDK。檢查 Project Structure 里是否添加了項目的 SDK并且 Tomcat 的 library 是否引入。IDEA 設置里 JSP 語法高亮也可以手動開啟Settings → Editor → File Types → 確認.jsp文件被識別為 JSP/JSPX。5. 實操過程中的一些體會整個項目做完我自己最大的感受是這種“老技術(shù)?!钡南到y(tǒng)反而能幫你把基礎打得非常牢。寫 JSP Servlet 的過程你會被迫去思考 HTTP 協(xié)議的無狀態(tài)性、Session 的過期機制、JDBC 的資源管理、SQL 的 JOIN 和聚合函數(shù)——這些都是換了任何框架都繞不開的知識點。我在做這個項目的時候最初也犯過“一張大表搞定所有字段”的錯誤把學生、課程、成績?nèi)咳M一張表結(jié)果查詢寫起來非常痛苦。后來拆成五張表邏輯一下就清楚了。這也算是一個經(jīng)驗數(shù)據(jù)庫設計一定先想清楚實體關(guān)系學生和課程是多對多中間要有一張關(guān)聯(lián)表而成績本身就可以作為關(guān)聯(lián)表的屬性存在。最后再分享一個小技巧在答辯或演示之前提前往數(shù)據(jù)庫里插入一批像樣的測試數(shù)據(jù)比如 20 個學生、5 門課、每條成績都有合理的分數(shù)分布這樣統(tǒng)計報表的折線圖、柱狀圖如果做了可視化看起來才會漂亮。如果數(shù)據(jù)全是 100 分或者全是隨機數(shù)老師一眼就覺得是隨便填的。這個項目最大的價值不是那個“成績管理系統(tǒng)”本身而是它訓練了你從需求分析、數(shù)據(jù)庫設計、編碼實現(xiàn)到故障排查的完整思維鏈條。本文還有配套的精品資源點擊獲取