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