ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

JSP九大内置对象详解:作用域、生命周期与实战案例

JSP九大内置对象详解:作用域、生命周期与实战案例 1. JSP九大内置对象速览先认人再办事很多人在学 JSP 的时候第一件事就是背那九个内置对象的名字out、request、response、session、application、pageContext、page、config、exception。背得下来是一回事真正到了写页面的时候能不能用对、敢不敢用又是另一回事。我见过不少刚接触 JavaWeb 的开发者项目里从头到尾只用 request 和 out其他对象要么不知道存在要么知道名字但完全想不起来什么时候用。其实这九个对象并不是凭空冒出来的它们是 Servlet 容器为 JSP 页面预先创建好的 Java 实例所以你在 JSP 页面里可以直接拿来用不需要new也不需要从别处获取引用。理解这一层很关键——JSP 本质上会被容器翻译成一个 Servlet而 JSP 页面里的内置对象对应的就是 Servlet 的_jspService()方法里那几行局部变量声明。你在 JSP 里写的所有脚本代码最终都会落到这个方法里执行。1.1 九大对象分别是谁、各自干什么我用最直白的方式把这九个对象先列出来方便你脑子里建立一张“花名册”。对象类型作用一句话总结outJspWriter向页面输出内容管理缓冲区requestHttpServletRequest封装客户端请求的所有数据responseHttpServletResponse封装服务器对客户端的响应sessionHttpSession保存单个用户多次请求之间的共享数据applicationServletContext保存整个 Web 应用范围内的共享数据pageContextPageContext当前页面的“大管家”可以操作其他所有对象pageObject当前 Servlet 实例代表当前 JSP 页面翻译后的 Servlet 对象configServletConfig当前 JSP 页面的配置信息exceptionThrowable页面异常对象只在错误页中可用这张表里前五个你大概率已经在用后四个可能用得少一点。尤其是 page 和 config很多项目跑完整个生命周期都没碰过它们这很正常但我建议你还是把它们的用途搞清楚因为有些框架或者老项目里会用到而且面试也比较喜欢拿它们来检验你有没有真正理解 JSP 的底层机制。1.2 从 Servlet 视角理解内置对象的本质我当年刚学的时候总想不通为什么 JSP 页面里能直接用request.getParameter(name)而写 Servlet 的时候必须靠方法参数传进来后来翻了 Tomcat 生成的 Java 代码才明白JSP 页面的_jspService()方法签名天生就是这个样子public void _jspService(HttpServletRequest request, HttpServletResponse response) throws java.io.IOException, ServletException { // 容器自动声明的内置对象 PageContext pageContext null; HttpSession session null; ServletContext application null; ServletConfig config null; JspWriter out null; Object page this; ... }也就是说这些对象是容器在调用 JSP 之前就帮你初始化好的局部变量。你页面里写的每一行 Java 代码实际都是在这些变量已经存在的前提下执行的。想通这一点之后内置对象就不再是九个孤立的“魔法变量”而是和 Servlet 生命周期、请求响应模型一一对应的实际对象。所以我的建议是不要死记九个大类名也不要背那种“xxx对象用于xxx”的填空式答案而是把每个对象想成“这个请求从浏览器发出、到服务器处理完、再回复浏览器”这条链路里的一个环节。request 管进来response 管出去session 管“这个用户是谁”application 管“整个站点共享什么”pageContext 管“当前页面内部怎么串起来”。有了这条主线后面所有细节都好理解。2. 四个作用域对象pageContext、request、session、application到底怎么选九大对象里头有四个同时是“作用域对象”——pageContext、request、session、application。它们都提供了setAttribute和getAttribute方法都可以往里面放数据再取出来但它们能活多久、能影响多大范围天差地别。这也是实际开发中最容易出问题的地方所以我把它们单独拎出来讲。2.1 四个作用域的生命周期与典型场景先看一张清晰的对比表把生命周期和处理范围一次说透作用域对象生命周期访问范围典型使用场景pageContext当前页面从开始到结束仅当前 JSP 页面页面内的临时变量传递、获取其他内置对象request一次请求从发出到响应结束一次请求链路上所有组件表单提交、请求转发时传递参数session从会话建立到会话销毁同一个浏览器的所有请求登录状态、购物车、用户偏好applicationWeb 应用启动到停止所有用户的所有请求站点统计、全局配置、缓存数据这里最容易混淆的是 request 和 session。我举一个生活中的例子你去超市购物老板问你要什么、你回答“一瓶水”这段对话就是一次 request——问完就结束下次再张嘴是新的对话。而老板记住了你这个人知道你上次买过水下次再来时他认出你、还记得你的口味这段“被记住”的关系就是 session。用代码来说request 里放的数据一次转发链路里都能拿到session 里放的数据只要你浏览器不关、会话不超时下一个请求照样能拿到。2.2 request和session的混用误区在实际项目里我见过最多的问题就是该放 request 的数据塞进 session导致用户信息串号。比如在一个学生信息管理系统里有人为了省事把查询条件直接放 session结果用户 A 在某台电脑查完“计算机系”换用户 B 登录同一个浏览器时看到的还是“计算机系”的查询结果。原因很简单session 是按“浏览器会话”隔离的同一台电脑、同一个浏览器共用一份 session。所以并发测试的时候光换账号不换浏览器就会复现这种“串号”问题。反过来该放 session 的数据放进 request 也会出问题。比如用户登录成功后你把用户对象放进了 request然后sendRedirect跳转到主页。重定向本质上是浏览器发起第二次全新请求第一个 request 对象早就没了主页自然取不到用户信息。正确做法是登录成功后把用户信息放 session页面跳转后仍然能读取。判断标准其实就一句话这个数据“这一锤子买卖”用完就扔放 request需要跨多个请求持续使用放 session整个应用的人都要用放 application只在当前页面内部临时算一步放 pageContext。2.3 application与并发注意application 是所有用户的共享空间听起来很香但千万不要什么数据都往里面放。如果放进去的是一个可变对象而且多个用户同时去改它就会产生并发问题。比如用一个 HashMap 存在 application 里做缓存没有做同步控制多个请求同时 put 或 remove轻则数据错乱重则抛 ConcurrentModificationException。我以前做过一个站点访问计数器刚开始就是把 int 类型 count 存进 application每次访问count。压测一上数字明显不对原因就是多个线程同时读改写这个变量。后来改成AtomicInteger或者加同步块才解决。所以使用 application 时要多问一句这个数据修改频繁吗多个线程同时改会不会出问题如果是请考虑并发控制或者干脆换用 Redis 这类外部存储。pageContext 则简单很多它只在当前 JSP 页面内有效。它最大的价值不是存数据而是它是“通往所有内置对象的入口”。你可以在页面里通过pageContext.getRequest()、pageContext.getSession()拿到其他对象这个特性在写自定义标签或者封装通用方法时特别有用相当于一个万能接线盒。3. 输出与类型相关对象out、page、config的使用要点作用域对象讲完了接下来看另三个不太起眼、但同样承担明确职责的内置对象out、page、config。它们不负责存数据却在页面输出、配置获取、以及对当前页面的自我引用方面有不可替代的作用。3.1 out对象缓冲区里的那些坑out 对应的类型是 JspWriter它本质上是对响应输出流的封装。你可能会问在 JSP 里直接写一行文本不就行了吗为什么还需要 out这是因为 JSP 的输出机制带了一个缓冲区JspWriter 的内容会先写入缓冲区等缓冲区满了或者页面执行结束才真正写到 HTTP 响应流里。缓冲区带来一个利好页面里可以先输出一部分内容后面发现出错还能通过response做重定向——因为内容还没真正发出去状态码还能改。但如果缓冲区满了或者你手动out.flush()内容就会提前发送给浏览器这时你再尝试重定向或设置响应头就会抛 IllegalStateException。我调试过的一个真实案例是JSP 页面前面输出了一个特别长的循环内容把缓冲区撑满了代码后面的重定向逻辑死活不生效控制台报“Response already committed”。排查了半天把输出内容改小或者调大缓冲区容量才解决。所以用 out 时有几个经验可以分享% out.print(hello); out.println( world); out.clear(); // 清空缓冲区但不能在 flush 之后调用 %优先使用% %这种表达式输出可读性更好容器对输出的处理也更规范。不要在输出很多内容之后再去做 response 级别的操作比如重定向、设置响应头。jsp:forward也是同样的道理如果在 forward 之前已经产生了大量输出会失败。out 的默认缓冲区大小由 JSP 的buffer指令控制默认是 8kb必要时可以用% page buffer16kb %调整。3.2 page和config平时不起眼、用对很关键page 对象代表当前 JSP 翻译后的 Servlet 实例也就是this。你在 JSP 里写this其实就等价于用 page 对象。它的实际用处在于如果你想在页面里调用当前 Servlet 的方法或者在某些组件代码里用一个统一方式拿到当前页面实例可以用它。比如 JSP 的标签处理器里需要通过pageContext.getPage()拿到当前页面对象来反射调用方法这种场景绕过 page 就麻烦了。config 对象对应 ServletConfig它封装了当前 JSP 页面的配置信息。通常你在 web.xml 里给某个 JSP 或 Servlet 配置初始化参数之后就能通过config.getInitParameter(key)拿到。比如我在一个老项目中把一个数据源标识配置成了 JSP 的初始化参数页面里用 config 读取这样换环境时只需要改配置不用动代码。现在的项目基本用 Spring Boot 了JSP 越来越少接触这类场景但理解 config 对理解整个 Servlet 规范仍然有帮助。还有一个很多人忽略的知识点config 里能拿到 ServletContext也就是 application 对象。这从侧面说明内置对象之间不是孤立的它们有明确的获取路径。整个获取层次可以概括为pageContext 是最大入口可以拿到 request、response、session、application、config、out、page。request 可以拿到 session也可以拿到 application通过 getServletContext。session 可以拿到 application。config 可以拿到 application。这个关系搞清楚了自定义标签、过滤器、监听器里怎么获取作用域数据心里就有底了。4. 请求与异常对象response、exception的进阶玩法request 和 response 是每一次 HTTP 交互的“当事人”。request 你已经很熟了接收参数、读请求头、转发请求都靠它。response 则代表服务器返回给客户端的结果除了输出 HTML很多控制层面的事情也需要它来做。4.1 response的重定向与响应头设置response 最常用的两个操作是重定向和设置响应头。% // 重定向到另一个地址 response.sendRedirect(login.jsp); // 设置响应头让浏览器禁用缓存 response.setHeader(Cache-Control, no-cache); response.setHeader(Pragma, no-cache); // 设置响应内容的编码 response.setCharacterEncoding(UTF-8); response.setContentType(text/html;charsetUTF-8); %重定向和请求转发是两个经常被放在一起比较的概念别搞混。重定向是服务器告诉浏览器“你去另一个地址”浏览器会发起一次全新的请求地址栏会变化转发是服务器内部直接把请求转给另一个资源浏览器感知不到地址栏不变。对应到 JSP 里转发用jsp:forward或者request.getRequestDispatcher(xxx).forward(request, response)重定向就是response.sendRedirect。实际项目中登录成功后跳转主页我会用重定向因为这样可以防止用户刷新时重复提交表单查询列表跳详情页我会用转发因为详情页需要携带 request 里的数据而且不想让地址栏暴露多余的参数。response 还可以用来生成非 HTML 内容比如导出文件时设置Content-Disposition响应头提示浏览器下载。当年我写过一个 JSP 导出 Excel 的功能就是靠这个头实现的% response.setContentType(application/vnd.ms-excel;charsetUTF-8); response.setHeader(Content-Disposition, attachment;filenamestudents.xls); %这段代码放在 JSP 的最前面后面直接输出表格结构浏览器就会自动下载成一个 Excel 文件。这个属于老一代 JavaWeb 开发者几乎人人都会的技能现在看来虽然不太洋气但理解原理仍然很有价值。4.2 exception要配合page指令的isErrorPageexception 对象比较特殊它不是每个页面都能用只有在声明为错误页的 JSP 中才能直接访问。所谓错误页就是 page 指令里设置了isErrorPagetrue的页面。而触发错误页的方式有两种一种是在出错的页面设置errorPage指向某个错误页一种是全局在 web.xml 里配置error-page。% page isErrorPagetrue % html headtitle出错了/title/head body 错误信息% exception.getMessage() % /body /html在普通的 JSP 页面里直接写exception.getMessage()会编译报错因为容器没有给普通页面初始化这个对象。而错误页拿到 exception 后可以把异常信息记录到日志里同时给用户一个友好的提示页面而不是一堆堆栈信息。我习惯在错误页里这样处理% page isErrorPagetrue importjava.io.PrintWriter % % // 记录异常到服务端日志 exception.printStackTrace(new PrintWriter(out)); %注意直接打印堆栈到页面虽然方便调试但生产环境千万别这么干用户看到一堆莫名其妙的英文堆栈体验极差而且有安全风险等于把代码类名、包名甚至文件路径都暴露了。生产环境的错误页应该保留一个友好提示把详细异常写进日志文件后台追踪问题再去看日志。5. 实操一个“个人信息展示页面”把内置对象用起来理论部分讲了不少接下来进入正题。我以热词里提到的“jsp个人信息展示页面”为例带你从零搭一个能跑起来的小项目。这个案例会覆盖 request、response、session、application、out、pageContext 的典型用法再加一点异常处理的演示。5.1 准备工程IDEA里创建JSP文件首先在 IntelliJ IDEA 里创建一个 JavaWeb 工程这里有个容易卡住新人的点IDEA 不同的版本创建项目的入口不太一样。新版本里我推荐用“Java Enterprise”方式创建勾选 Web Application应用服务器选 TomcatIDEA 会自动生成 web 目录和 index.jsp。如果你打开的是普通的 Java 项目也可以在项目结构里的 Modules 面板给它添加 Web 支持选中模块 → 右键 → Add Framework Support → 勾选 Web Application。之后得手动把 Tomcat 配置到 Run/Debug Configurations 里。第一次跑 JSP 前记得在 Deployment 标签页把 Application context 设置成/demo这类路径访问地址才会是http://localhost:8080/demo/xxx.jsp。新建 JSP 文件就更简单了在 web 目录下右键 → New → JSP/JSPX 就行。如果你右键没看到 JSP 选项大多数情况是项目没有正确添加 Web 支持或者是 IDEA 版本 / 语言设置的问题去 File → Project Structure 里确认 Facets 有没有 Web 模块。5.2 完整代码与内置对象调用点我准备两个页面一个是填写个人信息的表单一个是展示信息的页面。为了演示内置对象我还加了一个简单的计数器。先看表单页info_form.jsp% page contentTypetext/html;charsetUTF-8 languagejava % html head title个人信息填写/title /head body h2请填写个人信息/h2 form actioninfo_show.jsp methodpost 姓名input typetext nameusernamebr 城市input typetext namecitybr 爱好 input typecheckbox namehobby valuecoding编程 input typecheckbox namehobby valuereading阅读 input typecheckbox namehobby valuegaming游戏 br input typesubmit value提交 /form hr % // 通过 application 记录整站访问次数 Integer count (Integer) application.getAttribute(visitCount); if (count null) { count 1; } else { count; } application.setAttribute(visitCount, count); out.print(本页面刷新次数 count); % /body /html然后是展示页info_show.jsp它同时演示了 request 接收数据、pageContext 获取其他对象、session 存取数据和 out 输出% page contentTypetext/html;charsetUTF-8 languagejava % html head title个人信息展示/title /head body % request.setCharacterEncoding(UTF-8); String username request.getParameter(username); String city request.getParameter(city); String[] hobbies request.getParameterValues(hobby); // 用 pageContext 同步获取 request 引用并设置属性 pageContext.getRequest().setAttribute(pageName, info_show.jsp); // 展示历史提交次数用 session 记录 Integer historyCount (Integer) session.getAttribute(submitCount); if (historyCount null) { historyCount 1; } else { historyCount; } session.setAttribute(submitCount, historyCount); % h2个人信息展示/h2 p姓名% username %/p p城市% city %/p p爱好 % if (hobbies ! null) { for (String hobby : hobbies) { out.print(hobby ); } } else { out.print(未选择); } % /p p本次请求页面% request.getAttribute(pageName) %/p p当前会话提交次数% session.getAttribute(submitCount) %/p p站点总访问次数% application.getAttribute(visitCount) %/p /body /html这段代码里我把 pageContext 也硬塞了一个用法目的是演示它能拿到 request 的引用。虽然这里用 pageContext 有点绕但在写标签库或者做通用工具时这个能力非常实用。注意如果表单提交的是中文一定要在读取参数前设置请求编码否则很容易乱码我在后面常见问题里还会再提。5.3 运行效果与对象分工说明启动 Tomcat 后访问http://localhost:8080/demo/info_form.jsp先看到表单页下面的“本页面刷新次数”每次刷新都会变化这是 application 在起作用。提交表单后展示页会显示你填写的姓名、城市和爱好这些数据是 request 接收的。展示页里“当前会话提交次数”每次提交都会加一但你关掉浏览器重新打开又会重置因为 session 是跟随会话的。而回到表单页刷新访问计数它会一直累计重启 Tomcat 才会清零这就是 application 的“全局性”。这个案例虽然简单但正好把四个作用域对象的生命周期差异用可视化的方式呈现出来了。建议你自己跑一遍点刷新、关浏览器重开、重启 Tomcat分别观察哪些数据变了、哪些没变比死记概念管用得多。6. 常见问题与排查技巧实录JSP 开发中遇到的问题很多都和内jsp内置对象的使用方式有关。我把这些年踩过的一些坑、以及热词里涉及到的几个高频问题整理出来方便你按图索骥。6.1 表单提交中文乱码这是 JSP 开发中出现频率最高的问题没有之一。POST 提交的中文参数在request.getParameter之前必须设置% request.setCharacterEncoding(UTF-8); %注意这一行必须放在读取任何参数之前否则已经按默认编码解析过的参数不会重新解析。另外页面本身的编码也要保证一致page 指令里写contentTypetext/html;charsetUTF-8同时 HTML 的 meta 标签里也写上 UTF-8。三个地方的编码不一致就会上演“明明设置了还是乱码”的经典戏码。排查时可以打开浏览器的开发者工具看响应头里的 Content-Type 是什么编码再用request.getCharacterEncoding()打印当前请求编码一步一步定位。6.2 IDEA里新建项目找不到JSP选项很多新手在 IDEA 里右键 New 找不到 JSP 文件原因通常是项目没启用 Web 支持。处理办法是右键模块名 → Add Framework Support → 勾选 Web Application让 IDEA 自动创建 web 目录并生成 web.xml。如果你用的是 Maven 项目也可以手动创建src/main/webapp目录并补上web.xml再在 Project Structure 里把 webapp 标记为 Web Resources Directory。另外一个冷门情况是 IDEA 里“color scheme 中没有 jsp”的配色方案选项这通常是因为当前主题或文件类型关联的问题。解决办法是在 Settings → Editor → File Types 里检查一下*.jsp是否关联到了 JSP 文件类型如果关联错了语法高亮和配色方案自然不生效。6.3 Tomcat下JSP编译后的Java类去哪找热词里有“web项目配置tomcat后查看jsp编译后的java类”这个问题好多人问。JSP 第一次被访问时Tomcat 会把 JSP 翻译成 Java 源文件并编译成 class 文件。这些文件默认放在 Tomcat 的work/Catalina/localhost/你的应用名/org/apache/jsp/目录下。比如应用名是 demo访问info_show.jsp你就能在work/Catalina/localhost/demo/org/apache/jsp/下找到info_show_jsp.java和info_show_jsp.class。打开那个 java 文件就能看到 JSP 是如何被翻译成 Servlet 的内置对象是在_jspService方法里怎么声明的out.write是怎么把 HTML 输出出来的。我强烈建议每个学 JSP 的人都去翻一次这个生成文件看完你对 JSP 的理解会有一个质的提升。这个路径在 IDEA 里更隐蔽一些因为 IDEA 往往配置了自定义的 Tomcat 工作目录。可以在 Run/Debug Configurations 的 Tomcat Server 里找到 HTTP port 旁边有个 Working directory 或者直接看控制台启动日志里面有 Catalina base 路径顺着去找 work 目录就行。6.4 离开页面时的确认提示怎么控制热词里有“屏蔽jsp离开页面提示”这个其实和 JSP 内置对象关系不大更多是浏览器行为。通常建议通过window.onbeforeunload实现但如果你根本不想要这个提示只需要保证页面里没有绑定beforeunload事件处理函数就行。如果页面被 iframe 嵌入导致刷新提示可以检查父页面和子页面的交互逻辑。如果需要实现离开确认注意不同浏览器对这个事件的处理方式有差异建议写成window.onbeforeunload function(e) { e e || window.event; // 现代浏览器要求必须设置 returnValue 才能弹窗 e.returnValue 确定要离开吗; return 确定要离开吗; };有些场景下表单数据未保存才需要提示保存成功后要移除这个事件监听否则用户每次离开都会被烦一次。6.5 out输出顺序错乱与 response 提交过早如果你在 JSP 页面里看到输出顺序不对或者在重定向时报 IllegalStateException十有八九是缓冲区已经 flush 或者页面输出太早导致的。记住一个原则想对 response 做“控制类操作”重定向、设置状态码、设置响应头一定要放在页面输出大量内容之前。如果业务逻辑要求必须先判断再跳转就不要在前面写大段 HTML 或者提前out.flush()。必要时可以调大 page 指令的 buffer 属性% page buffer32kb %这个设置能延迟响应提交的时机给后面的判断留出空间。但这不是万能的最根本的解法还是把业务判断前置不要边输出边决定要不要跳转。6.6 作用域取值为空的排查思路取不到值的时候不要急着怀疑对象出了问题按顺序排查第一步确认存数据和取数据的时机对不对request 范围的值是不是跨了重定向第二步确认存的时候 key 和取的时候 key 是不是完全一致包括大小写和空格第三步确认取出来的对象类型是否匹配强制转型失败也会导致页面报错第四步确认是不是多个浏览器窗口共用同一个 session 导致数据交叉。这套思路我用了很多年基本能解决九成以上的“明明存了为什么取不到”的问题。结语九大内置对象是 JSP 的骨架也是理解 Servlet 容器运作机制的一扇门。我个人的体会是刚学时不要硬背定义最好是打开 Tomcat 生成的源码看一眼再自己写一个小页面把 request、session、application 各存一个值观察它们在不同操作下的变化很快就能建立直觉。开发中遇到作用域串数据、输出顺序错乱、response 已提交这类问题时也可以先想想是不是内置对象的生命周期或者缓冲机制在背后捣乱。另外再分享一个小技巧如果你打算长期做 JavaWeb 开发不妨研究一下pageContext的源码它内部持有的那些对象和查找顺序其实就是整个 JSP/Servlet 容器对象模型的缩影。把这个点吃透了以后看 Spring MVC、看各种视图解析器都会顺畅很多。
返回列表