做Java Web前端的同学,尤其是刚接触JSP的初学者,十有八九第一个真正用熟的内置对象就是request。它和你每天打交道最多:用户提交了一个表单,你想知道填了什么;页面之间要跳转,你想带点数据过去;甚至你想看看用户用的什么浏览器、从哪里访问,统统都得靠它。可以说,弄懂了request,Java Web前端的入门之路就打通了一半。
这篇内容不打算把JSP九大内置对象挨个念一遍,而是只针对request这一个对象,从原理到常用方法,从取参到转发,从乱码到坑点,全部掰开揉碎讲清楚。适合刚接触Servlet和JSP的后端初学者,也适合做Java Web课程设计、毕业设计时需要快速上手页面交互的同学。看完之后,你至少能独立完成一个带表单提交、参数获取、请求转发和个人信息展示的完整JSP页面。
1. JSP内置对象与request的定位:为什么它排在第一个
1.1 九大内置对象里,request和response是最核心的一对
JSP内置对象不需要你手动new就能直接用,因为它们都是容器在页面运行时自动创建好的。九大内置对象分别是request、response、out、session、application、pageContext、config、page、exception,其中request和response是所有请求处理的两条主线:request代表浏览器发过来的"问话",response代表服务器回给浏览器的"应答"。其他对象要么是围绕请求过程中的数据保存(session、application、pageContext),要么是输出辅助(out),要么是配置信息(config、page、exception)。
这里想强调一个容易被忽略的点:JSP页面本质上就是一个Servlet。Tomcat会把JSP编译成对应的Java类,然后这个类的_jspService(HttpServletRequest request, HttpServletResponse response)方法就是整个页面的执行入口。你在JSP里能直接用的request,就是这个方法参数里的HttpServletRequest对象。所以,研究JSP内置对象,本质上就是在研究Servlet API里的HttpServletRequest和HttpServletResponse,这个定位搞清楚了,很多"为什么能直接用"的疑问自然解开。
1.2 request对象到底封装了什么
你可以把request理解成一个"快递包裹",里面装了浏览器在发出请求那一刻的所有信息,主要分成三大类。
第一类是请求行信息,就是请求行里那一行的内容,包括请求方式(GET还是POST)、请求的URL路径、使用的HTTP协议版本。这些信息直接决定了你页面后面怎么处理。
第二类是请求头信息,也就是HTTP头那一堆键值对,比如User-Agent(浏览器类型)、Referer(从哪个页面跳过来的)、Cookie(浏览器本地保存的数据)、Accept-Language(用户语言偏好)等。这些Header信息平时用得相对少,但在防爬虫、做下载统计、判断客户端类型时非常有用。
第三类是请求参数,也就是用户真正提交的业务数据。无论用户是在URL后面用?name=xxx拼接的查询参数,还是在表单里通过POST提交的字段,到了服务器端都被解析成一组一组的键值对。request对象把这些参数放到了一个类似Map的结构里,通过getParameter系列方法就可以随时取出来。
记住一句话:凡是客户端能告诉服务器的信息,基本都在request里。你在Servlet或者JSP里处理的业务逻辑,绝大多数都是从"从request里拿东西"开始的。
2. request核心方法拆解:从取参数到传值一网打尽
2.1 获取请求参数:getParameter、getParameterValues与getParameterMap
你写页面的第一件事,大概率就是接收上一个页面传过来的参数。request获取参数最常用的方法是getParameter(String name),它接收一个参数名,返回对应的参数值,格式是字符串。
举个最简单的例子。用户在一个注册页输入了用户名,提交到一个名为reg.jsp的处理页,这个页面想拿到用户名,代码就是:
<% String username = request.getParameter("username"); out.print("你输入的用户名是:" + username); %>看起来很容易,但有几个隐藏的细节需要特别注意。
第一,getParameter一次只能拿一个值。如果页面上有多个同名控件,比如一组同名的checkbox多选框,那用getParameter只能拿到第一个值,后面的全丢了。这时候得换getParameterValues(String name),它返回String数组:
<% String[] hobbies = request.getParameterValues("hobby"); if (hobbies != null) { for (String hobby : hobbies) { out.print("选中爱好:" + hobby + "<br/>"); } } %>第二,如果你既不想一个字段一个字段地取,又想知道这次请求一共传了哪些参数,可以直接用getParameterMap()方法,把整个参数集合当成一个Map来遍历:
<% Map<String, String[]> paramMap = request.getParameterMap(); for (Map.Entry<String, String[]> entry : paramMap.entrySet()) { String paramName = entry.getKey(); String[] paramValues = entry.getValue(); out.print("参数名:" + paramName + ",参数值:" + Arrays.toString(paramValues) + "<br/>"); } %>这个方法在做通用日志记录、参数自动封装的时候特别方便,比如你想把前端传上来的所有参数原样打出来调试,一行遍历就全出来了。
第三,也是最常见的坑:参数永远是字符串。用户提交的是"18",你用getParameter拿到的是字符串"18",不是数字18。想比较年龄大小、做数值运算,必须手动转成int或double。转的时候建议用Integer.parseInt,但要先做好判空,否则参数没传时getParameter返回null,直接parse会抛NumberFormatException。
2.2 获取请求行信息:method、URI与协议
很多新人不理解为什么要看请求方式,总觉得"接口能通就行"。实际上,GET和POST在业务语义上有明显区别:GET通常用于查数据、带参跳转,参数会暴露在URL和浏览器历史里;POST通常用于提交数据、修改状态,参数放在请求体里,相对隐蔽且能承载更大的数据量。
在页面里判断用户是GET还是POST过来的,是getMethod()方法:
<% String method = request.getMethod(); if ("GET".equals(method)) { out.print("这是GET请求"); } else if ("POST".equals(method)) { out.print("这是POST请求"); } %>需要知道的还有这几个:
getRequestURI():返回请求的资源路径,比如/jsp_learn/reg.jsp,通常用于记录日志或做权限判定。getRequestURL():返回URL全地址,比如http://localhost:8080/jsp_learn/reg.jsp,包含协议、域名、端口和路径,适合做跳转地址拼装。getRemoteAddr():返回发起请求的客户端IP地址,比如192.168.1.100。做访问日志的时候必用。getProtocol():返回协议和版本号,比如HTTP/1.1。
一个典型的场景是,你做了个后台管理页面,不想让别的机器随便访问,可以先用getRemoteAddr()拿IP做个校验,不是内网IP就拒绝。虽然真实项目里会把这层逻辑放到Filter里做,但在JSP学习阶段,用内置对象直接判断也是个很好的练手方式。
2.3 获取请求头:User-Agent与防盗链
HTTP请求头里藏着很多"隐形的信息",其中日常开发里最常用的两个请求头是User-Agent和Referer。
浏览器访问你的页面时,会自动带上自己的User-Agent,标明自己是Chrome、Firefox还是Safari。你可以用getHeader("User-Agent")把它读取出来。常见的应用场景是下载页面判断客户端类型:手机访问就提示下载App,PC访问就提供安装包;再比如有些老项目需要兼容IE浏览器,发现UA里包含"MSIE"或者"Trident",就自动跳到一个低版本兼容页面。
Referer则是记录用户是从哪个页面链接跳转过来的。图片防盗链就是利用这个头做的:如果Referer不是你自己域名的页面,就返回一张替换的提示图。注意getHeader("Referer")可能为null,比如用户直接在浏览器地址栏输入网址访问就不会带Referer,所以用的时候也要判空。
遍历所有请求头的方法也很有用:
<% Enumeration<String> headerNames = request.getHeaderNames(); while (headerNames.hasMoreElements()) { String headerName = headerNames.nextElement(); String headerValue = request.getHeader(headerName); out.print(headerName + ":" + headerValue + "<br/>"); } %>新手调试的时候,把这段代码丢到一个临时页面里跑一下,就能看见浏览器发来的全部请求头信息,对理解HTTP协议非常有帮助。
2.4 请求作用域:setAttribute与getAttribute
request对象不只是"接数据",它也能"带数据"。在JSP里,如果你想把一些计算好的结果传递给另一个页面,可以使用setAttribute(String name, Object value)往request里放数据,然后在目标页面用getAttribute(String name)把数据取出来。
<% // 页面A:把用户信息放入request User user = new User("张三", 25); request.setAttribute("userInfo", user); request.getRequestDispatcher("show.jsp").forward(request, response); %><%-- 页面B:show.jsp 取出数据 --%> <% User user = (User) request.getAttribute("userInfo"); out.print("用户名:" + user.getName() + ",年龄:" + user.getAge()); %>这就是典型的"请求转发 + request域传值"组合。注意setAttribute放的是Object,取出来的时候需要强转成原来的类型。如果request里没有对应的key,getAttribute返回的是null,所以取出来之后最好先判空再使用。
这里要重点区分两个概念:setAttribute/getAttribute操作的是"服务器端内存中的数据",而getParameter读取的是"客户端传过来的请求参数"。两者都通过request调用,但完全是两回事。初学者经常混,看到getParameter取不到数据,就以为是setAttribute没放进去,其实是搞混了数据来源。
2.5 请求转发:getRequestDispatcher的配合用法
request.getRequestDispatcher(String path)返回一个RequestDispatcher对象,调用它的forward(request, response)方法,可以把当前请求转发到另一个Servlet、JSP或HTML页面。
<% request.getRequestDispatcher("/login.jsp").forward(request, response); %>forward执行后,浏览器地址栏的URL不会变化,整个过程发生在服务器内部,对用户来说就像访问了一个页面。这种做法在MVC模式中非常常见:Servlet处理完业务逻辑后,把结果数据放进request,然后forward到JSP做展示。由于request域在这次请求范围内是共享的,因此转发过去的页面能拿到之前setAttribute的数据。
很多人会把forward和response.sendRedirect搞混。forward是"服务器内部跳转",只发一次请求;sendRedirect是"浏览器重新发一次请求",跳转后地址栏会变。需要强调的是,forward之后不要在代码里再往response里写内容,否则可能报IllegalStateException或者页面内容重复输出。
3. 实操:用request对象完成一个完整的用户信息展示页面
3.1 需求拆解与页面设计
光讲方法不落地等于白看。这里带着完整做一个"个人信息展示页面",把request对象的多数知识点串起来。需求很简单:一个表单页让用户填写姓名、年龄、性别、爱好、备注,提交后跳到一个展示页,把刚才填的信息全部回显出来,同时在展示页把这次请求的IP、浏览器信息和请求方式也一并显示出来。
这个需求覆盖了getParameter单值取值、getParameterValues多值取值、getHeader请求头读取、getRemoteAddrIP获取、getMethod请求方式判断以及表单POST提交的中文乱码处理。本就是热搜词里"jsp个人信息展示页面"的典型场景,做完这个,你对request的日常用法就有了完整的肌肉记忆。
3.2 表单页实现:form.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %> <html> <head> <title>个人信息填写</title> <meta charset="UTF-8"> </head> <body> <h2>请填写你的个人资料</h2> <form action="show.jsp" method="post"> 姓名:<input type="text" name="name"/><br/><br/> 年龄:<input type="text" name="age"/><br/><br/> 性别: <input type="radio" name="gender" value="男" checked/>男 <input type="radio" name="gender" value="女"/>女 <br/><br/> 爱好: <input type="checkbox" name="hobby" value="Java"/>Java <input type="checkbox" name="hobby" value="前端"/>前端 <input type="checkbox" name="hobby" value="数据库"/>数据库 <input type="checkbox" name="hobby" value="算法"/>算法 <br/><br/> 备注: <textarea name="remark" rows="3" cols="40"></textarea> <br/><br/> <input type="submit" value="提交信息"/> </form> </body> </html>这里有几个设计细节要说一下。性别用单选框设置name="gender",四个爱好共用name="hobby",这样在服务器端就能对应上单值参数和多值参数的不同取法。表单的method设置为post,是为了演示POST请求参数的获取方式,同时避开GET方式下中文参数在URL里的编码问题。表单提交目标show.jsp的action写成相对路径,是因为两个页面在同一个目录下。
3.3 展示页实现:show.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ page import="java.util.Arrays" %> <html> <head> <title>个人信息展示</title> <meta charset="UTF-8"> </head> <body> <% // 处理POST请求的中文乱码,必须在获取任何参数之前调用 request.setCharacterEncoding("UTF-8"); // 1. 取单个参数 String name = request.getParameter("name"); String age = request.getParameter("age"); String gender = request.getParameter("gender"); // 2. 取多个同名参数(复选框) String[] hobbies = request.getParameterValues("hobby"); String hobbyStr = (hobbies != null) ? Arrays.toString(hobbies) : "无"; // 3. 取文本框内容 String remark = request.getParameter("remark"); // 4. 取请求行信息 String method = request.getMethod(); String ip = request.getRemoteAddr(); String uri = request.getRequestURI(); // 5. 取请求头信息 String userAgent = request.getHeader("User-Agent"); %> <h2>你提交的个人信息如下</h2> <table border="1" cellpadding="8" cellspacing="0"> <tr> <td>姓名</td> <td><%= name %></td> </tr> <tr> <td>年龄</td> <td><%= age %></td> </tr> <tr> <td>性别</td> <td><%= gender %></td> </tr> <tr> <td>爱好</td> <td><%= hobbyStr %></td> </tr> <tr> <td>备注</td> <td><%= remark %></td> </tr> </table> <h3>本次请求的技术信息</h3> <table border="1" cellpadding="8" cellspacing="0"> <tr> <td>请求方式</td> <td><%= method %></td> </tr> <tr> <td>客户端IP</td> <td><%= ip %></td> </tr> <tr> <td>请求URI</td> <td><%= uri %></td> </tr> <tr> <td>User-Agent</td> <td><%= userAgent %></td> </tr> </table> <a href="form.jsp">返回重新填写</a> </body> </html>代码的逻辑很直观,但有两处需要特别提示一下。第一,request.setCharacterEncoding("UTF-8")必须在获取任何参数之前执行,放到页面的最前面。如果页面已经调用了getParameter取过值,再调用这个方法就来不及了。第二,复选框的hobby参数可能为null(用户一个都没选),所以在拼字符串时做了判空处理,否则Arrays.toString(null)会直接抛NullPointerException。
把这两个页面部署到Tomcat的webapps目录下,启动服务器,浏览器访问http://localhost:8080/你的项目名/form.jsp,填写信息后提交,就能在show.jsp看到完整的回显内容。这个页面虽然简单,但已经把request对象最常用的API都走了一遍,初学者在做课程设计时照搬这套模式就能应付大部分表单类需求。
3.4 业务扩展:登录判定与页面间数据传递
上面的表单演示只是"取数据",更接近真实项目的是"存数据再传递"。这里扩展一个场景:用户登录成功后,把用户名和登录时间放进request里,转发到欢迎页展示。
login.jsp模拟一个极简登录表单:
<%@ page contentType="text/html;charset=UTF-8" language="java" %> <html> <head> <title>登录</title> <meta charset="UTF-8"> </head> <body> <form action="login_check.jsp" method="post"> 用户名:<input type="text" name="username"/><br/><br/> 密码:<input type="password" name="password"/><br/><br/> <input type="submit" value="登录"/> </form> </body> </html>login_check.jsp做校验和数据传递:
<%@ page contentType="text/html;charset=UTF-8" language="java" %> <% request.setCharacterEncoding("UTF-8"); String username = request.getParameter("username"); String password = request.getParameter("password"); // 模拟校验:用户名和密码不为空就算登录成功 if (username != null && !username.isEmpty() && password != null && !password.isEmpty()) { // 把数据放入request作用域 request.setAttribute("username", username); request.setAttribute("loginTime", new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new java.util.Date())); // 请求转发到欢迎页 request.getRequestDispatcher("welcome.jsp").forward(request, response); } else { out.print("<script>alert('用户名或密码不能为空');location.href='login.jsp';</script>"); } %>welcome.jsp取出数据:
<%@ page contentType="text/html;charset=UTF-8" language="java" %> <% String username = (String) request.getAttribute("username"); String loginTime = (String) request.getAttribute("loginTime"); %> <html> <body> <h2>欢迎,<%= (username != null ? username : "未知用户") %></h2> <p>登录时间:<%= (loginTime != null ? loginTime : "无") %></p> </body> </html>这个过程体现的就是JSP里非常标准的"请求转发+request域传值"模式。因为用的是forward而不是sendRedirect,welcome.jsp和login_check.jsp之间共享同一个request对象,所以setAttribute进去的数据在welcome.jsp里能原样取出来。等以后学到MVC或者Servlet做控制器时,你会发现这套玩法其实就是Spring MVC里Model传值的底层原型。
4. 乱码、空指针与请求响应的边界:request使用问题排查实录
4.1 中文乱码:POST和GET的处理方式不一样
中文乱码是初学者用request时遇到最多的坑,没有之一。要理解乱码的根源,得先明白编码和解码的过程:浏览器在表单页使用UTF-8编码把中文参数发给服务器,服务器端默认却用ISO-8859-1来解码,两边的码表不一致,数据自然就变成乱码了。
POST请求的处理方式最简单,在取任何参数之前加一行:
<% request.setCharacterEncoding("UTF-8"); %>这行代码的作用是告诉服务器,本次请求的body内容使用UTF-8进行解码。注意它只对POST请求体里带的参数生效,而且必须在首次调用getParameter之前执行,晚了就无效。
GET请求的乱码是另一条路:GET参数放在URL上,URL的编码行为由服务器端的连接器配置决定。Tomcat 8及以上版本默认就将URI解码设置为UTF-8,所以多数情况下GET中文不会出问题。但如果你用老版本Tomcat(7及以下),就需要在conf/server.xml里的Connector节点加一个属性URIEncoding="UTF-8"。如果项目里既有GET也有POST,而且Tomcat版本不统一,稳妥的做法是在代码里对GET参数手动做一次转码:
<% String name = request.getParameter("name"); if (name != null) { name = new String(name.getBytes("ISO-8859-1"), "UTF-8"); } %>这是一种"以毒攻毒"的做法,先把拿到的参数按服务器的错误解码结果还原成字节数组,再用UTF-8重新解码。能解决部分老环境下的GET乱码,但有局限性,新项目不建议主动使用,了解原理即可。
最简单的排查思路是:先确认页面ContentType里是否设置了UTF-8,再确认Tomcat版本,最后看请求方式是POST还是GET。90%的乱码问题都出在前两环节,挨个排查就好。
4.2 参数为null导致的空指针问题
新手在展示页里写<%= name %>没报错,但一旦代码变成if (name.equals("张三"))就会抛NullPointerException。常见场景是:你从页面A跳到页面B,但B里用了页面A才有的参数名;或者复选框一个都没勾,取出来的数组就是null。
规避空指针的核心习惯有两个:
第一,取出来的参数先用"是否为null"做一次判断,再进入业务逻辑。判断空和判断空字符串还不一样,表单里用户什么都没输入,提交上来的是空串"";但字段根本没传,才是null。很多业务逻辑要求"既不为null也不为空串",写法就是if (param == null || param.isEmpty())。
第二,做类型转换时要加try-catch或者用工具方法。比如年龄字段用户手滑输入了"abc",Integer.parseInt会直接抛NumberFormatException。比较稳妥的写法是:
<% String ageStr = request.getParameter("age"); int age = 0; if (ageStr != null && !ageStr.isEmpty()) { try { age = Integer.parseInt(ageStr); } catch (NumberFormatException e) { age = 0; } } %>这种"防御式取参"的写法确实啰嗦,但它在生产环境的可靠性很高。等你后面用框架时,框架自带的参数绑定和类型转换会帮你省掉大部分模板代码,但底层原理仍然是这一套。
4.3 请求转发vs重定向:最容易选错的跳转方式
很多初学者把request.getRequestDispatcher("xxx").forward(request, response)和response.sendRedirect("xxx")混用在同一个项目里,结果要么数据取不到,要么URL莫名其妙变来变去。这两者的核心区别,用一句话概括:forward一次请求,redirect两次请求。
forward是服务器内部转发,浏览器地址栏不变,整个过程共享同一个request和response对象,所以在forward之后的页面里能从request里getAttribute到数据。redirect是服务器告诉浏览器"你去另一个地址找我",地址栏会变化,浏览器会重新发起一次全新的请求,原来的request对象已经销毁,用setAttribute放进去的数据自然取不到。
实际场景里,往一个正在编辑的页面提交数据后,为了防止用户刷新页面导致重复提交,通常用redirect做"提交后跳转",地址栏变成结果页URL,刷新不会重复提交。而在MVC里Servlet处理完业务,需要把数据交给JSP渲染,一定用forward。
还有一种坑是forward的路径问题。getRequestDispatcher的路径分为两种:以/开头的是相对当前Web应用的根路径,不加/则是相对当前请求路径的路径。同一个项目里稍微一混,就会出现404。建议统一使用以/开头的写法,比如request.getRequestDispatcher("/welcome.jsp"),这样路径的起点就是项目根目录,不容易错。
4.4 request对象的生命周期与内存注意点
request对象的生命周期很短,一次请求开始就创建,请求结束就被回收。这意味着你不能在request里存太多数据,更不能把request存到session或者application里长期保存。一些新手为了图省事,把大对象(比如一个列表查询结果)直接setAttribute进去再forward,会造成不必要的内存占用,尤其在并发量高的时候压力明显。
正确的做法是:request域适合存一次请求内需要传递的轻量数据,比如表单校验结果、查询条件、用户ID等。全局共享的数据放application,跨请求但同会话的数据放session。三者各有明确的分工,按数据的使用范围选对存储位置,是Java Web开发的常识,也是面试常问的基础问题。
4.5 其他容易被忽略的小细节
request.getParameter取到的值,HTML标签里的特殊字符会原样输出,存在XSS风险。在JSP页面输出用户提交的数据时,最好用<c:out>标签或者手动对<、>、&等字符做转义,不然用户提交一段<script>代码,别人的浏览器就会中招。
用户快速双击提交按钮可能导致同一请求发送两次,页面上的处理逻辑要么做好幂等(重复执行结果相同),要么用JavaScript在提交后禁用按钮。这个问题和request本身没直接关系,但排查"为什么数据插了两条"时经常发现是这里的问题。
最后提一个调试小技巧:临时在JSP页面里写一段遍历getParameterMap()的循环,把请求里的所有参数名和值打出来,定位参数丢失问题比挨个getParameter快得多。排查HTTP头信息就遍历getHeaderNames(),看User-Agent、Referer、Cookie到底有没有传过来。掌握这两个调试手段,很多"页面传参问题"的排查效率能提升一大截。
我在最初上手JSP的时候,对request也是一知半解,总觉得"反正能拿到参数就行"。直到有一次被一个诡异的中文乱码问题折磨了大半天,才老老实实地把HttpServletRequest的API文档从头翻了一遍,又把netstat抓包和浏览器开发者工具里的请求报文对照着看了一场,才算真正把这套机制吃透。对这个曾经最不起眼的内置对象,我的体会是:它没有框架里那些高级组件那样光鲜,却是整个Java Web请求链路的起点。你把request的取参、传值、转发和编码这四件事练扎实了,后面学Servlet、Filter、Spring MVC时到处都能看到它的影子。如果这篇内容对你有一点点帮助,建议你把文中的案例手敲一遍,跑通了再往真实项目里延伸——亲自踩过坑和只在文章里看懂,完全是两种体验。