WD
Classnote Docs课程课件
06

06 Request & Response

学习目标:

  • 理解 Request、Response 与 HTTP 请求报文、响应报文之间的对应关系
  • 掌握从 Request 中获取请求行、请求头、请求参数、请求体和客户端信息的方法
  • 掌握使用 Response 返回文本、HTML、JSON 和文件内容的基础方式
  • 能区分请求转发与重定向,并理解它们各自的适用场景
  • 能处理 POST 中文乱码、文件上传、响应头设置等高频问题
  • 掌握使用 BeanUtils 完成参数封装的方法,并理解反射在通用请求分发中的作用

本章重点:

  • HTTP 报文与 Request / Response API 的映射关系
  • getParameter()getReader()getInputStream() 等 Request 核心方法
  • setContentType()getWriter()getOutputStream() 与关键响应头
  • 转发、重定向、文件上传与文件下载的常见写法
  • BeanUtils.populate() 的参数封装用法与反射驱动的通用分发思路
01 / Section

1. 前置知识准备

  • 已理解 Servlet 的基本开发方式
  • 已知道浏览器访问 Tomcat 时会发起 HTTP 请求
  • 已具备表单提交、URL 参数的基础认知
  • 已掌握 Java 基础语法、面向对象和方法调用

在进入 Request / Response 之前,先把两个容易混淆的概念对齐:

1.1 URL 与 URI

概念 全称 示例 你该怎么理解
URL Uniform Resource Locator http://localhost:8080/demo/hello 完整访问地址
URI Uniform Resource Identifier /demo/hello 资源标识路径

可以先用一句话记忆:

URL 更完整,URI 更聚焦路径本身。

1.2 HTTP 报文的基本结构

请求报文可以简化成:

text
请求行
请求头
空行
请求体

响应报文可以简化成:

text
响应行
响应头
空行
响应体

后面学习 Request 和 Response,本质上就是把这四块内容映射到 Java API 上。

1.3 学这章时要一直抓住的一件事

Request 负责“拿输入”,Response 负责“做输出”。

如果你在学习过程中搞不清某个方法的用途,就回到这句判断:

  • 这是为了从客户端拿数据吗?那大概率属于 Request
  • 这是为了把结果发回浏览器吗?那大概率属于 Response

本章小结

本章核心概念:学习 Request / Response 前,必须先把 URL / URI 和 HTTP 报文结构分清。

你现在应该掌握

  • URL 和 URI 的差别
  • 请求报文与响应报文各自由哪些部分组成
  • Request 偏输入,Response 偏输出
02 / Section

2. HTTP 报文与 Request / Response 的对应关系

2.1 请求报文示例

http
POST /admin/auth/login HTTP/1.1
Host: 39.101.189.16:8083
Connection: keep-alive
Content-Length: 45
Accept: application/json, text/plain, */*
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36
Content-Type: application/json;charset=UTF-8

{"username":"admin123","password":"admin123"}

可以拆成四部分:

部分 示例 对应关注点
请求行 POST /admin/auth/login HTTP/1.1 方法、路径、协议
请求头 HostContent-TypeUser-Agent 元信息
空行 分隔头和体 没有业务含义
请求体 JSON / 表单数据 / 文件数据 真正提交的数据

2.2 响应报文示例

http
HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
Set-Cookie: JSESSIONID=24287278-5ebb-407d-a3f7-56b74782c4c7; Path=/; HttpOnly
Content-Length: 200

{"errno":0,"data":{"nickName":"admin123"},"errmsg":"成功"}

同样可以拆成四部分:

部分 示例 对应关注点
响应行 HTTP/1.1 200 OK 协议、状态码
响应头 Content-TypeSet-Cookie 浏览器如何解释响应
空行 分隔头和体 没有业务含义
响应体 HTML / JSON / 图片 / 文件字节流 真正返回给客户端的内容

2.3 Request 与 Response 到底是什么

可以把它们理解成:

对象 本质 作用
HttpServletRequest 服务器对请求报文的封装 读取客户端传来的信息
HttpServletResponse 服务器对响应报文的封装 组织并返回响应内容

在 Servlet 中最常见的入口就是:

java
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
    // req 负责拿请求信息
    // resp 负责写响应结果
}

如果你对“浏览器、Tomcat、Servlet、Request、Response”这几层关系还没完全建立起来,可以先看下面这张总览图:

浏览器到 Tomcat 再到 Servlet 中 Request / Response 封装关系图
浏览器到 Tomcat 再到 Servlet 中 Request / Response 封装关系图

2.4 为什么这一层映射很重要

后面你看到任意一个 API,都可以回到报文层去理解:

  • getMethod() 对应请求行里的方法
  • getHeader() 对应请求头
  • getParameter() 对应参数区
  • setStatus() 对应响应行里的状态码
  • setHeader() 对应响应头
  • getWriter() / getOutputStream() 对应响应体输出

一旦你把“API 背后是哪一块 HTTP 报文”建立起关联,这章内容会顺很多。

本章小结

本章核心概念:Request / Response 不是凭空出现的对象,而是对 HTTP 报文的 Java 封装。

你现在应该掌握

  • 请求报文和响应报文的基本结构
  • Request 对应请求报文,Response 对应响应报文
  • 常见 API 应该映射回哪一块报文内容去理解
03 / Section

3. Request:从客户端拿到什么

3.1 请求行信息获取

请求行最常见的是:

http
POST /demo/login?username=zhangsan HTTP/1.1

对应 API:

信息 方法 说明
请求方法 getMethod() GET / POST / PUT / DELETE
完整 URL getRequestURL() 包含协议、主机、端口、路径
URI getRequestURI() 只保留资源路径
上下文路径 getContextPath() 应用根路径
查询字符串 getQueryString() ? 后面的内容
协议 getProtocol() 例如 HTTP/1.1

示例:

java
@WebServlet("/line")
public class LineServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException {
        System.out.println(req.getMethod());
        System.out.println(req.getRequestURL());
        System.out.println(req.getRequestURI());
        System.out.println(req.getContextPath());
        System.out.println(req.getQueryString());
        System.out.println(req.getProtocol());
    }
}

3.2 请求头信息获取

请求头常见内容:

http
Host: localhost:8080
User-Agent: Mozilla/5.0
Accept: application/json, text/plain, */*
Content-Type: application/json;charset=UTF-8

最常用的方法有两个:

方法 说明
getHeader(String name) 获取指定请求头
getHeaderNames() 获取全部请求头名称

示例:

java
Enumeration<String> headerNames = req.getHeaderNames();
while (headerNames.hasMoreElements()) {
    String name = headerNames.nextElement();
    System.out.println(name + ": " + req.getHeader(name));
}

String host = req.getHeader("Host");
String userAgent = req.getHeader("User-Agent");

请求头名称大小写不敏感,所以 Hosthost 都能取到值。

3.3 客户端与服务器信息

除了请求行和请求头,Request 还能让你知道“这次请求是谁发来的、发到哪里”。

信息 方法
服务器 IP getLocalAddr()
服务器端口 getLocalPort()
客户端 IP getRemoteAddr()
客户端端口 getRemotePort()

示例:

java
System.out.println("客户端: " + req.getRemoteAddr() + ":" + req.getRemotePort());
System.out.println("服务器: " + req.getLocalAddr() + ":" + req.getLocalPort());

3.4 请求转发(了解)

请求转发是服务器内部把请求交给另一个资源继续处理:

java
req.getRequestDispatcher("/target.jsp").forward(req, resp);

特点:

  • 浏览器地址栏不变
  • 还是同一次请求
  • 可以共享同一个 Request 中的数据

例如:

java
req.setAttribute("user", user);
req.getRequestDispatcher("/success.jsp").forward(req, resp);

目标资源里仍然可以取到 user

3.5 请求体读取:getReader()getInputStream()

当你处理 JSON 或二进制数据时,参数不一定适合直接用 getParameter()

这时要读请求体:

方法 适合场景
getReader() 文本数据,如 JSON、XML
getInputStream() 二进制数据

文本读取示例:

java
BufferedReader reader = req.getReader();
String line;
StringBuilder sb = new StringBuilder();
while ((line = reader.readLine()) != null) {
    sb.append(line);
}
System.out.println(sb.toString());

字节流示例:

java
ServletInputStream inputStream = req.getInputStream();
byte[] buffer = new byte[1024];
int len;
while ((len = inputStream.read(buffer)) != -1) {
    System.out.write(buffer, 0, len);
}

例如当前端按 raw + JSON 提交请求体时,请求数据就不再适合优先用 getParameter(),而更适合用 getReader() 去读整段文本:

Postman 中 raw JSON 请求体示意
Postman 中 raw JSON 请求体示意

注意:

同一个请求里,字符流和字节流不能混用。

本章小结

本章核心概念:Request 不只是“拿参数”,它还能拿到请求行、请求头、来源信息、转发上下文和原始请求体。

你现在应该掌握

  • 如何读取请求方法、URL、URI、上下文路径
  • 如何读取请求头和来源地址
  • 请求转发为什么能共享同一个 Request 对象
  • 什么时候该用 getReader() / getInputStream() 直接读请求体
04 / Section

4. Request:参数与文件上传

4.1 请求参数获取

请求参数最常见的来源有两个:

  • GET:出现在 URL 查询字符串里
  • POST:出现在请求体里

对应最常用的四个方法:

方法 返回值 用途
getParameter(String) String 取单个参数
getParameterValues(String) String[] 取同名多值参数
getParameterNames() Enumeration<String> 枚举全部参数名
getParameterMap() Map<String, String[]> 拿到全部参数映射

示例:

java
String username = req.getParameter("username");
String[] hobbies = req.getParameterValues("hobby");
Map<String, String[]> paramMap = req.getParameterMap();

这四个方法最容易混在一起,建议直接结合下面这张图理解它们各自“拿的是哪一层数据”:

getParameter、getParameterValues、getParameterNames 与 getParameterMap 的关系图
getParameter、getParameterValues、getParameterNames 与 getParameterMap 的关系图

4.2 多值参数为什么要用 getParameterValues()

例如多选框:

text
hobby=sing&hobby=dance

在底层会被封装成:

text
{
  "hobby": ["sing", "dance"]
}

所以:

  • 单值参数可以用 getParameter()
  • 多值参数必须考虑 getParameterValues()

4.3 POST 中文乱码问题

这是课堂里最常见的坑之一。

解决方式:

java
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
        throws ServletException, IOException {
    req.setCharacterEncoding("UTF-8");
    String username = req.getParameter("username");
}

关键点只有一句话:

setCharacterEncoding("UTF-8") 必须写在任何 getParameter() 之前。

4.4 参数封装为对象

如果参数一多,手动 setXxx() 会很啰嗦:

java
User user = new User();
user.setUsername(req.getParameter("username"));
user.setPassword(req.getParameter("password"));

更常见的做法是直接使用 BeanUtils 这类工具类。

如果是 Maven 项目,先引入依赖:

xml
<dependency>
    <groupId>commons-beanutils</groupId>
    <artifactId>commons-beanutils</artifactId>
    <version>1.9.4</version>
</dependency>

如果不是 Maven 工程,也要先把对应的 commons-beanutils 相关 jar 包导入项目。

示例:

java
import org.apache.commons.beanutils.BeanUtils;

User user = new User();
BeanUtils.populate(user, req.getParameterMap());

这样就能把参数名和 JavaBean 属性名对应起来,自动完成封装。

注意两点:

  • 表单参数名要和对象属性名保持一致
  • BeanUtils.populate() 底层依赖反射,但课堂里优先掌握工具类的使用方式即可

4.5 文件上传处理

文件上传和普通表单的区别在于:表单必须声明 multipart/form-data

前端示例:

html
<form action="/demo/upload" method="post" enctype="multipart/form-data">
    用户名:<input type="text" name="username"><br>
    头像:<input type="file" name="avatar"><br>
    <input type="submit" value="上传">
</form>

浏览器侧看到的就是一个普通表单加文件选择框,但一旦有文件字段,提交方式就必须切到 multipart/form-data

浏览器中的文件上传表单示意
浏览器中的文件上传表单示意

Servlet 端要点:

  • 使用 @MultipartConfig
  • getPart() 获取上传文件
java
@MultipartConfig
@WebServlet("/upload")
public class UploadServlet extends HttpServlet {
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException {
        String username = req.getParameter("username");
        Part avatarPart = req.getPart("avatar");

        String fileName = avatarPart.getSubmittedFileName();
        long size = avatarPart.getSize();
        String contentType = avatarPart.getContentType();

        String savePath = getServletContext().getRealPath("/uploads");
        avatarPart.write(savePath + File.separator + fileName);
    }
}

常用 Part 方法:

方法 作用
getInputStream() 读取文件内容
getSubmittedFileName() 获取原始文件名
getContentType() 获取 MIME 类型
getSize() 获取文件大小
write(String path) 保存文件

如果你是用接口工具调试文件上传,请重点观察 form-data 和文件字段类型,而不是误选成 rawx-www-form-urlencoded

Postman 中 form-data 文件上传示意
Postman 中 form-data 文件上传示意

进一步往底层看,multipart/form-data 请求体其实会把普通字段和文件字段分段封装。下面这张图很适合解释为什么文件上传不能按普通字符串参数去理解:

multipart/form-data 请求体中普通字段与文件字段的分段结构示意
multipart/form-data 请求体中普通字段与文件字段的分段结构示意

本章小结

本章核心概念:当请求参数越来越复杂时,要分清普通参数封装与文件上传这两条处理路径。

你现在应该掌握

  • 如何把请求参数更高效地封装为对象
  • 为什么 POST 中文乱码要先设置编码
  • 文件上传为什么必须用 multipart/form-data@MultipartConfig
05 / Section

5. Response:向客户端输出什么

5.1 响应行:状态码设置

Response 最基础的能力之一,就是设置状态码:

java
resp.setStatus(200);
resp.setStatus(302);
resp.setStatus(404);
resp.setStatus(500);

常见状态码:

状态码 含义 场景
200 OK 成功返回
302 Found 重定向
404 Not Found 资源不存在
500 Internal Server Error 服务器异常

5.2 响应头:告诉浏览器如何处理结果

通用写法:

java
resp.setHeader("key", "value");

你可以把响应头理解成:

返回正文之前,先告诉浏览器“这份内容是什么、该怎么处理”。

5.3 响应体输出:字符流 vs 字节流

最常用的两个方法:

方法 适合场景
getWriter() 文本、HTML、JSON
getOutputStream() 图片、文件、二进制数据

文本输出:

java
resp.setContentType("text/html;charset=UTF-8");
PrintWriter writer = resp.getWriter();
writer.write("<h1>Hello</h1>");
writer.write("<p>你好</p>");

字节输出:

java
resp.setContentType("image/jpeg");
InputStream is = getServletContext().getResourceAsStream("/images/photo.jpg");
ServletOutputStream os = resp.getOutputStream();

byte[] buffer = new byte[1024];
int len;
while ((len = is.read(buffer)) != -1) {
    os.write(buffer, 0, len);
}

注意:

同一个响应中,字符流和字节流也不能混用。

5.4 Content-Type:最重要的响应头之一

java
resp.setContentType("application/json;charset=UTF-8");
resp.getWriter().write("{\"code\":200}");

常见取值:

内容 Content-Type
HTML text/html;charset=UTF-8
纯文本 text/plain;charset=UTF-8
JSON application/json;charset=UTF-8
JPEG 图片 image/jpeg
PNG 图片 image/png
PDF application/pdf

它既决定浏览器如何解释响应内容,也常常顺手解决中文乱码问题。

5.5 Content-Disposition:文件下载

如果你希望浏览器把内容当“附件下载”,常用写法是:

java
resp.setHeader("Content-Disposition", "attachment;filename=report.pdf");
resp.setContentType("application/pdf");

再配合字节流把文件写出去即可。

5.6 Location 与重定向

最底层的重定向可以这么写:

java
resp.setStatus(302);
resp.setHeader("Location", "https://www.example.com");

更常用的是:

java
resp.sendRedirect("https://www.example.com");

5.7 Refresh:定时刷新或定时跳转

java
resp.setHeader("Refresh", "1");
resp.setHeader("Refresh", "3;url=/login.html");

这类写法现在不算主流核心能力,但课堂里经常作为了解性案例出现,知道它是响应头控制即可。

本章小结

本章核心概念:Response 的核心职责是“组织响应行、响应头和响应体”,而不是简单地“打印字符串”。

你现在应该掌握

  • 何时改状态码,何时改响应头
  • 何时用 getWriter(),何时用 getOutputStream()
  • Content-TypeContent-DispositionLocation 的典型用途
06 / Section

6. 反射与通用分发思路

6.1 为什么这一章会补反射

Request / Response 这章之所以常常顺带讲反射,是因为真实开发里很容易遇到这样一个问题:

一个 Servlet 里如果要分发多个业务方法,手写 if...else 很笨重。

反射恰好能帮助我们提升“通用分发”的能力。

6.2 获得 Class 对象的三种方式

java
Class<UserServiceImpl> c1 = UserServiceImpl.class;

UserServiceImpl service = new UserServiceImpl();
Class<? extends UserServiceImpl> c2 = service.getClass();

Class<?> c3 = Class.forName("com.cskaoyan.service.UserServiceImpl");

在“通用性开发”里,最常用的是:

java
Class.forName(...)

因为它适合从配置或字符串动态加载类。

6.3 通过反射调用字段和方法

字段示例:

java
Field field = clazz.getDeclaredField("username");
field.setAccessible(true);
field.set(instance, "zhangsan");

方法示例:

java
Method method = clazz.getDeclaredMethod("login", String.class);
method.setAccessible(true);
Object result = method.invoke(instance, "admin");

6.4 一个典型应用:通用请求分发器

比如请求:

text
/user/login
/user/register
/user/delete

你可以取 URI 最后一段,再通过反射去调用同名方法:

java
String uri = req.getRequestURI();
String methodName = uri.substring(uri.lastIndexOf("/") + 1);
Method method = this.getClass().getDeclaredMethod(methodName, HttpServletRequest.class, HttpServletResponse.class);
method.invoke(this, req, resp);

这就是很多“简化版 MVC 分发器”的入门思路。

本章小结

本章核心概念:反射不是本章主角,但它能帮助我们把请求分发做得更通用。

你现在应该掌握

  • 为什么这章要补反射
  • 反射如何服务于通用分发思路
07 / Section

7. 常见问题与易混点

7.1 getRequestURL()getRequestURI() 容易混

记忆方式:

  • getRequestURL() 更完整
  • getRequestURI() 更聚焦路径

7.2 getParameter() 和读请求体容易混

判断原则:

  • 普通表单参数优先想 getParameter()
  • JSON / XML / 原始文本内容优先想 getReader()
  • 二进制内容优先想 getInputStream()

7.3 转发和重定向容易混

特性 转发 重定向
URL 是否变化 不变 改变
请求次数 1 次 2 次
是否共享 Request 共享 不共享
典型用途 服务器内部跳转 页面跳转、外部地址跳转

7.4 字符流和字节流容易混

在 Request 和 Response 两边,都有同一个规则:

字符流和字节流二选一,不要混用。

7.5 文件上传和普通表单容易混

文件上传要特别记住三件事:

  • 表单必须是 multipart/form-data
  • Servlet 端通常要加 @MultipartConfig
  • 文件上传读的是 Part,不是普通字符串参数

本章小结

本章核心概念:这章的很多错误不是“不会写代码”,而是把几组相似概念混在了一起。

你现在应该掌握

  • 如何快速区分 URL / URI、转发 / 重定向、参数 / 请求体
  • 为什么流不能混用
  • 为什么文件上传不是普通表单处理
08 / Section

8. 本章总复盘

8.1 一条完整主线

把本章压缩成一条链路,就是:

text
浏览器发起 HTTP 请求
    ->
Tomcat 封装为 HttpServletRequest / HttpServletResponse
    ->
Servlet 从 Request 中读取方法、头、参数、请求体
    ->
业务逻辑处理
    ->
Servlet 通过 Response 设置状态码、响应头、响应体
    ->
浏览器按响应头解释并展示结果

8.2 你现在最该记住的五件事

  1. Request 对应请求报文,Response 对应响应报文
  2. Request 偏输入,Response 偏输出
  3. 参数区、请求体、文件上传是三种不同输入来源
  4. Content-TypeLocationContent-Disposition 是高频响应头
  5. BeanUtils 能简化参数封装,反射让通用分发更有扩展性

8.3 学完这一章后,下一章会自然衔接什么

学完 Request / Response 后,下一步最自然的方向是:

  • 如何记住用户状态
  • 为什么 HTTP 本身是无状态的
  • Cookie 和 Session 如何参与会话管理

这正是下一章“会话技术”要继续展开的内容。

09 / Section

9. 实战练习

<!-- 实战练习内容已分离到 practices/markdown/06-request-response-practice.md -->

建议先复习本章重点:请求行 / 请求头 / 请求参数 / 请求体的读取方式,以及 Content-Type、重定向、文件下载这些 Response 输出能力,再进入练习。 练习建议按“获取请求信息 -> 参数处理 -> JSON 响应 -> 文件上传下载 -> 通用分发器”这个顺序完成。 配套练习文件:content/optimized/practices/markdown/06-request-response-practice.md