
做Java Web开发的人不管现在项目里还用不用JSP大概率都绕不开“九大内置对象”这个词。学校课程讲、老项目在用、面试题也爱问。我带新人的时候最常被问到的一个问题就是为什么我在JSP页面里连new都不用写直接就能用一个叫request的对象它到底是谁凭什么能用这篇就以request对象为主线把这个内置对象从“背定义”提升到“随手用”的层面。在JSP九大内置对象里request是绝对的主角。做表单提交、用户注册、个人信息展示页面凡是需要从浏览器拿数据的场景第一行核心代码基本就是request.xxx。不管你是准备面试、写课程作业还是在维护一个老Web项目把这篇看完遇到和request相关的取参、乱码、传值、跳转问题应该都能心里有数。我尽量按实战思路来讲不复制教材里那套干巴巴的方法列表。1. 为什么JSP页面里能直接用request——内置对象到底从哪来1.1 JSP本质上是穿了马甲的ServletJSP全称是Java Server Pages很多人把它当成“页面模板”来学但它的底层其实是一个Servlet。页面文件在被第一次访问时容器会把它翻译成一个Java类页面里所有的HTML、标签、脚本片段全部会被塞进一个名叫_jspService的方法里。这个方法的长相是这样的public void _jspService(HttpServletRequest request, HttpServletResponse response) { // 容器先在这里声明一堆局部变量 // 然后把你页面里的输出语句逐行放进来 }看到这里你就明白了JSP里能直接用的request、response其实就是这个方法的两个参数。浏览器发来一次请求容器把这次请求封装成一个实现了HttpServletRequest接口的对象然后调用_jspService方法把你页面里的代码从头跑一遍。所以我一直建议学JSP的人做一件事随便写一个最简单的JSP页面访问一次之后去Tomcat的work目录里翻到翻译出来的.java源文件从头到尾读一遍。有过这个动作内置对象“从哪来”的问题就彻底通了后面学Servlet、学Spring MVC都会顺很多——因为本质上都是同一套东西。1.2 九大内置对象的身份卡JSP规范一共定义了九个内置对象它们都是在_jspService方法里预先声明好的局部变量。我直接列一张表内置对象真实类型作用范围日常用途requestHttpServletRequest一次请求读取参数、请求头、转发、传值responseHttpServletResponse一次响应设置响应头、重定向、写输出pageContextPageContext当前页面访问其他内置对象、页面作用域sessionHttpSession一个会话保存登录状态、购物车applicationServletContext整个应用全局配置、访问次数统计outJspWriter当前响应向页面输出内容configServletConfig当前Servlet读取初始化参数pageObject当前页面当前Servlet实例基本不用exceptionThrowable错误页配合isErrorPage使用业务开发中打交道最多的是request、response、session、application这四个。request负责“接收信息”response负责“发送信息”session负责“记住你是谁”application负责“大家共享”。搞清楚这层分工后面各种数据放哪里的问题就迎刃而解。1.3 request的真实身份HttpServletRequest接口在Tomcat里你在JSP页面中拿到的request实际对象类型是org.apache.catalina.connector.RequestFacade它实现了HttpServletRequest接口。而HttpServletRequest又继承自ServletRequest所以request身上既有通用请求的方法getParameter、getAttribute也有HTTP协议特有的方法getMethod、getHeader、getCookies。这个继承关系有很实际的用处以后你写Servlet里的doGet(HttpServletRequest request, ...)或者在Spring MVC的Controller方法里注入HttpServletRequest参数用的都是同一套API。也就是说JSP里学的这些方法到了框架时代照样能用框架只是帮你又封装了一层而已。2. 请求参数、请求头与请求行request核心方法实操2.1 单值参数与多值参数getParameter和getParameterValues先看最基础的从表单或者URL里拿参数。比如这样一个注册表单form actionregister.jsp methodpost 用户名input typetext nameusername/br/ 密码input typepassword namepassword/br/ 爱好input typecheckbox namehobby value读书/读书 input typecheckbox namehobby value运动/运动 input typecheckbox namehobby value编程/编程br/ input typesubmit value提交/ /form在register.jsp里接收% String username request.getParameter(username); String password request.getParameter(password); String[] hobbies request.getParameterValues(hobby); %一个name只对应一个值用getParameter一个name可能有多个值比如checkbox多选要用getParameterValues拿字符串数组。这个区分是初学者最容易犯的错误之一——以为getParameter(hobby)能拿到全部选中的选项实际上只能拿到第一个。还有一个高发坑必须重点说getParameter在参数不存在时返回null不是空字符串。如果你直接在页面表达式里写欢迎您% request.getParameter(username) %当username没有传过来时页面上就会出现“欢迎您null”非常难看。正确做法是先判空% String username request.getParameter(username); if (username ! null !username.trim().isEmpty()) { out.println(欢迎您 username); } %另外记住一点getParameter返回的永远是字符串。要数字得自己转换比如Integer.parseInt(request.getParameter(age))转换之前必须处理null和格式异常否则一个空参数就能让页面抛NumberFormatException白屏一片。2.2 一键全收getParameterMap的巧用有时候参数很多或者你正在写调试工具不想挨个写getParameter可以用getParameterMap()把所有参数一次性拿进来% MapString, String[] paramMap request.getParameterMap(); for (Map.EntryString, String[] entry : paramMap.entrySet()) { out.println(entry.getKey() Arrays.toString(entry.getValue()) br/); } %用这个方法要注意两个细节值的类型永远是String[]即使某个参数只传了一个值这也是为什么很多人说“getParameterMap取出来的全是数组”。这个Map是容器提供的只读视图你不能往里put数据否则运行时会报错。要往参数集合里添加内容必须自己先做拷贝。对于动态表单处理、日志记录、参数透传这类需求getParameterMap是效率很高的工具。我在做接口调试页面时经常用它一个循环就能把全部表单内容枚举出来。2.3 请求头信息getHeader和getHeaderNames请求头是HTTP请求里容易被忽略但信息量很大的部分。浏览器会把客户端环境、Cookie、引用页、编码偏好、缓存策略等信息都放在请求头里带给服务器。比如request.getHeader(User-Agent)就能拿到浏览器的完整标识。遍历所有请求头的写法% EnumerationString headerNames request.getHeaderNames(); while (headerNames.hasMoreElements()) { String name headerNames.nextElement(); String value request.getHeader(name); out.println(name : value br/); } %“把所有请求头打印出来”这个操作我每次排查线上问题都要用到。比如怀疑代理缓存问题看一眼Cache-Control就能判断想知道用户从哪个页面跳转过来的看Referer要做移动端和PC端的简单区分看User-Agent里的字段就能写个粗糙的设备判断。2.4 方法、URI、查询串、客户端IP请求行上的信息HTTP请求由请求行、请求头、请求体三部分构成。请求行上也有不少信息可以用request拿到方法返回内容典型用途getMethod()GET / POST 等区分提交方式走不同逻辑getRequestURI()/项目名/页面.jsp日志记录、权限判断getQueryString()问号后的原始串参数透传、诊断问题getRemoteAddr()客户端IP访问统计、IP黑白名单getProtocol()HTTP/1.1 等协议判断getContentType()请求体类型判断是form表单还是JSON这里特别提醒一点getRemoteAddr()拿到的IP如果项目部署在Nginx、Apache或者云负载均衡后面往往不是真实客户端IP而是经过最后一跳代理的IP。常见做法是从X-Forwarded-For或X-Real-IP请求头里取但也要注意客户端可以伪造这些头需要在网关层配合配置一起信任。3. 中文乱码的根源与系统性解法request这一层的编码链路3.1 乱码不是在某个环节一次性发生的我见过太多人遇到中文乱码就乱试改JSP的pageEncoding、加meta标签、在代码里new String(旧字节, ISO-8859-1)试了一大圈还是乱。原因是编码问题往往不止一个源头而且每个环节“各管一段”。一次表单提交中文要走过这样一条链路页面按UTF-8显示和提交 - 浏览器按页面编码把表单数据转成字节 - 字节经过HTTP传输到达服务器 - 服务器按某种编码解码成String - JSP输出时按某种编码生成HTML - 浏览器再按某种编码显示request在这一串环节里负责的是“服务器解码”这一段。也就是说浏览器给你的是一堆字节到底用什么字符集把这堆字节变成字符串这是request能控制的。3.2 POST请求setCharacterEncoding必须在第一次取参数之前对于POST表单参数在请求体里。要让request正确解码必须调用% request.setCharacterEncoding(UTF-8); %这句话的生效前提非常严格必须在第一次调用getParameter、getParameterValues或getParameterMap之前执行。因为容器是在第一次取参数的时候按照当时设置的编码去解析请求体的。一旦参数已经被解析成String你再改编码也影响不到已经解析出来的内容了。我复现过很多次这种现场JSP页面顶部写了pageEncoding和contentType也在代码里调了setCharacterEncoding但乱码依旧。最后翻代码发现是某个Filter或者公共方法提前调了一次getParameter把参数先解析掉了。这种问题特别隐蔽排查方向就是要“把setCharacterEncoding放在最早、最统一的位置”。3.3 GET请求的参数在URL上和POST完全不是一回事GET请求的参数不在请求体里在URL上所以setCharacterEncoding对GET是无效的。GET的中文乱码主要取决于服务器对URI的默认解码方式。Tomcat 8.0及以上版本默认将URI编码设置为UTF-8所以正常情况下GET带中文参数不乱码。Tomcat 7及更早版本默认用ISO-8859-1这种情况就要在server.xml的Connector节点上加Connector port8080 protocolHTTP/1.1 URIEncodingUTF-8 /提醒一句改server.xml影响的是整个应用生产环境改完要重启Tomcat属于容器级配置变更最好走正规变更流程别在开发环境偷偷改了就觉得万事大吉。3.4 最可靠的统一方案一个Filter搞定全站编码与其在每个JSP里写setCharacterEncoding不如把所有请求在一个Filter里统一处理public class EncodingFilter implements Filter { public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { request.setCharacterEncoding(UTF-8); response.setCharacterEncoding(UTF-8); chain.doFilter(request, response); } }这个方案的思路是把零散在各页面的编码设置收敛到系统的统一入口。Spring框架里的CharacterEncodingFilter也是同样的思路forceEncodingtrue时连响应编码一起强制设置。我在老项目里做乱码治理基本都是先加这么个Filter再检查页面本身的contentType双管齐下乱码基本能根治。4. 拿着request传数据请求转发、作用域与会话的分工4.1 请求转发把当前请求“递交”给下一个资源request有一个重要方法getRequestDispatcher(path)可以拿到一个RequestDispatcher再通过forward把当前请求转交给另一个资源request.getRequestDispatcher(/welcome.jsp).forward(request, response);转发的核心特点是全程只有一个请求。页面A接收请求后容器在服务器内部把控制权交给页面B浏览器从头到尾不知道内部发生了转发地址栏也不会变。这个特性带来一个很有用的结果由于是同一个请求你在页面A里往request里放的数据页面B能原样读到。这就是request作为“一次请求范围内的数据传递通道”的底层基础。4.2 request作用域setAttribute和getAttribute典型场景是处理完登录逻辑要把用户名带到一个欢迎页。先在处理逻辑里放数据request.setAttribute(loginUser, username); request.getRequestDispatcher(/welcome.jsp).forward(request, response);在welcome.jsp里取% String loginUser (String) request.getAttribute(loginUser); if (loginUser ! null) { out.println(欢迎回来 loginUser); } %注意getAttribute返回的是Object需要强转成你放进去的类型属性不存在时返回null所以又要判空。这里必须强调一下getParameter和getAttribute的区别这是初学者最容易混的两个方法getParameter获取的是浏览器提交的参数是客户端给的getAttribute获取的是服务端在request里存入的属性是服务器自己放的记住一句话就够了参数是浏览器带来的属性是服务器放进去的。4.3 include和forward的区别别搞混RequestDispatcher除了forward还有一个include方法。forward是把当前请求完全转交给目标资源之后当前页面代码不再继续执行include则是把目标资源的输出“包含”进当前页面两段代码都会执行并且共享同一个request对象。做布局模板、页头页脚这类场景更适合include。这个区别是面试里高频送分题实际编码中选错了也会导致输出内容和预期不符。4.4 request、session、application三个作用域怎么选三个作用域配合场景区分作用域有效范围典型用途使用建议request一次请求内表单回显、临时数据、转发传值优先使用session一个浏览器会话登录态、购物车、用户偏好精简使用application整个应用全局配置、公共数据注意并发安全我见过很多老项目session里塞了十几个key什么数据都往session放结果用户开多个标签页就串数据登出也不清干净。这里分享一个适用性很强的原则一次请求结束后就没用的数据用request需要在整个用户访问期间保留的用session全局所有人都一样的数据用application。能用request解决的绝不要上session。5. 三个实战场景把request用顺手的正确姿势5.1 表单校验失败之后的回显做注册页面用户填了一堆内容提交校验说用户名已被占用返回注册页时如果所有字段都被清空了用户得全部重填体验非常差。正确做法是把已填内容放回request然后转发回表单页面request.setAttribute(oldUsername, username); request.setAttribute(oldEmail, email); request.setAttribute(errorMsg, 用户名已被注册); request.getRequestDispatcher(/register.jsp).forward(request, response);register.jsp的表单里这样回显input typetext nameusername value% request.getAttribute(oldUsername) null ? : request.getAttribute(oldUsername) %/用户被退回来时填过的内容还在只需要改出问题的那一处。这是request作用域最典型的应用也是“回显”这个说法的由来。5.2 一个简易的防重复提交令牌表单重复提交是老问题用户双击提交按钮、提交后刷新页面都可能把同一笔数据写入两次。一个经典解法是令牌机制打开表单页时生成唯一token放入session同时在表单里放一个隐藏域表单提交时对比request参数里的token和session里的token一致则正常处理并把session里的token移除让同一个token只能使用一次不一致就拒绝String tokenFromForm request.getParameter(token); String tokenFromSession (String) session.getAttribute(token); if (tokenFromForm ! null tokenFromForm.equals(tokenFromSession)) { // 正常处理表单业务 session.removeAttribute(token); } else { // 拒绝提示用户不要重复提交 }这个场景里request.getParameter是数据入口session是令牌仓库两者配合就把“防止重复提交”这个需求落地了。实际系统里还能加验证码、超时时间戳等增强但核心思路都是“通过同一次请求传递关键标识”。5.3 一个所见即所得的请求调试页最后分享一个我长期放在项目里的调试小工具——debug.jsp。作用就是把当前请求的所有参数、请求头、基本信息原样打印出来% page contentTypetext/html;charsetUTF-8 languagejava % % page importjava.util.* % html headtitle请求调试页/title/head body h3请求参数/h3 % MapString, String[] params request.getParameterMap(); for (Map.EntryString, String[] entry : params.entrySet()) { out.println(entry.getKey() Arrays.toString(entry.getValue()) br/); } % h3请求头/h3 % EnumerationString headers request.getHeaderNames(); while (headers.hasMoreElements()) { String name headers.nextElement(); out.println(name request.getHeader(name) br/); } % h3基本信息/h3 % out.println(Method request.getMethod() br/); out.println(URI request.getRequestURI() br/); out.println(QueryString request.getQueryString() br/); out.println(RemoteAddr request.getRemoteAddr() br/); % /body /html遇到“参数没收到”“编码不对”“不知道客户端到底发了什么”这类问题把这页面的路径拼在接口后面访问一次所有线索一目了然。排查效率比加日志、打断点快得多我在老项目里靠它救过不少次急。6. 用request容易踩的坑真实报错现场的复盘6.1 getInputStream之后getParameter突然拿不到值做文件上传和JSON接口开发时经常遇到这个坑代码先调了request.getInputStream()或getReader()读取请求体之后再去getParameter(xxx)发现返回null。原因是请求体的字节流只能被读取一次。你提前把请求体消费掉了容器就没法重新解析出表单参数。当业务同时需要“读取原始请求数据”和“获取表单参数”时不能指望同一个request两边都要。解决办法要么分开处理要么使用能缓存请求体的包装对象比如Spring提供的ContentCachingRequestWrapper。6.2 转发路径必须以斜杠开头getRequestDispatcher的路径写法有讲究最稳妥的方式是request.getRequestDispatcher(/pages/welcome.jsp).forward(request, response);以斜杠开头表示相对于当前Web应用的根路径。如果少了最前面的斜杠或者写成了相对路径很容易遇到容器找不到目标资源的报错。项目目录结构越深这个坑越容易踩进去而且报错信息还比较难懂排查起来费时间。6.3 getSession() 会悄悄帮你建会话request.getSession()等价于getSession(true)意思是如果当前没有会话就自动创建一个。有些场景你只是想看看当前用户是否已登录并没有打算主动建会话这时候应该用getSession(false)。否则一个简单的展示页面就能让服务端为每个游客都生成一个session对象白白占内存。我做过一次性能排查一个纯展示页面没有任何登录逻辑只因为顺手调了request.getSession()结果每个访问者都会在服务端产生session积累多了内存压力非常大。改成getSession(false)之后未登录用户不再产生会话内存曲线立刻平稳了。6.4 JSP里Java代码越少越好request的定位要清晰最后说点思路层面的东西。JSP里虽然可以直接写脚本片段什么request.getParameter、request.getAttribute随便用但页面里塞满Java代码的项目后续维护基本都是灾难。现在的主流习惯是JSP只负责展示业务逻辑放到Servlet、Service层或者交给框架处理。request在JSP里的核心职责就是“读参数、取属性”复杂的业务判断不要堆在页面上。我在维护老项目的时候经常收到这种工单“页面某个地方要加个判断”打开一看JSP里已经躺了几百行脚本片段。这种代码不是说不能改而是每改一行都要特别小心——JSP里的Java代码最终都混在_jspService那个大方法里和页面的HTML输出交织在一起一旦出错定位成本比正常工程高得多。关于request对象我能分享的实操经验大概就是这些。说到底它就是一个封装了“浏览器这次请求带给我的一切”的对象读参数找它、读头找它、转发传值也找它。你把它当成“这波请求的服务员”浏览器带了什么、想要什么你都通过它去问。用熟了再看Servlet里的HttpServletRequest、Spring MVC里的Controller参数会发现完全是一脉相承的思路。