本質)
簡介這是一份面向Java Web初學者與課程設計者的JSP動態(tài)網站實戰(zhàn)源碼基于JSPMySQL實現(xiàn)完整的個人日記管理功能涵蓋用戶登錄、日記增刪改查、分類管理及個人中心等核心模塊適用于高校Web開發(fā)實訓、畢業(yè)設計參考或Servlet/JSP技術入門實踐。壓縮包共90個文件包含18個Java后端邏輯類如Servlet控制器與DAO層、8個JSP頁面登錄頁、日記列表、編輯表單等視圖、27個HTML靜態(tài)模板與21張PNG界面素材輔以6份Markdown說明文檔含中英文README整體僅256KB輕量易讀。目前已有223人學習下載資源結構清晰采用近似MVC分層組織含JDBC數(shù)據(jù)庫連接、Session會話管理、表單驗證與基礎SQL操作等關鍵實現(xiàn)細節(jié)可直接部署運行是理解Java Web基礎架構與前后端交互流程的典型教學案例。1. JSP個人日記本為什么一個“過時”的技術棧反而成了理解Web底層最干凈的教科書你可能第一眼看到“JSP MySQL”就下意識劃走——這不就是十年前高校實驗課的標配連IDEA都默認不帶JSP支持了Tomcat 10干脆把JSP API移出核心包。但恰恰是這份看似落伍的jsp_diary源碼藏著現(xiàn)代Web框架里早已被封裝到黑匣子深處的原始脈搏HTTP請求如何一幀一幀流經容器、Java代碼怎樣在HTML里“呼吸”、session到底存在哪、SQL查詢怎么從字符串變成數(shù)據(jù)庫里的真實行記錄。它沒用Spring Boot自動裝配沒用MyBatis動態(tài)代理所有鏈路都赤裸裸攤開在.jsp、.java和.sql文件里。某高校實驗室曾用它做Web原理課實驗學生第一次親手改完login.jsp里那行request.getParameter(pwd)再在UserServlet.java里打斷點看到密碼明文進來的那一刻才真正信了“表單提交不是魔法”。這不是懷舊而是把Web開發(fā)從“調API”拉回“造輪子”的臨界點——當你能徒手復現(xiàn)一次登錄態(tài)校驗全過程Vue的v-model、React的useState、甚至Next.js的Server Actions就再也不是玄學。2. JSP執(zhí)行機制從.jsp文件到_login_jsp.classTomcat到底做了什么JSP從來不是“直接運行”的腳本而是一套編譯時生成Servlet的約定。這份日記本源碼里所有.jsp文件如login.jsp、diary_list.jsp在首次訪問時都會被Tomcat的Jasper引擎翻譯成標準Java Servlet類再編譯成字節(jié)碼。這個過程暴露了Web容器最原始的契約HTTP請求必須落地為Java對象響應必須包裝成HTTP報文。2.1 JSP編譯流程三步拆解login.jsp的“變形記”以login.jsp為例其核心邏輯是接收表單參數(shù)、調用DAO驗證用戶、跳轉到主頁或返回錯誤。Tomcat處理它的真實路徑如下解析階段Jasper讀取login.jsp識別% %中的Java代碼塊、% %中的表達式、% page %指令將靜態(tài)HTML內容轉為out.write()調用動態(tài)Java代碼原樣保留生成Servlet源碼輸出類似org.apache.jsp.login_jsp.java的Java文件結構為public final class login_jsp extends org.apache.jasper.runtime.HttpJspBase { public void _jspService(HttpServletRequest request, HttpServletResponse response) { // 1. 靜態(tài)HTML轉為out.write() out.write(htmlbody); // 2. 表達式轉為out.print() out.print( request.getParameter(msg) ); // 3. 腳本片段原樣嵌入 String username request.getParameter(username); if (username ! null !username.trim().isEmpty()) { // ... DAO調用邏輯 } } }編譯與加載javac編譯該Java文件為login_jsp.class由JasperLoader加載進JVM后續(xù)請求直接調用_jspService()方法。提示你可以在Tomcat的work/Catalina/localhost/jsp_diary/org/apache/jsp/目錄下找到這些生成的.java和.class文件——這是理解JSP本質的唯一實證入口。刪掉它下次訪問login.jsp會觸發(fā)重新編譯。2.2page指令背后的容器契約isThreadSafe與session的真相l(xiāng)ogin.jsp頂部常有這行% page languagejava contentTypetext/html; charsetUTF-8 pageEncodingUTF-8 sessiontrue isThreadSafefalse %sessiontrue并非“開啟session”而是聲明當前JSP需要訪問HttpSession對象。Tomcat據(jù)此在_jspService()開頭插入HttpSession session request.getSession(true);若設為false則session變量不可用編譯報錯isThreadSafefalse是關鍵陷阱它告訴容器此JSP生成的Servlet不是線程安全的容器會為每個請求創(chuàng)建獨立實例實際是通過同步_jspService方法實現(xiàn)。而設為true默認值時容器復用同一實例但要求開發(fā)者自行保證成員變量線程安全——這正是早期JSP項目高并發(fā)下數(shù)據(jù)錯亂的根源。2.3 EL表達式與JSTL為什么${user.name}比% user.getName() %更安全日記本源碼中diary_list.jsp大量使用${diary.title}而非% diary.getTitle() %。這不是語法糖而是兩套完全不同的執(zhí)行模型% %在_jspService()中直接調用Java方法異常會中斷整個頁面渲染如diary為null時拋NullPointerException${}由EL Resolver按作用域page→request→session→application逐層查找自動處理null值輸出空字符串且支持.操作符鏈式調用${user.profile.avatar}。JSTL標簽庫如c:if test${not empty msg}進一步將控制邏輯從Java代碼中剝離其本質是TagHandler接口實現(xiàn)類在_jspService()中被編譯為new IfTag().doStartTag()調用——這正是MVC中View層“去邏輯化”的雛形。3. JDBC直連MySQL沒有ORM遮蔽的數(shù)據(jù)庫交互全鏈路這份源碼未使用Hibernate或MyBatis所有數(shù)據(jù)庫操作都基于原生JDBC。這意味著每一步——從驅動加載、連接獲取、SQL拼接、參數(shù)綁定到結果集遍歷——都必須手動編碼。這種“笨辦法”恰恰是理解數(shù)據(jù)持久層本質的必經之路。3.1DBUtil.java一個50行工具類道盡連接池之前的生存法則src/utils/DBUtil.java是整個項目的數(shù)據(jù)庫心臟其核心邏輯只有三段public class DBUtil { private static final String URL jdbc:mysql://localhost:3306/diary_db?useSSLfalseserverTimezoneUTC; private static final String USER root; private static final String PASSWORD 123456; public static Connection getConnection() throws SQLException { // 1. 顯式加載驅動JDBC 4.0后可省略但源碼保留 try { Class.forName(com.mysql.cj.jdbc.Driver); } catch (ClassNotFoundException e) { throw new RuntimeException(MySQL Driver not found, e); } // 2. 每次請求新建Connection無連接池 return DriverManager.getConnection(URL, USER, PASSWORD); } public static void close(Connection conn, PreparedStatement ps, ResultSet rs) { // 3. 嚴格按rs→ps→conn順序關閉ResultSet依賴StatementStatement依賴Connection if (rs ! null) try { rs.close(); } catch (SQLException e) {} if (ps ! null) try { ps.close(); } catch (SQLException e) {} if (conn ! null) try { conn.close(); } catch (SQLException e) {} } }URL參數(shù)深意useSSLfalse關閉SSL開發(fā)環(huán)境簡化serverTimezoneUTC強制時區(qū)對齊避免java.sql.SQLException: The server time zone value XXX is unrecognized這類經典翻車無連接池的代價每次getConnection()都建立TCP新連接MySQL默認最大連接數(shù)151高并發(fā)下直接Too many connections——這正是后來Druid/HikariCP誕生的原始驅動力。3.2UserDAO.javaPreparedStatement防注入的底層實現(xiàn)用戶登錄驗證邏輯在src/dao/UserDAO.java中public User findUserByUsernameAndPassword(String username, String password) { String sql SELECT id, username, email FROM users WHERE username ? AND password ?; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, username); // 參數(shù)1綁定username ps.setString(2, password); // 參數(shù)2綁定password try (ResultSet rs ps.executeQuery()) { if (rs.next()) { return new User(rs.getInt(id), rs.getString(username), rs.getString(email)); } } } catch (SQLException e) { e.printStackTrace(); } return null; }?占位符的本質PreparedStatement將SQL語句發(fā)送給MySQL服務端預編譯setString()只傳輸參數(shù)值服務端絕不會將參數(shù)值拼進SQL字符串。即使username傳入 OR 11最終執(zhí)行的仍是WHERE username ?問號處的值被當作純數(shù)據(jù)處理對比Statement的災難若用Statement stmt conn.createStatement(); stmt.executeQuery(SELECT ... WHERE username username )惡意輸入會直接拼成WHERE username OR 11繞過所有校驗。3.3 事務邊界DiaryDAO.java中save()方法的手動控制日記保存需同時寫入diary主表和diary_tag關聯(lián)表源碼用Connection.setAutoCommit(false)顯式管理事務public boolean save(Diary diary, ListString tags) { String sql1 INSERT INTO diary(title, content, create_time) VALUES(?, ?, ?); String sql2 INSERT INTO diary_tag(diary_id, tag_name) VALUES(?, ?); try (Connection conn DBUtil.getConnection()) { conn.setAutoCommit(false); // 關閉自動提交 try (PreparedStatement ps1 conn.prepareStatement(sql1, Statement.RETURN_GENERATED_KEYS)) { ps1.setString(1, diary.getTitle()); ps1.setString(2, diary.getContent()); ps1.setTimestamp(3, new Timestamp(diary.getCreateTime().getTime())); int affected ps1.executeUpdate(); if (affected 0) throw new SQLException(Insert diary failed); // 獲取自增主鍵 try (ResultSet rs ps1.getGeneratedKeys()) { if (rs.next()) { int diaryId rs.getInt(1); // 批量插入標簽 try (PreparedStatement ps2 conn.prepareStatement(sql2)) { for (String tag : tags) { ps2.setInt(1, diaryId); ps2.setString(2, tag); ps2.addBatch(); } ps2.executeBatch(); } } } conn.commit(); // 全部成功才提交 return true; } catch (Exception e) { conn.rollback(); // 任一環(huán)節(jié)失敗則回滾 throw e; } } catch (SQLException e) { e.printStackTrace(); return false; } }RETURN_GENERATED_KEYS的妙用ps1.executeUpdate()后通過ps1.getGeneratedKeys()獲取剛插入行的自增ID這是跨表關聯(lián)的基石addBatch()的性能邏輯將多條INSERT合并為單次網絡請求避免N次往返——但注意MySQL默認rewriteBatchedStatementsfalse需在JDBC URL中顯式添加rewriteBatchedStatementstrue才能真正生效。4. Session會話管理用戶狀態(tài)如何在無狀態(tài)HTTP上“粘住”HTTP協(xié)議天生無狀態(tài)而日記本需要記住“誰正在寫日記”。源碼用HttpSession實現(xiàn)但它的運作遠非session.setAttribute(user, user)這么簡單——背后是Cookie、內存存儲、超時策略的精密配合。4.1 Session ID的傳遞從JSESSIONIDCookie到URL重寫用戶登錄成功后LoginServlet.java中HttpSession session request.getSession(); // 創(chuàng)建或獲取session session.setAttribute(user, user); // 存儲用戶對象 response.sendRedirect(main.jsp); // 重定向到主頁此時Tomcat做了三件事生成唯一sessionId如2F7C9E3A1B5D8F0C4E6A9B2D7F0C4E6A將sessionId寫入響應頭Set-Cookie: JSESSIONID2F7C9E3A...; Path/; HttpOnly在服務器內存中創(chuàng)建StandardSession對象以sessionId為key存儲user等屬性。注意若客戶端禁用CookieTomcat會自動啟用URL重寫——在response.sendRedirect(main.jsp)生成的Location頭中URL變?yōu)閙ain.jsp;jsessionid2F7C9E3A...。你可在瀏覽器開發(fā)者工具Network面板中觀察Set-Cookie頭是否存在。4.2 Session存儲位置內存、文件、數(shù)據(jù)庫的三種實現(xiàn)源碼默認使用Tomcat內置的StandardManager將Session對象存于JVM堆內存。但生產環(huán)境必須考慮集群部署問題若用戶請求被負載均衡分發(fā)到不同Tomcat節(jié)點內存Session無法共享解決方案對比方案實現(xiàn)方式優(yōu)點缺點PersistentManagerSession序列化到本地磁盤文件進程重啟不丟失磁盤IO瓶頸不支持集群RedisStore需擴展Session存入Redis高性能、天然支持集群需額外部署Redis增加運維復雜度JDBCStoreSession存入MySQL表利用現(xiàn)有數(shù)據(jù)庫數(shù)據(jù)庫成為單點瓶頸日記本源碼未涉及這些但web.xml中session-config配置已埋下伏筆session-config session-timeout30/session-timeout !-- 單位分鐘 -- cookie-http-onlytrue/cookie-http-only !-- 防XSS竊取 -- tracking-modeCOOKIE/tracking-mode !-- 可選URL、COOKIE、SSL -- /session-config4.3 Session失效的四種觸發(fā)條件Session并非永生以下任一情況即失效超時session-timeout30/session-timeout配置后30分鐘內無請求則銷毀主動失效調用session.invalidate()如LogoutServlet.java中容器重啟內存Session全部丟失除非配置了持久化JVM崩潰進程退出內存清空。提示session.getAttribute(user)返回null時不能簡單認為“用戶未登錄”必須結合request.getRequestedSessionId()是否為空、request.isRequestedSessionIdValid()是否為true綜合判斷——這是很多新手排查登錄態(tài)丟失的盲區(qū)。5. 常見問題排查十個血淚經驗總結出的JSPMySQL踩坑清單這份源碼雖小但覆蓋了Java Web開發(fā)的典型故障場景。以下是某開發(fā)者在部署調試過程中記錄的真實問題按“現(xiàn)象→原因→解決”結構整理每一條都對應源碼中的具體文件和配置。5.1 現(xiàn)象訪問login.jsp報錯HTTP Status 500 – java.lang.ClassNotFoundException: com.mysql.cj.jdbc.Driver原因MySQL 8.0驅動類名從com.mysql.jdbc.Driver改為com.mysql.cj.jdbc.Driver而DBUtil.java中Class.forName()硬編碼了舊類名或mysql-connector-java.jar未放入WEB-INF/lib/目錄。解決修改DBUtil.java第12行Class.forName(com.mysql.cj.jdbc.Driver);下載mysql-connector-java-8.0.33.jar復制到WEB-INF/lib/重啟Tomcat熱部署不生效因驅動需在容器啟動時加載。5.2 現(xiàn)象登錄成功后跳轉main.jsp頁面顯示java.lang.NullPointerException在% session.getAttribute(user).getUsername() %原因LoginServlet.java中session.setAttribute(user, user)執(zhí)行前user對象為null如數(shù)據(jù)庫查無此人導致存入null值后續(xù)main.jsp直接調用getUsername()拋NPE。解決在LoginServlet.java中增加判空if (user ! null) { session.setAttribute(user, user); response.sendRedirect(main.jsp); } else { request.setAttribute(msg, 用戶名或密碼錯誤); request.getRequestDispatcher(login.jsp).forward(request, response); }main.jsp中用EL表達式${empty user}替代直接調用。5.3 現(xiàn)象中文日記內容存入MySQL后顯示為???亂碼原因三處編碼未統(tǒng)一① JSP頁面% page pageEncodingUTF-8 %② MySQL連接URL缺少characterEncodingutf8③ MySQL數(shù)據(jù)庫/表字符集非utf8mb4。解決DBUtil.java中URL改為jdbc:mysql://localhost:3306/diary_db?useSSLfalseserverTimezoneUTCcharacterEncodingutf8MySQL執(zhí)行ALTER DATABASE diary_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; ALTER TABLE diary CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;Tomcat的conf/server.xml中Connector標簽添加URIEncodingUTF-8。5.4 現(xiàn)象diary_list.jsp中c:forEach items${diaryList} vardiary不渲染任何日記原因JSTL標簽庫缺失。WEB-INF/lib/中無jstl.jar和standard.jarJSP 2.0需jakarta.servlet.jsp.jstl-api和jakarta.servlet.jsp.jstl。解決下載jakarta.servlet.jsp.jstl-api-2.0.0.jar和jakarta.servlet.jsp.jstl-2.0.0.jar復制到WEB-INF/lib/diary_list.jsp頂部添加% taglib prefixc urihttp://java.sun.com/jsp/jstl/core %Tomcat 10需改為http://xmlns.jcp.org/jsp/jstl/core。5.5 現(xiàn)象修改web.xml中welcome-file-list為welcome-fileindex.jsp/welcome-file但訪問http://localhost:8080/jsp_diary/仍顯示404原因index.jsp不存在或web.xml未被Tomcat識別文件名大小寫錯誤、XML格式非法、未放在WEB-INF/目錄下。解決確認index.jsp位于Web應用根目錄與login.jsp同級用XML校驗工具檢查web.xml是否符合DTD/XSD規(guī)范如web-app根標簽的xmlns屬性查看Tomcat啟動日志搜索Deploying web application directory確認應用是否成功部署。6. 設計取舍反思當MVC被拆成三個物理文件夾我們失去了什么又得到了什么這份日記本源碼的目錄結構極簡WEB-INF/下放web.xml和lib/Java類按功能散落在src/dao/、src/servlet/、src/utils/JSP頁面平鋪在Web根目錄。它沒有Spring MVC的Controller注解沒有MyBatis的Select甚至沒有Maven的pom.xml——所有依賴、配置、路由都靠手工維護。這種“返祖式”設計表面是技術落后實則是刻意為之的認知錨點。6.1 物理分層即邏輯分層src/目錄下的MVC三原色打開src/目錄你能清晰看到MVC的物理映射Model數(shù)據(jù)模型src/entity/User.java、src/entity/Diary.java——純粹的POJO無任何框架注解字段與數(shù)據(jù)庫表一一對應View視圖login.jsp、diary_list.jsp——僅含HTML和EL/JSTL零Java腳本% %被嚴格禁止Controller控制器src/servlet/LoginServlet.java、src/servlet/DiaryServlet.java——接收請求、調用DAO、設置請求屬性、轉發(fā)跳轉。這種分離不是靠框架約定而是靠開發(fā)者自覺遵守的目錄約束。某導師曾讓學生故意把DAO代碼寫進JSP結果diary_list.jsp體積暴漲到800行修改一個SQL要同時改JSP和Servlet——三天后全班自發(fā)重構為標準三層。物理隔離倒逼邏輯清晰這比任何注解都管用。6.2web.xml一份被遺忘的中央路由注冊表現(xiàn)代框架用WebServlet(/login)或RequestMapping(/login)將路由分散到每個類上而本源碼的web.xml是唯一的路由中心servlet servlet-nameLoginServlet/servlet-name servlet-classsrc.servlet.LoginServlet/servlet-class /servlet servlet-mapping servlet-nameLoginServlet/servlet-name url-pattern/login/url-pattern /servlet-mapping優(yōu)勢全局路由一目了然安全策略如security-constraint可集中配置劣勢新增一個Servlet需手動改三處servlet、servlet-mapping、welcome-file易遺漏現(xiàn)實啟示Spring Boot的SpringBootApplication本質是自動生成web.xml的替代品但當application.properties里server.servlet.context-path/api和spring.mvc.servlet.path/v1沖突時你依然得回到web.xml思維去理解容器行為。6.3 為什么不用Filter做統(tǒng)一編碼因為request.setCharacterEncoding()必須在getParameter()之前src/filter/EncodingFilter.java實現(xiàn)了UTF-8編碼過濾器但它的doFilter()方法必須這樣寫public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) { HttpServletRequest req (HttpServletRequest) request; HttpServletResponse resp (HttpServletResponse) response; req.setCharacterEncoding(UTF-8); // 必須在getParameter()前調用 resp.setCharacterEncoding(UTF-8); chain.doFilter(req, resp); }關鍵細節(jié)request.setCharacterEncoding()只對POST請求的請求體Body生效對GET請求的URL參數(shù)無效需在server.xml中配置URIEncoding血淚教訓某開發(fā)者把req.setCharacterEncoding(UTF-8)放在chain.doFilter()之后結果所有中文參數(shù)仍是ISO-8859-1亂碼——因為getParameter()在Filter鏈中已被前置Filter或Servlet調用過編碼已固化。從那以后我每次寫Filter都強制走一遍getParameterNames()打印所有參數(shù)名確認編碼正確后再繼續(xù)。這份源碼里EncodingFilter的存在不是為了炫技而是把“請求生命周期中編碼設置的窗口期”這個抽象概念釘死在doFilter()方法的第3行。希望幫到你。本文還有配套的精品資源點擊獲取