一、概述
1、基本原理
在使用 Servlet 开发 Java Web 应用程序时必不可少地要使用到其中的ServletRequest
和ServletResponse
,这两个对象都是通过方法参数的形式被我们使用的。那么这两个对象是如何产生的?又是如何被我们使用的?下面通过一张图来进行说明:
下面对上图中的内容进行解释:
- 首先 Tomcat 服务器会根据请求 URL 中的资源路径创建对应的 Servlet 对象
- 接下来 Tomcat 服务器会创建
ServletRequest
和ServletResponse
对象(ServletRequest
和ServletResponse
是接口,因此实际上 Tomcat 创建的是这两个接口的实现类,这两个实现类由 Tomcat 提供,开发人员无需关心),ServletRequest
对象中封装请求消息数据
- 之后 Tomcat 将
ServletRequest
和ServletResponse
对象传递给 service 方法,并调用 service 方法
- 开发人员可以通过
ServletRequest
获取请求消息数据,通过ServletResponse
对象设置响应消息数据
- 服务器在对浏览器做出响应之前会从
ServletResponse
对象中获取的响应消息数据
2、作用
ServletRequest
和ServletResponse
对象均由服务器创建,我们使用ServletRequest
用来获取请求消息,使用ServletResponse
来设置响应消息。
二、ServletRequest
1、体系结构
下面通过一张图来说明ServletRequest
的体系结构:
其中org.apache.catalina.connector.RequestFacade
为 Tomcat 中的实现类。
2、获取请求数据
(1)获取请求行
下面给出一些常用的获取请求行内容的方法:
方法 & 返回值 |
功能 |
String getMethod() |
获取请求方式 |
String getContextPath() |
获取虚拟目录 |
String getServletPath() |
获取 Servlet 的路径 |
String getQueryString() |
获取路径请求参数 |
String getRequestURI() |
获取请求 URI(统一资源标志符) |
StringBuffer getRequestURL() |
获取请求 URL(统一资源定位符) |
String getProtocol() |
获取协议及版本 |
String getRemoteAddr() |
获取客户机的IP地址 |
假设某一次请求的请求头为GET /project/demo1?name=frank&age=21 HTTP/1.1
,Servlet 的代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String method = request.getMethod(); System.out.println(method); String contextPath = request.getContextPath(); System.out.println(contextPath); String servletPath = request.getServletPath(); System.out.println(servletPath); String queryString = request.getQueryString(); System.out.println(queryString); String requestURI = request.getRequestURI(); System.out.println(requestURI); StringBuffer requestURL = request.getRequestURL(); System.out.println(requestURL); String protocol = request.getProtocol(); System.out.println(protocol); String remoteAddr = request.getRemoteAddr(); System.out.println(remoteAddr); }
|
启动服务器,假设服务器运行在本机的 8080 端口,那么通过本机的浏览器访问上面的 Servlet,控制台输出的结果如下:
1 2 3 4 5 6 7 8
| GET /project /demo1 name=frank&age=21 /project/demo1 http://localhost:8080/project/demo1 HTTP/1.1 0:0:0:0:0:0:0:1
|
(2)获取请求头
HttpServletRequest
接口定义了以下三个方法来获取请求头:
方法 & 返回值 |
功能 |
String getHeader(String name) |
以字符串形式返回指定请求头的值 |
Enumeration<String> getHeaderNames() |
返回此请求包含的所有头名称的枚举 |
Enumeration<String> getHeaders(String name) |
以 String 类型的枚举形式返回指定请求头的所有值 |
下面通过一个例子来说明如何使用上面介绍的方法来获取请求头:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { Enumeration<String> headerNames = request.getHeaderNames(); while (headerNames.hasMoreElements()) { String name = headerNames.nextElement(); String value = request.getHeader(name); System.out.println(name + ": " + value); } }
|
假设某一次HTTP请求的报文内容如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| GET /project/demo1?name=frank&age=21 HTTP/1.1 host: localhost:8080 connection: keep-alive cache-control: max-age=0 upgrade-insecure-requests: 1 user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.190 Safari/537.36 accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9 sec-fetch-site: none sec-fetch-mode: navigate sec-fetch-user: ?1 sec-fetch-dest: document accept-encoding: gzip, deflate, br accept-language: zh-CN,zh;q=0.9
|
那么控制台输出的结果如下:
1 2 3 4 5 6 7 8 9 10 11 12
| host: localhost:8080 connection: keep-alive cache-control: max-age=0 upgrade-insecure-requests: 1 user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.190 Safari/537.36 accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9 sec-fetch-site: none sec-fetch-mode: navigate sec-fetch-user: ?1 sec-fetch-dest: document accept-encoding: gzip, deflate, br accept-language: zh-CN,zh;q=0.9
|
(3)获取请求体
首先需要注意的是只有POST和PUT方式才有请求体,请求体中封装了POST或PUT请求的请求参数,下面介绍一下获取请求体的步骤:
第 1 步:获取流对象
在ServletRequest
接口中提供如下方法来获取流对象:
方法 & 返回值 |
功能 |
ServletInputStream getInputStream() |
获取字节输入流,可以操作所有类型数据 |
BufferedReader getReader() |
获取字符输入流,只能操作字符类型 |
当请求体中的数据为纯文本时需要获取字符输入流,但如果请求体中的数据为二进制数据(如上传图片时)则需要获取字节输入流。
第 2 步:从流对象中获取数据
1 2 3 4 5 6 7 8 9 10
| @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { BufferedReader br = request.getReader(); String line = null; while ((line = br.readLine()) != null) { System.out.println(line); } }
|
启动服务器,假设在表单中输入name=frank
和age=21
,则控制台输出的结果如下:
3、获取请求参数的通用方法
在ServletRequest
接口中定义了一些方法用来获取请求参数,与前面介绍的获取请求参数的方法所不同的是,这些方法都是通用的,也就是无论请求参数是在请求路径上还是请求体中都可以获取到。下面给出该接口中定义的用来获取请求参数的通用方法:
方法 & 返回值 |
功能 |
String getParameter(String name) |
根据参数名称获取参数值,如果不存在则返回null |
Map<String,String[]> getParameterMap() |
获取所有参数的 Map 集合 |
Enumeration<String> getParameterNames() |
以 String 类型的枚举形式获取所有请求的参数名称 |
String[] getParameterValues(String name) |
根据参数名称获取参数值的数组,如果不存在则返回null |
假设这里有一个 Servlet,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| public class RequestDemo extends HttpServlet { @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { this.doPost(request, response); }
@Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String username = request.getParameter("username"); System.out.println(username);
String[] hobbies = request.getParameterValues("hobby"); for (String hobby : hobbies) { System.out.println(hobby); }
Enumeration<String> parameterNames = request.getParameterNames(); while (parameterNames.hasMoreElements()) { String name = parameterNames.nextElement(); String value = request.getParameter(name); System.out.println(name + ": " + value); }
Map<String, String[]> parameterMap = request.getParameterMap(); for (String name : parameterMap.keySet()) { String[] values = parameterMap.get(name); System.out.println(name); for (String value : values) { System.out.println(value); } } } }
|
仔细观察上面的代码,可以发现上面的代码并没有针对请求方法的不同(如GET
请求方式和POST
请求方式)做不同的调整,也就是上面的代码是通用的,无论是GET
请求方式还是POST
请求方式都可以从HttpServletRequest
对象中获取到请求参数。由此可知使用这些通用的方法可以简化代码,因此在一些时候可以使用通用方法来减少代码量。
在获取请求参数时需要解决的一个问题就是中文乱码的问题。对于GET请求方式,在 Tomcat 8+ 版本已将乱码问题解决了;而对于POST请求方式,则需要在获取参数之前设置流的编码request.setCharacterEncoding("utf-8")
来解决中文乱码问题。
4、请求转发
所谓请求转发,就是指一种在服务器内部的资源跳转方式。下面我们来介绍一下请求转发的步骤:
第 1 步:获取请求转发器对象
使用getRequestDispatcher(String path)
方法来获取请求转发器RequestDispatcher
:
1 2 3 4 5
| @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { RequestDispatcher requestDispatcher = request.getRequestDispatcher("/demo2"); }
|
注:这里getRequestDispatcher
方法所填的参数是要跳转的 Servlet 的路径。
第 2 步:由转发器对象进行请求转发
使用forward(ServletRequest request, ServletResponse response)
方法进行请求转发:
1 2 3 4 5 6 7
| @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { RequestDispatcher requestDispatcher = request.getRequestDispatcher("/demo2"); requestDispatcher.forward(request, response); }
|
下面介绍一下请求转发的特点:
- 浏览器地址栏路径不发生变化
- 只能转发到当前服务器内部的资源
- 转发是一次请求,即客户端只需请求一次
5、共享数据
所谓域对象就是指一个有作用范围的对象,可以在范围内共享数据。而ServletRequest
域代表一次请求的范围,一般用于请求转发的多个资源中共享数据。下面介绍一些有关数据共享的方法:
方法 & 返回值 |
功能 |
void setAttribute(String name, Object o) |
存储数据 |
Object getAttribute(String name) |
通过键值对获取 |
void removeAttribute(String name) |
通过键移除键值对 |
接着上面介绍请求转发所用的例子,假设我们需要将 Servlet 路径为/demo1
的请求转发到路径为/demo2
的 Servlet 来处理,并且需要传递一些数据,可以在第一个 Servlet 中采用如下写法:
1 2 3 4 5 6 7
| @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setAttribute("msg", "hello"); request.getRequestDispatcher("/demo2").forward(request, response); }
|
在第二个 Servlet 中采用如下写法:
1 2 3 4 5 6 7
| @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { Object msg = request.getAttribute("msg"); System.out.println(msg); }
|
这样当客户端向第一个 Servlet 发送请求时,该 Servlet 将请求转发到第二个 Servlet 并在HttpServletRequest
对象中添加了共享的数据,第二个 Servlet 接收到请求并获取到共享的数据,最后将共享的数据输出到控制台。
6、获取ServletContext
关于ServletContext
的相关内容已经在之前的一篇文章《Servlet体系结构及ServletContext》中进行了详细介绍,这里仅简单介绍一下获取ServletContext
对象的方法,其余内容不再进行赘述。
方法 & 返回值 |
功能 |
ServletContext getServletContext() |
获取此ServletRequest 上次调度到的 Servlet 上下文。 |
三、ServletResponse
1、体系结构
下面通过一张图来说明ServletResponse
的体系结构:
其中org.apache.catalina.connector.ResponseFacade
为 Tomcat 中的实现类。
2、设置响应数据
(1)设置响应行
在HttpServletResponse
接口中定义了如下方法用于设置响应的状态码:
方法 & 返回值 |
功能 |
void setStatus(int sc) |
设置此响应的状态码。 |
例如需要设置响应的状态码为 302,则可采用如下写法:
1 2 3 4 5
| @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.setStatus(HttpServletResponse.SC_FOUND); }
|
(2)设置响应头
在HttpServletResponse
接口中定义了如下方法用于设置响应头:
方法 & 返回值 |
功能 |
void setHeader(String name, String value) |
用给定的名称和值设置响应头。 |
例如需要设置响应体的类型为text/html
,并采用UTF-8
字符集,则可使用如下写法:
1 2 3 4 5
| @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.setHeader("Content-Type", "text/html;charset=utf-8"); }
|
注:这里响应头的名称是不区分大小写的。
(3)设置响应体
当我们需要设置响应体时,首先需要获取输出流。ServletResponse
接口提供了两个方法用于获取输出流:
方法 & 返回值 |
功能 |
ServletOutputStream getOutputStream() |
返回适合在响应中写入二进制数据的ServletOutputStream 。 |
PrintWriter getWriter() |
返回可以向客户端发送字符文本的PrintWriter 对象。 |
显然,getOutputStream
方法适用于需要写入二进制数据(如:图片、视频等)时,而getWriter
适用于需要写入文本数据(如:HTML页面、JSON 格式的数据等)时。
之后使用输出流写入响应体数据即可。下面将演示如何使用输出流写入响应体数据:
1 2 3 4 5 6 7 8 9 10
| @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.setContentType("text/html;charset=utf-8"); PrintWriter out = response.getWriter(); out.write("<h1>Hello Response!</h1>"); out.write("<h1>你好 Response!</h1>"); }
|
注意这里输出流的刷新和关闭操作由 Web 服务器来完成, 不需要开发人员手动刷新和关闭。
3、重定向
在实际的业务中,经常存在需要重定向的情况。所谓重定向,就是指资源跳转方式,该响应的状态码为 302,响应头为location
,可以使用HttpServletResponse
接口中的sendRedirect
方法进行重定向操作。下面将演示如何进行重定向:
1 2 3 4 5 6 7
| @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String contextPath = request.getContextPath(); response.sendRedirect(contextPath + "/demo2"); }
|
启动服务器,当访问该 Servlet 时,可以观察到进行了重定向操作,下面给出重定向的特点:
- 地址栏发生变化
- 可以访问其它站点(服务器)的资源
- 重定向是两次请求,不能使用
HttpServletRequest
对象来共享数据
要注意重定向和前面介绍的请求转发之间的区别。