一、概述

1、基本原理

在使用 Servlet 开发 Java Web 应用程序时必不可少地要使用到其中的ServletRequestServletResponse,这两个对象都是通过方法参数的形式被我们使用的。那么这两个对象是如何产生的?又是如何被我们使用的?下面通过一张图来进行说明:

1614421553181.png

下面对上图中的内容进行解释:

  • 首先 Tomcat 服务器会根据请求 URL 中的资源路径创建对应的 Servlet 对象
  • 接下来 Tomcat 服务器会创建ServletRequestServletResponse对象(ServletRequestServletResponse是接口,因此实际上 Tomcat 创建的是这两个接口的实现类,这两个实现类由 Tomcat 提供,开发人员无需关心),ServletRequest对象中封装请求消息数据
  • 之后 Tomcat 将ServletRequestServletResponse对象传递给 service 方法,并调用 service 方法
  • 开发人员可以通过ServletRequest获取请求消息数据,通过ServletResponse对象设置响应消息数据
  • 服务器在对浏览器做出响应之前会从ServletResponse对象中获取的响应消息数据

2、作用

ServletRequestServletResponse对象均由服务器创建,我们使用ServletRequest用来获取请求消息,使用ServletResponse来设置响应消息。

二、ServletRequest

1、体系结构

下面通过一张图来说明ServletRequest的体系结构:

1614421825732.png

其中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 {
// 1.获取请求方式
String method = request.getMethod();
System.out.println(method);
// 2.获取虚拟路径
String contextPath = request.getContextPath();
System.out.println(contextPath);
// 3.获取Servlet路径
String servletPath = request.getServletPath();
System.out.println(servletPath);
// 4.获取请求参数
String queryString = request.getQueryString();
System.out.println(queryString);
// 5.获取请求URI
String requestURI = request.getRequestURI();
System.out.println(requestURI);
// 6.获取请求URL
StringBuffer requestURL = request.getRequestURL();
System.out.println(requestURL);
// 7.获取协议及版本
String protocol = request.getProtocol();
System.out.println(protocol);
// 8.获取客户机的IP地址
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 {
// 1.获取所有的请求头名称
Enumeration<String> headerNames = request.getHeaderNames();
// 2.遍历
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 {
// 1.获取字符流
BufferedReader br = request.getReader();
// 2.读取数据
String line = null;
while ((line = br.readLine()) != null) {
System.out.println(line);
}
}

启动服务器,假设在表单中输入name=frankage=21,则控制台输出的结果如下:

1
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集合
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 {
// 1.获取请求转发器对象
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 {
// 1.获取请求转发器对象
RequestDispatcher requestDispatcher = request.getRequestDispatcher("/demo2");
// 2.进行请求转发
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的体系结构:

1614422089612.png

其中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");
// 1.获取字符输出流
PrintWriter out = response.getWriter();
// 2.输出数据
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对象来共享数据

要注意重定向和前面介绍的请求转发之间的区别。