戰(zhàn)避坑)
1. 從一次“數(shù)據(jù)莫名消失”的調(diào)試說起做 JSP 開發(fā)的人幾乎都經(jīng)歷過這樣的場景明明在頁面里把數(shù)據(jù)塞進(jìn)去了翻到下一個(gè)頁面卻怎么也取不出來或者同一個(gè)變量在 A 頁面是值在 B 頁面變成了 null在 C 頁面又莫名其妙有了值。我最早接觸 JSP 那會(huì)兒為了一個(gè)購物車數(shù)據(jù)在跳轉(zhuǎn)后丟失的問題硬是排查了大半天最后發(fā)現(xiàn)是把數(shù)據(jù)放進(jìn)了pageContext而不是session。這類問題的根源十有八九都指向同一個(gè)知識點(diǎn)——JSP 隱式對象與四大作用域。這篇內(nèi)容就是圍繞這個(gè)主題展開的。它講的是 JSP 技術(shù)里九個(gè)內(nèi)置對象隱式對象的用法以及 request、session、application、page 這四大作用域的生命周期與數(shù)據(jù)存取規(guī)則。說白了就是搞清楚“數(shù)據(jù)該放在哪個(gè)容器里才能在該出現(xiàn)的地方出現(xiàn)在該銷毀的時(shí)候銷毀”。這套東西看起來基礎(chǔ)但它是 JSP 開發(fā)的地基地基沒打牢后面寫再多標(biāo)簽庫、再花哨的 MVC 分層都是空中樓閣。適合誰來讀如果你是剛接觸 JSP、正在做課程設(shè)計(jì)或者實(shí)訓(xùn)項(xiàng)目的同學(xué)這篇能幫你把隱式對象和作用域一次理清楚少走我當(dāng)年走過的彎路如果你已經(jīng)寫過一些 JSP 頁面但對“為什么這里要用 session 不用 request”還停留在背結(jié)論的階段那這篇會(huì)從原理和實(shí)操兩個(gè)層面幫你把結(jié)論背后的邏輯補(bǔ)上。下面我按“整體設(shè)計(jì)思路—核心細(xì)節(jié)—實(shí)操落地—問題排查”的順序展開中間穿插我自己踩過的坑和實(shí)測有效的做法。2. 整體設(shè)計(jì)與思路拆解為什么 JSP 要設(shè)計(jì)隱式對象和作用域2.1 隱式對象的本質(zhì)容器替你準(zhǔn)備好的“工具箱”要理解隱式對象先得明白 JSP 到底是怎么跑起來的。一個(gè).jsp文件在第一次被訪問時(shí)會(huì)被容器比如 Tomcat翻譯成一個(gè) Java 的 Servlet 類然后編譯、加載、實(shí)例化。在這個(gè)翻譯過程中容器會(huì)自動(dòng)往生成的方法里注入一批“現(xiàn)成可用”的變量這些變量就是隱式對象。你在 JSP 里寫% request.getParameter(name) %能直接用request不是因?yàn)樗鞘裁慈肿兞慷且驗(yàn)槿萜髟赺jspService方法里已經(jīng)幫你聲明好了。這個(gè)設(shè)計(jì)思路很務(wù)實(shí)JSP 的定位是“讓頁面開發(fā)少寫樣板代碼”。如果每個(gè)頁面都要自己new出請求對象、響應(yīng)對象、會(huì)話對象那 JSP 就失去了存在的意義。容器把這些高頻使用的對象預(yù)先準(zhǔn)備好開發(fā)者拿來即用本質(zhì)上是把 Servlet 里那些繁瑣的獲取步驟封裝掉了。九個(gè)隱式對象各有分工我習(xí)慣把它們按“職責(zé)”分成三組來記比死記硬背九個(gè)名字高效得多分組隱式對象類型核心職責(zé)輸入輸出組requestHttpServletRequest拿請求參數(shù)、請求頭、請求域數(shù)據(jù)responseHttpServletResponse設(shè)置響應(yīng)、重定向、輸出outJspWriter向?yàn)g覽器輸出內(nèi)容會(huì)話狀態(tài)組sessionHttpSession跨請求保存用戶狀態(tài)applicationServletContext全局共享數(shù)據(jù)pageContextPageContext頁面域存取、獲取其他隱式對象環(huán)境信息組pageObjectthis當(dāng)前 Servlet 實(shí)例configServletConfig獲取初始化參數(shù)exceptionThrowable處理異常頁面這張表建議直接記下來。實(shí)際開發(fā)中request、session、application、pageContext、out這五個(gè)是絕對的高頻response在重定向和設(shè)置響應(yīng)頭時(shí)會(huì)用exception只在isErrorPagetrue的頁面里有效page和config用得相對少但面試和原理理解繞不開。2.2 四大作用域的設(shè)計(jì)邏輯數(shù)據(jù)的“保質(zhì)期”和“可見范圍”作用域這個(gè)概念用生活化的類比最好理解。你可以把數(shù)據(jù)想象成一張便簽紙四大作用域就是四種不同的“貼法”page 作用域便簽貼在你自己的手背上只有當(dāng)前這個(gè)頁面能看到頁面一執(zhí)行完便簽就撕掉了。對應(yīng)pageContext僅在當(dāng)前 JSP 頁面內(nèi)有效。request 作用域便簽貼在這次快遞包裹上包裹送到哪便簽跟到哪。一次請求包括請求轉(zhuǎn)發(fā)內(nèi)有效轉(zhuǎn)發(fā)到下一個(gè)頁面還能看到但用戶刷新或者發(fā)起新請求就沒了。對應(yīng)request。session 作用域便簽貼在你的會(huì)員檔案上只要你還在這家店逛同一個(gè)會(huì)話走到哪個(gè)柜臺都能查到。對應(yīng)session一次會(huì)話內(nèi)有效默認(rèn) 30 分鐘不活動(dòng)就失效。application 作用域便簽貼在店門口的大公告板上所有顧客、所有柜臺都能看到店不關(guān)門應(yīng)用不重啟就一直存在。對應(yīng)applicationServletContext。為什么要有四種而不是一種因?yàn)椴煌瑪?shù)據(jù)的“生命周期需求”完全不同。一次請求里臨時(shí)傳遞的查詢結(jié)果放 application 里就是內(nèi)存泄漏全站共享的配置信息放 request 里每個(gè)請求都要重新加載。作用域設(shè)計(jì)的核心思想就是讓數(shù)據(jù)的存活時(shí)間和可見范圍精確匹配它的業(yè)務(wù)用途。選錯(cuò)作用域輕則數(shù)據(jù)取不到重則內(nèi)存被撐爆或者出現(xiàn)并發(fā)臟數(shù)據(jù)。2.3 方案選型的判斷標(biāo)準(zhǔn)三個(gè)問題定作用域我在實(shí)際項(xiàng)目里選作用域習(xí)慣問自己三個(gè)問題基本能一次選對這份數(shù)據(jù)需要跨幾次請求只在本頁面用page一次請求內(nèi)含轉(zhuǎn)發(fā)夠用request需要跨多次請求維持用戶狀態(tài)session全應(yīng)用共享application。這份數(shù)據(jù)是“用戶私有”還是“全局公共”用戶私有且需要跨請求session全局公共且讀多寫少application。這份數(shù)據(jù)量大不大、會(huì)不會(huì)累積大對象、集合類數(shù)據(jù)盡量別往 session 和 application 里塞尤其是 application塞進(jìn)去不清就是內(nèi)存泄漏。舉個(gè)具體例子用戶登錄后的用戶名需要跨多次請求且是用戶私有選 session網(wǎng)站公告內(nèi)容全站共享且變化不頻繁選 application一次查詢的結(jié)果列表只在當(dāng)前請求的轉(zhuǎn)發(fā)鏈路里用選 request循環(huán)渲染時(shí)的臨時(shí)計(jì)數(shù)變量選 page。這套判斷邏輯比背“session 存登錄信息”這種結(jié)論管用得多因?yàn)闃I(yè)務(wù)場景千變?nèi)f化結(jié)論會(huì)過時(shí)判斷標(biāo)準(zhǔn)不會(huì)。3. 核心細(xì)節(jié)解析與實(shí)操要點(diǎn)九個(gè)隱式對象逐個(gè)拆3.1 request 與 response一次請求的“入口”和“出口”request是HttpServletRequest的實(shí)例它承載了客戶端發(fā)來的所有信息。最常用的三個(gè)能力拿參數(shù)getParameter、拿請求頭getHeader、操作請求域setAttribute/getAttribute。這里有個(gè)新手極易踩的坑getParameter和getAttribute長得像但完全是兩回事。前者拿的是瀏覽器提交上來的表單/URL 參數(shù)返回 String后者拿的是服務(wù)端自己往 request 域里存的對象返回 Object。我見過有同學(xué)往 request 里setAttribute一個(gè)對象然后用getParameter去取取出來永遠(yuǎn)是 null排查半天以為是作用域問題其實(shí)是方法用錯(cuò)了。response是HttpServletResponse的實(shí)例負(fù)責(zé)響應(yīng)側(cè)的操作。兩個(gè)高頻用法重定向response.sendRedirect()和設(shè)置響應(yīng)頭/內(nèi)容類型。這里要重點(diǎn)區(qū)分重定向和請求轉(zhuǎn)發(fā)因?yàn)樗苯記Q定了 request 作用域的數(shù)據(jù)能不能傳過去對比項(xiàng)請求轉(zhuǎn)發(fā)forward重定向redirect實(shí)現(xiàn)方式request.getRequestDispatcher().forward()response.sendRedirect()請求次數(shù)1 次2 次地址欄不變改變r(jià)equest 域數(shù)據(jù)可以傳遞丟失能否訪問 WEB-INF可以不可以這張表是 JSP 開發(fā)的高頻考點(diǎn)也是實(shí)操中的高頻坑點(diǎn)。你在 A 頁面往 request 里存了數(shù)據(jù)轉(zhuǎn)發(fā)到 B 頁面能取到一旦改成重定向數(shù)據(jù)就沒了——因?yàn)橹囟ㄏ蚴亲尀g覽器重新發(fā)起一次請求是一個(gè)全新的 request 對象。這個(gè)區(qū)別不理解調(diào)試時(shí)會(huì)非常痛苦。3.2 session 與 application跨請求狀態(tài)的“兩級緩存”session是HttpSession的實(shí)例代表一次用戶會(huì)話。它的工作機(jī)制值得說清楚用戶第一次訪問時(shí)容器創(chuàng)建一個(gè) session 對象并生成一個(gè)唯一的 sessionId通過 Cookie默認(rèn)名為 JSESSIONID發(fā)給瀏覽器之后瀏覽器每次請求都帶上這個(gè) ID容器據(jù)此找到對應(yīng)的 session。所以 session 能跨請求保持狀態(tài)靠的是這個(gè) ID 的往返傳遞。session 的常用操作就三個(gè)setAttribute、getAttribute、removeAttribute外加一個(gè)invalidate()用于注銷時(shí)銷毀會(huì)話。這里有個(gè)實(shí)操要點(diǎn)用戶注銷時(shí)一定要調(diào)invalidate()只removeAttribute掉用戶名是不夠的session 對象還在其他存在里面的數(shù)據(jù)也還在存在安全隱患。另外 session 默認(rèn)超時(shí)是 30 分鐘可以在web.xml里用session-configsession-timeout調(diào)整單位是分鐘。application是ServletContext的實(shí)例整個(gè) Web 應(yīng)用只有一個(gè)所有用戶共享。它的典型用途是存全局配置、全局計(jì)數(shù)器、應(yīng)用啟動(dòng)時(shí)加載的緩存數(shù)據(jù)。實(shí)操中最需要注意的是并發(fā)問題多個(gè)用戶同時(shí)往 application 里寫數(shù)據(jù)如果不加同步計(jì)數(shù)就會(huì)錯(cuò)亂。我一般建議 application 里只放“讀多寫少”或者“初始化后不再修改”的數(shù)據(jù)需要?jiǎng)討B(tài)修改的計(jì)數(shù)器要么用AtomicInteger要么加鎖別直接setAttribute一個(gè)普通 int 然后。3.3 pageContext被低估的“萬能鑰匙”pageContext是PageContext的實(shí)例很多人只知道它能存 page 域數(shù)據(jù)其實(shí)它的能力遠(yuǎn)不止于此。它有兩個(gè)殺手級用法第一統(tǒng)一操作四大作用域。pageContext提供了一組帶scope參數(shù)的方法比如setAttribute(String name, Object value, int scope)scope 可以取PageContext.PAGE_SCOPE、REQUEST_SCOPE、SESSION_SCOPE、APPLICATION_SCOPE。這意味著你可以用同一個(gè)對象往四個(gè)作用域里寫數(shù)據(jù)寫工具類或者自定義標(biāo)簽時(shí)特別方便。第二獲取其他八個(gè)隱式對象。pageContext.getRequest()、getSession()、getServletContext()等等都能拿到。在自定義標(biāo)簽的doStartTag里拿不到隱式對象時(shí)就是通過傳入的pageContext去取的。這個(gè)能力讓 pageContext 成了連接 JSP 頁面和標(biāo)簽處理器的橋梁。還有一個(gè)容易被忽略的細(xì)節(jié)pageContext的findAttribute(String name)方法會(huì)按照 page → request → session → application 的順序依次查找找到就返回。這個(gè)“就近查找”機(jī)制在 EL 表達(dá)式里被廣泛使用${name}的底層查找邏輯就是它。理解這一點(diǎn)你就能明白為什么同名屬性放在不同作用域時(shí)EL 取到的是范圍最小的那個(gè)。3.4 out、page、config、exception各司其職的“配角”out是JspWriter的實(shí)例負(fù)責(zé)向響應(yīng)輸出內(nèi)容。它和response.getWriter()的區(qū)別在于緩沖機(jī)制JspWriter自帶緩沖區(qū)默認(rèn) 8KB內(nèi)容先寫進(jìn)緩沖區(qū)滿了或者頁面結(jié)束時(shí)才真正刷給瀏覽器。這個(gè)特性帶來一個(gè)實(shí)操注意點(diǎn)在 JSP 里不要混用out和response.getWriter()兩者的緩沖區(qū)獨(dú)立混用會(huì)導(dǎo)致輸出順序錯(cuò)亂。我見過頁面里一部分內(nèi)容用 out 輸出、一部分用 response.getWriter() 輸出結(jié)果瀏覽器上看到的順序和代碼順序?qū)Σ簧暇褪沁@個(gè)原因。page就是當(dāng)前 Servlet 實(shí)例的引用等價(jià)于 Java 里的this實(shí)際開發(fā)幾乎不用了解即可。config是ServletConfig用于獲取 JSP 的初始化參數(shù)在web.xml里給 JSP 配置init-param后可以用它取用得也少。exception比較特殊只有在頁面指令里寫了isErrorPagetrue的頁面才能用它代表拋出的異常對象配合errorPage指令做統(tǒng)一錯(cuò)誤處理。這里有個(gè)細(xì)節(jié)exception在普通頁面里是取不到的直接引用會(huì)編譯報(bào)錯(cuò)別在普通頁面里試。4. 實(shí)操過程與核心環(huán)節(jié)實(shí)現(xiàn)從零跑通作用域驗(yàn)證4.1 環(huán)境準(zhǔn)備與項(xiàng)目骨架理論說再多不如動(dòng)手跑一遍。下面這套實(shí)操方案我用它給不少人做過演示步驟清晰、結(jié)果可驗(yàn)證。環(huán)境上JDK 8 及以上、Tomcat 8 及以上、任意 IDEEclipse、IDEA 都行即可不需要 Spring 之類的框架純 JSP 就能跑。項(xiàng)目結(jié)構(gòu)很簡單在webapp目錄下建幾個(gè) JSP 文件webapp/ ├── scope-set.jsp // 往四個(gè)作用域?qū)憯?shù)據(jù) ├── scope-get.jsp // 讀取四個(gè)作用域的數(shù)據(jù) ├── forward-target.jsp // 轉(zhuǎn)發(fā)目標(biāo)頁驗(yàn)證 request 域 └── WEB-INF/ └── web.xmlweb.xml里只需要配置 session 超時(shí)可選其余用默認(rèn)即可。這個(gè)骨架的目的很純粹把“寫”和“讀”分開到不同頁面才能真實(shí)檢驗(yàn)作用域的跨頁面能力。4.2 往四大作用域?qū)懭霐?shù)據(jù)先寫scope-set.jsp核心代碼如下% page contentTypetext/html;charsetUTF-8 languagejava % % pageContext.setAttribute(pageData, 我是page域數(shù)據(jù)); pageContext.setAttribute(requestData, 我是request域數(shù)據(jù), PageContext.REQUEST_SCOPE); pageContext.setAttribute(sessionData, 我是session域數(shù)據(jù), PageContext.SESSION_SCOPE); pageContext.setAttribute(appData, 我是application域數(shù)據(jù), PageContext.APPLICATION_SCOPE); % h3數(shù)據(jù)已寫入四大作用域/h3 a hrefscope-get.jsp點(diǎn)我讀取數(shù)據(jù)/a這里我特意用pageContext的帶 scope 參數(shù)的方法來寫而不是分別用request.setAttribute、session.setAttribute目的是讓你直觀看到 pageContext 統(tǒng)一操作四大作用域的能力。兩種寫法效果等價(jià)但前者在需要?jiǎng)討B(tài)決定作用域時(shí)更靈活。寫入完成后頁面提供一個(gè)超鏈接跳到讀取頁。注意這里是超鏈接跳轉(zhuǎn)屬于新的請求正好用來驗(yàn)證哪些作用域的數(shù)據(jù)能跨請求存活。4.3 讀取數(shù)據(jù)并對比結(jié)果scope-get.jsp的讀取代碼% page contentTypetext/html;charsetUTF-8 languagejava % % Object pageData pageContext.getAttribute(pageData); Object requestData request.getAttribute(requestData); Object sessionData session.getAttribute(sessionData); Object appData application.getAttribute(appData); % ppage 域% pageData %/p prequest 域% requestData %/p psession 域% sessionData %/p papplication 域% appData %/p跑起來之后你會(huì)看到這樣的結(jié)果page 域是 nullrequest 域是 nullsession 域有值application 域有值。這個(gè)結(jié)果完全符合預(yù)期——超鏈接是新請求page 和 request 都失效了session 和 application 還在。這一步是理解作用域最直觀的驗(yàn)證建議每個(gè)人都親手跑一遍比看十遍文字描述都管用。4.4 用請求轉(zhuǎn)發(fā)驗(yàn)證 request 域的傳遞為了對比再做一個(gè)轉(zhuǎn)發(fā)驗(yàn)證。在scope-set.jsp里加一個(gè)轉(zhuǎn)發(fā)鏈接或者單獨(dú)寫一個(gè)頁面% request.setAttribute(forwardData, 轉(zhuǎn)發(fā)攜帶的數(shù)據(jù)); request.getRequestDispatcher(forward-target.jsp).forward(request, response); %forward-target.jsp里用request.getAttribute(forwardData)讀取能正常取到值。把同樣的代碼改成response.sendRedirect(forward-target.jsp)再讀就是 null。這一組對比實(shí)驗(yàn)做下來轉(zhuǎn)發(fā)和重定向?qū)?request 域的影響就刻在腦子里了。4.5 參數(shù)計(jì)算與選擇過程的補(bǔ)充說明作用域本身不涉及復(fù)雜參數(shù)計(jì)算但 session 超時(shí)時(shí)間的設(shè)定值得算一筆賬。默認(rèn) 30 分鐘意味著用戶 30 分鐘不操作session 就失效登錄狀態(tài)丟失。如果你的應(yīng)用是后臺管理系統(tǒng)用戶可能長時(shí)間掛著頁面思考30 分鐘偏短可以調(diào)到 60 分鐘如果是涉及敏感信息的系統(tǒng)反而應(yīng)該縮短到 15 分鐘甚至更短。調(diào)整方式是在web.xml里session-config session-timeout60/session-timeout /session-config這個(gè)值的單位是分鐘寫 0 或負(fù)數(shù)表示永不過期——但強(qiáng)烈不建議這么干session 永不失效意味著內(nèi)存里的會(huì)話對象只增不減用戶量一大就是內(nèi)存泄漏。我一般建議根據(jù)業(yè)務(wù)敏感度在 15 到 60 分鐘之間取值寧可讓用戶重新登錄也別把服務(wù)器拖垮。5. 常見問題與排查技巧實(shí)錄5.1 數(shù)據(jù)取不到先按作用域鏈路排查數(shù)據(jù)取不到是最高頻的問題我整理了一個(gè)排查順序按這個(gè)鏈路走基本能定位現(xiàn)象可能原因排查方法所有作用域都取不到屬性名拼寫不一致檢查 set 和 get 的 name 是否完全一致大小寫敏感page/request 取不到session 能取到發(fā)生了重定向或新請求確認(rèn)跳轉(zhuǎn)方式需要跨請求改用 sessionsession 取不到session 已超時(shí)或已 invalidate檢查是否調(diào)用了 invalidate檢查超時(shí)配置application 取不到應(yīng)用重啟過application 隨應(yīng)用重啟清空屬正?,F(xiàn)象用 getParameter 取 setAttribute 的值方法用錯(cuò)改用 getAttribute這張表里最后一條我單獨(dú)強(qiáng)調(diào)getParameter和getAttribute混用是新手最常見的錯(cuò)誤兩者名字太像但數(shù)據(jù)來源完全不同。記住一句話——參數(shù)來自瀏覽器屬性來自服務(wù)端。5.2 中文亂碼作用域無關(guān)但總被誤認(rèn)為作用域問題往作用域里存中文取出來變亂碼很多人第一反應(yīng)是作用域出問題了其實(shí)是編碼問題。JSP 頁面頂部要寫% page contentTypetext/html;charsetUTF-8 %同時(shí)如果接收表單參數(shù)還要在取參數(shù)前調(diào)request.setCharacterEncoding(UTF-8)。這兩個(gè)地方漏一個(gè)中文就可能亂碼。亂碼和作用域沒有半點(diǎn)關(guān)系但因?yàn)楝F(xiàn)象是“取出來的值不對”容易被誤判排查時(shí)先排除編碼因素。5.3 application 并發(fā)寫入導(dǎo)致數(shù)據(jù)錯(cuò)亂前面提過 application 的并發(fā)問題這里給一個(gè)具體場景用 application 做全站訪問計(jì)數(shù)器代碼寫成Integer count (Integer) application.getAttribute(count); application.setAttribute(count, count 1);。單用戶測試沒問題一上并發(fā)計(jì)數(shù)就偏小。原因是讀和寫之間不是原子操作兩個(gè)請求可能讀到同一個(gè)舊值。解決辦法有兩個(gè)用synchronized包住這段邏輯或者干脆用AtomicInteger存進(jìn) application調(diào)它的incrementAndGet()。我傾向后者代碼更干凈性能也更好。5.4 獨(dú)家避坑心得三個(gè)“別”最后分享三條我踩坑踩出來的經(jīng)驗(yàn)都是文檔里不太會(huì)寫的第一別在 JSP 里寫業(yè)務(wù)邏輯。隱式對象用起來方便但把數(shù)據(jù)庫查詢、復(fù)雜計(jì)算都塞進(jìn) JSP頁面會(huì)變得無法維護(hù)。JSP 的定位是視圖數(shù)據(jù)準(zhǔn)備好再傳進(jìn)來這是 MVC 的基本紀(jì)律。第二別往 session 里塞大對象。有人圖省事把整個(gè)用戶對象、甚至查詢結(jié)果列表塞進(jìn) session用戶一多服務(wù)器內(nèi)存扛不住。session 里只放標(biāo)識性的小數(shù)據(jù)比如用戶 ID需要詳情時(shí)再查。第三別依賴 page 作用域做跨標(biāo)簽傳遞。page 域只在當(dāng)前頁面有效自定義標(biāo)簽之間傳數(shù)據(jù)應(yīng)該用 request 域或者 pageContext 的 findAttribute 機(jī)制用 page 域容易在標(biāo)簽嵌套時(shí)出問題。提示調(diào)試作用域問題時(shí)可以在頁面里臨時(shí)把四個(gè)作用域的值全部打印出來一眼就能看出數(shù)據(jù)到底存在哪一層、哪一層丟了比單點(diǎn)斷點(diǎn)調(diào)試效率高得多。這套隱式對象和作用域的知識說到底就是 JSP 的“內(nèi)存管理入門”。把每個(gè)對象管什么、每份數(shù)據(jù)活多久搞清楚后面無論學(xué) EL、JSTL 還是過渡到框架底層邏輯都是相通的。我自己后來轉(zhuǎn)去做其他技術(shù)?;仡^再看這段反而更能體會(huì)“作用域”這個(gè)抽象設(shè)計(jì)的通用性——幾乎每個(gè) Web 框架都有自己的 request/session/application 對應(yīng)物只是叫法不同罷了。