
接手一个七八年前的Java web项目页面里清一色是JSP。改需求的第一天我就被request对象教做人了——同事留下的代码里一个列表页的筛选条件怎么传都传不过去查了半天才发现是把getParameter和getAttribute混着用了。这种场景我相信很多搞Java的老兵都遇到过。JSP内置对象里的request看着简单真正用明白的人却不多尤其是从Servlet规范层面去理解它的人更少。这篇文章就专门把request对象掰开揉碎讲清楚。它是什么、能做什么、怎么用、坑在哪一次说完。不管你是刚学Java web的新手还是在老项目里挣扎的维护者又或是准备面试需要梳理基础的开发这篇文章都能帮你省下不少瞎试的时间。我尽量用实际项目里的真实场景来讲不整虚的。1. 内置对象全景request在JSP中的定位1.1 九大内置对象速览request排第几JSP为了简化页面开发预先定义了九个可以直接在脚本片段和表达式中使用的对象这就是所谓的内置对象隐含对象。它们分别是request、response、session、application、out、pageContext、config、page、exception。这九个对象不是随便设计的每一个都对应着web开发中的一类核心需求。request负责承接客户端发过来的所有请求数据response负责把处理结果写回客户端session保存单个用户会话状态application是所有用户共享的应用级数据仓库out是输出流pageContext是页面级上下文管理器config是Servlet配置信息page就是当前JSP实例本身exception只有在错误页里才有效。在这些对象里request的使用频率是最高的因为几乎每个页面都要读取客户端传过来的参数、请求头、Cookie等信息。我见过很多初学者把request仅仅当成“取参数”的工具实际上它的能力远不止于此。从Servlet规范的角度看request是javax.servlet.http.HttpServletRequest接口的一个实例这个接口继承了ServletRequest所以它既包含了通用的请求处理方法也包含了HTTP协议特有的方法。1.2 request对象从哪里来Servlet与JSP的关系要说清楚request对象的本质必须回到JSP和Servlet的关系上。JSP文件在第一次被访问时容器比如Tomcat会把JSP翻译成一个Servlet源码再编译成class文件。你写的一行行JSP代码最后都变成了_jspService(HttpServletRequest request, HttpServletResponse response)方法里的Java代码。这正是内置对象可以直接用的根本原因——它们不是凭空出现的而是_jspService方法签名里已经声明好的局部变量。容器在调用时把真实的请求和响应对象传了进来你在JSP页面里写request.getParameter(name)等价于在Servlet的doGet或doPost方法里写request.getParameter(name)。理解了这一点很多疑惑就能解开。比如为什么在JSP页面里不能重新声明一个名为request的变量——因为_jspService方法里已经有这个局部变量了你再声明一个就编译报错。又比如为什么在普通Java类里不能用request因为你没有那个方法上下文。有人说“JSP就是Servlet”从内置对象的角度看这句话是成立的而且这是理解整个Java web技术栈的重要基石。2. request对象核心方法解析从请求头到请求体2.1 获取请求头信息getHeader家族HTTP请求由请求行、请求头、请求体三部分组成。request对象针对这三部分都有对应的方法。先看请求头用得最多的就是getHeader(String name)传入头名称返回字符串值。String userAgent request.getHeader(User-Agent); String referer request.getHeader(Referer);User-Agent用来判断客户端是浏览器还是爬虫是手机还是PC我在做访问统计和页面适配时经常用。Referer表示来源页面可以用来做简单的防盗链——如果来源域名不在白名单里直接拒绝图片或文件访问。但getHeader有个细节容易被忽略有些头可能重复出现比如Cookie头在某些代理环境下会被合并成多个。这时候用getHeaders(String name)它返回EnumerationString遍历拿到所有值。我踩过一次坑用getHeader(Cookie)拿不到完整的Cookie串换成getHeaders遍历拼接才解决。还有一些衍生方法值得记住。getHeaderNames()返回所有请求头的名字调试时用它把所有头打印出来非常方便。getIntHeader(String name)直接把头的值转成int取不到或转换失败会抛NumberFormatException使用时最好先判断是否为空。getDateHeader(String name)则把头的值解析成毫秒时间戳适合处理If-Modified-Since这类时间头。2.2 获取请求参数getParameter与getParameterValues请求参数是日常开发接触最多的部分。getParameter(String name)按参数名取值返回StringgetParameterValues(String name)返回String数组专门处理同名多值的情况典型场景就是checkbox多选。String username request.getParameter(username); String[] hobbies request.getParameterValues(hobby);这两个方法背后有个共同的底层方法getParameterMap()它返回MapString, String[]把所有参数名和对应的值数组一次性拿出来。我排查询问题时最喜欢用它一行代码就能看到客户端到底传了哪些参数request.getParameterMap().forEach((k, v) - System.out.println(k Arrays.toString(v)));参数从哪来三处URL查询字符串Query String、表单体POST、Cookie。容器会把这几个来源按规范合并处理所以你在Servlet里不用区分参数是GET带的还是POST提交的。如果GET和POST都有同名参数哪个优先要看容器实现Tomcat的处理顺序是Query String在前还是Body在前这个细节不同版本可能有差异依赖这个行为写代码是危险的应该避免。还有一点参数值永远是字符串。就算你前端传的是数字到后端拿到的也是String需要自己转成int、long等类型。转换时要注意异常处理用户传入非法格式时Integer.parseInt会抛NumberFormatException我通常在工具类里包一层返回默认值或者null。2.3 request属性与参数别把它们搞混了这是新手最容易混淆的一组概念。参数Parameter来自客户端是用户或前端传过来的数据属性Attribute是服务端代码在请求处理过程中主动设置的数据用request.setAttribute(String name, Object value)存放用request.getAttribute(String name)取出。request.setAttribute(user, user); User user (User) request.getAttribute(user);为什么容易混因为方法名太像了一个getParameter一个getAttribute差三个字母。但它们的使用场景完全不同。参数是请求的输入属性是请求处理过程中的中间产物作用范围只在一次请求内转发到下一个页面后可以取到重定向后就丢了。我在实际项目里见过把业务对象塞进getParameter的写法——当然写不进去编译器就报错了。也见过用getAttribute去取表单字段值的取出来永远是null。记住一条从客户端取值用getParameter从服务端取值用getAttribute准没错。3. 实操用request处理表单数据与中文乱码3.1 GET和POST请求参数怎么传输表单提交有两种方式GET和POST它们在参数传输方式上有本质区别。GET请求的参数拼接在URL的查询字符串里https://example.com/login?usernameadminpassword123URL长度受限而且会在浏览器历史记录、服务器日志里留下痕迹不适合传敏感信息。POST请求的参数放在请求体里理论上长度不受限安全性相对好一些但本质上还是明文传输需要走HTTPS才能真正加密。在写JSP页面时form标签的method属性控制提交方式form actionlogin.jsp methodpost input typetext nameusername input typepassword namepassword button typesubmit登录/button /form后端用request.getMethod()可以拿到当前请求是GET还是POST这个值偶尔用来做统一入口的分流处理。比如同一个JSP页面GET进来显示空表单POST进来处理提交数据这种页面写法在老项目里很常见if (POST.equals(request.getMethod())) { // 处理表单提交 String username request.getParameter(username); // 业务逻辑... } else { // 显示空白表单 }3.2 中文乱码的根源与三种解决办法中文乱码是Java web开发里最经典的问题几乎每个人都遇到过。乱码的根源在于编码不一致浏览器用某种编码把中文转成字节流发送服务器用另一种编码去解码字节流两者对不上出来的就是乱码。先看POST请求。Tomcat 8.0及以上版本请求体解码的默认编码是UTF-8但如果你用的老版本Tomcat或者设置了其他编码就需要手动指定。方法是在读取任何参数之前调用request.setCharacterEncoding(UTF-8);注意这个方法必须在第一次getParameter之前调用否则容器已经按默认编码解析完参数再设置就来不及了。这也是为什么规范里建议把它放在JSP页面的最前面或者直接用Servlet的Filter统一设置。再看GET请求。GET参数在URL里Tomcat对URL的解码走的是URI编码。Tomcat 8.0以上默认是UTF-8问题不大。但如果是Tomcat 7及以下默认是ISO-8859-1中文参数必乱码。那时候的解决办法是修改server.xml里的Connector配置加上URIEncodingUTF-8Connector port8080 protocolHTTP/1.1 URIEncodingUTF-8/如果生产环境不方便改配置还有一个流传很广的土办法先用ISO-8859-1把参数还原成字节再用UTF-8重新解码String name request.getParameter(name); name new String(name.getBytes(ISO-8859-1), UTF-8);这个方法的原理是容器用ISO-8859-1解码时每个字节被直接映射成对应的Unicode字符没有信息丢失所以可以逆操作换编码。但这种方式写起来丑性能也差只适合应急。正规项目里我强烈建议配置Filter统一处理Spring框架里就是CharacterEncodingFilter一个请求进来先设置UTF-8全链路无忧。3.3 封装请求参数做一个简单的BeanUtils实际项目里表单字段往往很多用户注册可能有十几个字段一个个getParameter再set到实体类代码又长又臭。我习惯写一个简单的参数封装工具用反射把request里的参数自动映射到JavaBean上。核心思路不复杂拿到Bean的所有属性名去request里找同名参数匹配上就反射调用setter方法赋值。写成代码大概长这样public static void populate(Object bean, HttpServletRequest request) { BeanInfo info Introspector.getBeanInfo(bean.getClass()); for (PropertyDescriptor pd : info.getPropertyDescriptors()) { String name pd.getName(); String value request.getParameter(name); if (value null) continue; Class? type pd.getPropertyType(); Object converted convert(value, type); pd.getWriteMethod().invoke(bean, converted); } }convert方法按类型做转换String直接返回Integer用Integer.parseIntLong、Double、Boolean类似还要处理空字符串转为null的情况。这样封装后页面上所有表单字段一行代码就装进对象了。用Apache Commons BeanUtils或Spring的BeanUtils也可以项目里已有依赖就用现成的没有就自己写个二三十行的工具类完全够用。我在老项目里就是这么干的维护成本很低。4. 请求转发与request作用域4.1 请求转发和重定向两条完全不同的路Servlet或JSP里从一个页面跳到另一个页面有两种方式请求转发和重定向。它们看着都能“跳转”但底层逻辑完全不同。请求转发用的是RequestDispatcherRequestDispatcher dispatcher request.getRequestDispatcher(/success.jsp); dispatcher.forward(request, response);转发是一次请求内的服务器端跳转。浏览器只发了一次HTTP请求服务器把处理权从一个资源转给另一个资源URL地址栏不变。最关键的是request对象从头到尾是同一个你在第一个页面用request.setAttribute放的数据在第二个页面能取到。重定向用的是response.sendRedirectresponse.sendRedirect(success.jsp);重定向是两次HTTP请求。服务器返回一个302状态码和Location头浏览器收到后自动再发一次请求去访问新地址。地址栏会变成新URLrequest对象已经换了一个之前set进去的属性全部丢失。怎么选如果只是页面跳转不涉及数据传递重定向更合适因为地址栏变化让用户知道页面变了。如果是表单提交后要显示结果并且需要在跳转后的页面读取处理结果数据就用转发。还有一个经验表单提交后如果用转发用户刷新页面会重复提交表单这是老生常谈的问题所以“提交后重定向”模式Post/Redirect/Get就是专门解决这个的。4.2 request.setAttribute传值一次请求内的“临时仓库”request对象能当作用域用因为它内部维护了一个属性MapsetAttribute往里面放数据getAttribute取出来removeAttribute删掉。这个Map的生命周期和request一致——请求开始创建请求结束销毁。在一次请求转发链路上这个特性很有用。比如列表查询首先list.jsp分析参数、查数据库、把结果放到request里转发给display.jsp去渲染。两个页面共享同一个request数据传递自然顺畅。我在之前的毕业论文管理系统里就大量用到了这个模式。登录成功后把查询出来的用户信息塞进request转发到主页主页直接${sessionScope.user}或者${requestScope.user}取值。用EL表达式取request作用域的值时可以省略前缀直接写${user}EL会按page、request、session、application的顺序自动查找所以不会写错。不过有一个原则要记住request作用域只适合一次请求内的数据传递。如果数据要在多次请求之间共享应该放session要所有用户共享放application。放错了作用域轻则内存浪费重则数据错乱。我在review代码时经常发现有人把大数据量的列表塞到session里一个用户登录一次就占不少内存用户一多服务器就吃不消。4.3 转发与includeJSP页面组合的艺术除了forwardRequestDispatcher还有一个include方法用于在页面中嵌入另一个资源。和forward不同include执行完被包含的资源后控制权会回到主页面继续往下执行。RequestDispatcher rd request.getRequestDispatcher(/header.jsp); rd.include(request, response);这个机制在页面布局里很有用。做一个公共的头部、导航栏、页脚每个页面include一下改动公共部分时所有页面同步更新。老式JSP项目里这种页面组合是最常见的维护方式。和它容易混淆的是JSP的include指令% include fileheader.jsp %区别很微妙但很重要include指令是编译期静态包含header.jsp的源码在编译时被原样嵌入到当前页面里相当于两个页面合成一个Servletjsp:include是运行期动态包含header.jsp会被单独编译成Servlet运行时通过RequestDispatcher调用。静态包含的好处是性能好缺点是header里如果有变量和主页面冲突会编译报错动态包含灵活每次请求都重新执行被包含页面适合内容会变化的场景。实际项目里静态页头页脚用include指令动态内容块用jsp:include这个选择标准我一直沿用。5. 常见问题与排查技巧实录5.1 参数为null和空字符串两种不同的坑request.getParameter(xxx)返回null说明客户端根本没传这个参数返回空字符串说明传了但值是空的。这两种情况在业务处理里必须区分。用户没填表单提交过来后端拿到的是空字符串URL里没带某个查询参数拿到的是null。处理不当会出现诡异的问题。比如判断“用户是否传了参数”if (request.getParameter(keyword) ! null) { // 用户传了关键词执行查询 }这种写法有个隐患用户传了空的keyword进入查询分支keyword.trim().length()为0可能查出全量数据也可能拼SQL时报错。正确做法是String keyword request.getParameter(keyword); if (keyword ! null keyword.trim().length() 0) { // 执行查询 } else { // 不查或查全部 }另一种常见场景是把参数转数字。URL里传idgetParameter返回空字符串Integer.parseInt()直接抛异常。所以我封装的转换工具里空字符串一律转成null或者defaultValue永远不让NumberFormatException飞到用户面前。5.2 参数拼写错误最隐蔽的Bug来源排查参数问题时不要相信自己的记忆也不要相信前端代码写的name直接用getParameterMap()打印出所有参数名和值一眼就能看出问题在哪。MapString, String[] params request.getParameterMap(); params.forEach((key, value) - System.out.println(key : Arrays.toString(value)));这个方法帮我解决了无数肉眼找不到的问题。有一次排查“修改密码不生效”前端明明提交了newPassword后端读的却是newPwd不打印永远看不出来。还有一个经典场景是前端框架格式化后的参数名带点号比如user.name在后端直接用getParameter(user.name)能取到但如果用了某些框架的自动绑定点号会被当作属性路径解析又是另一番折腾。5.3 请求转发后CSS和图片加载失败转发的坑不在后端在前端。当你在/user/detail这个URL上forward到/WEB-INF/page/detail.jsp时页面里的相对路径资源就全乱了。浏览器地址栏还是/user/detail页面里写的css/style.css会被解析成/user/css/style.css404一片。解决办法有几种。最简单的是在JSP页面的head里加base标签base href${pageContext.request.contextPath}/这样页面上所有相对路径都以应用根路径为基准不管从哪个URL转发进来都不会错。这个代码我在老项目里加过无数次每次都能解决问题。另一个方案是全部用绝对路径比如${pageContext.request.contextPath}/css/style.css代码里到处写contextPath维护稍微麻烦点。5.4 多值参数的取值checkbox真实场景表单里同一组checkbox选中多个值时多个同名参数会同时发送到后端。用getParameter只能拿到第一个必须用getParameterValuesString[] hobbies request.getParameterValues(hobby); if (hobbies ! null) { for (String hobby : hobbies) { System.out.println(hobby); } }这个场景在用户画像、权限配置、多选筛选条件里都很常见。别忘了判空——用户一个都没选时getParameterValues返回null直接遍历会抛NullPointerException。还有一个细节值得注意getParameterValues返回的数组长度永远大于等于1。如果前端确实传了一个空值数组长度是1元素是空字符串。这和你调用getParameter(hobby)拿到空字符串是等价的。在处理多选值时如果想区分“没选”和“选了空值”只能通过数组元素内容来判断这一点容易被忽略。5.5 一次真实排查实录request参数神秘丢失去年维护一个JSP写的老系统用户反馈说在列表页点“下一页”时搜索条件全部丢失翻到第二页就变成全量数据了。我看了一下代码列表页用GET方式提交搜索表单分页链接是手工拼的a hreflist.jsp?page2下一页/a问题在这里搜索条件是表单里的字段用户点击“下一页”这个超链接时只有page2这个参数随GET请求发送搜索条件根本没拼到链接里。后端只获取到了分页参数搜索条件自然是null或者空。解决办法是在服务端把搜索条件拼到URL里。我写了个工具方法接收request的多个参数名动态生成带全部参数的分页链接public static String buildPagedUrl(HttpServletRequest request, int page) { StringBuilder sb new StringBuilder(request.getRequestURI()); sb.append(?page).append(page); request.getParameterMap().forEach((k, v) - { if (!page.equals(k)) { Arrays.stream(v).forEach(val - sb.append().append(k).append().append(encode(val))); } }); return sb.toString(); }这种问题在老项目中很典型列表页的搜索条件和分页经常是两套逻辑各管各的稍不注意就丢了参数。理解了request参数的来源和拼接方式解决起来就快多了。这是我个人在实际项目里反复踩过的坑分享出来希望你能少走弯路。