学习目标:
- 理解 Request、Response 与 HTTP 请求报文、响应报文之间的对应关系
- 掌握从 Request 中获取请求行、请求头、请求参数、请求体和客户端信息的方法
- 掌握使用 Response 返回文本、HTML、JSON 和文件内容的基础方式
- 能区分请求转发与重定向,并理解它们各自的适用场景
- 能处理 POST 中文乱码、文件上传、响应头设置等高频问题
- 掌握使用
BeanUtils完成参数封装的方法,并理解反射在通用请求分发中的作用本章重点:
- HTTP 报文与 Request / Response API 的映射关系
getParameter()、getReader()、getInputStream()等 Request 核心方法setContentType()、getWriter()、getOutputStream()与关键响应头- 转发、重定向、文件上传与文件下载的常见写法
BeanUtils.populate()的参数封装用法与反射驱动的通用分发思路
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 报文的基本结构
请求报文可以简化成:
请求行
请求头
空行
请求体响应报文可以简化成:
响应行
响应头
空行
响应体后面学习 Request 和 Response,本质上就是把这四块内容映射到 Java API 上。
1.3 学这章时要一直抓住的一件事
Request 负责“拿输入”,Response 负责“做输出”。
如果你在学习过程中搞不清某个方法的用途,就回到这句判断:
- 这是为了从客户端拿数据吗?那大概率属于 Request
- 这是为了把结果发回浏览器吗?那大概率属于 Response
本章小结
本章核心概念:学习 Request / Response 前,必须先把 URL / URI 和 HTTP 报文结构分清。
你现在应该掌握:
- URL 和 URI 的差别
- 请求报文与响应报文各自由哪些部分组成
- Request 偏输入,Response 偏输出
2. HTTP 报文与 Request / Response 的对应关系
2.1 请求报文示例
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 |
方法、路径、协议 |
| 请求头 | Host、Content-Type、User-Agent |
元信息 |
| 空行 | 分隔头和体 | 没有业务含义 |
| 请求体 | JSON / 表单数据 / 文件数据 | 真正提交的数据 |
2.2 响应报文示例
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-Type、Set-Cookie |
浏览器如何解释响应 |
| 空行 | 分隔头和体 | 没有业务含义 |
| 响应体 | HTML / JSON / 图片 / 文件字节流 | 真正返回给客户端的内容 |
2.3 Request 与 Response 到底是什么
可以把它们理解成:
| 对象 | 本质 | 作用 |
|---|---|---|
HttpServletRequest |
服务器对请求报文的封装 | 读取客户端传来的信息 |
HttpServletResponse |
服务器对响应报文的封装 | 组织并返回响应内容 |
在 Servlet 中最常见的入口就是:
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
// req 负责拿请求信息
// resp 负责写响应结果
}如果你对“浏览器、Tomcat、Servlet、Request、Response”这几层关系还没完全建立起来,可以先看下面这张总览图:

2.4 为什么这一层映射很重要
后面你看到任意一个 API,都可以回到报文层去理解:
getMethod()对应请求行里的方法getHeader()对应请求头getParameter()对应参数区setStatus()对应响应行里的状态码setHeader()对应响应头getWriter()/getOutputStream()对应响应体输出
一旦你把“API 背后是哪一块 HTTP 报文”建立起关联,这章内容会顺很多。
本章小结
本章核心概念:Request / Response 不是凭空出现的对象,而是对 HTTP 报文的 Java 封装。
你现在应该掌握:
- 请求报文和响应报文的基本结构
- Request 对应请求报文,Response 对应响应报文
- 常见 API 应该映射回哪一块报文内容去理解
3. Request:从客户端拿到什么
3.1 请求行信息获取
请求行最常见的是:
POST /demo/login?username=zhangsan HTTP/1.1对应 API:
| 信息 | 方法 | 说明 |
|---|---|---|
| 请求方法 | getMethod() |
GET / POST / PUT / DELETE |
| 完整 URL | getRequestURL() |
包含协议、主机、端口、路径 |
| URI | getRequestURI() |
只保留资源路径 |
| 上下文路径 | getContextPath() |
应用根路径 |
| 查询字符串 | getQueryString() |
? 后面的内容 |
| 协议 | getProtocol() |
例如 HTTP/1.1 |
示例:
@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 请求头信息获取
请求头常见内容:
Host: localhost:8080
User-Agent: Mozilla/5.0
Accept: application/json, text/plain, */*
Content-Type: application/json;charset=UTF-8最常用的方法有两个:
| 方法 | 说明 |
|---|---|
getHeader(String name) |
获取指定请求头 |
getHeaderNames() |
获取全部请求头名称 |
示例:
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");请求头名称大小写不敏感,所以
Host和host都能取到值。
3.3 客户端与服务器信息
除了请求行和请求头,Request 还能让你知道“这次请求是谁发来的、发到哪里”。
| 信息 | 方法 |
|---|---|
| 服务器 IP | getLocalAddr() |
| 服务器端口 | getLocalPort() |
| 客户端 IP | getRemoteAddr() |
| 客户端端口 | getRemotePort() |
示例:
System.out.println("客户端: " + req.getRemoteAddr() + ":" + req.getRemotePort());
System.out.println("服务器: " + req.getLocalAddr() + ":" + req.getLocalPort());3.4 请求转发(了解)
请求转发是服务器内部把请求交给另一个资源继续处理:
req.getRequestDispatcher("/target.jsp").forward(req, resp);特点:
- 浏览器地址栏不变
- 还是同一次请求
- 可以共享同一个 Request 中的数据
例如:
req.setAttribute("user", user);
req.getRequestDispatcher("/success.jsp").forward(req, resp);目标资源里仍然可以取到 user。
3.5 请求体读取:getReader() 和 getInputStream()
当你处理 JSON 或二进制数据时,参数不一定适合直接用 getParameter()。
这时要读请求体:
| 方法 | 适合场景 |
|---|---|
getReader() |
文本数据,如 JSON、XML |
getInputStream() |
二进制数据 |
文本读取示例:
BufferedReader reader = req.getReader();
String line;
StringBuilder sb = new StringBuilder();
while ((line = reader.readLine()) != null) {
sb.append(line);
}
System.out.println(sb.toString());字节流示例:
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() 去读整段文本:

注意:
同一个请求里,字符流和字节流不能混用。
本章小结
本章核心概念:Request 不只是“拿参数”,它还能拿到请求行、请求头、来源信息、转发上下文和原始请求体。
你现在应该掌握:
- 如何读取请求方法、URL、URI、上下文路径
- 如何读取请求头和来源地址
- 请求转发为什么能共享同一个 Request 对象
- 什么时候该用
getReader()/getInputStream()直接读请求体
4. Request:参数与文件上传
4.1 请求参数获取
请求参数最常见的来源有两个:
- GET:出现在 URL 查询字符串里
- POST:出现在请求体里
对应最常用的四个方法:
| 方法 | 返回值 | 用途 |
|---|---|---|
getParameter(String) |
String | 取单个参数 |
getParameterValues(String) |
String[] | 取同名多值参数 |
getParameterNames() |
Enumeration<String> |
枚举全部参数名 |
getParameterMap() |
Map<String, String[]> |
拿到全部参数映射 |
示例:
String username = req.getParameter("username");
String[] hobbies = req.getParameterValues("hobby");
Map<String, String[]> paramMap = req.getParameterMap();这四个方法最容易混在一起,建议直接结合下面这张图理解它们各自“拿的是哪一层数据”:

4.2 多值参数为什么要用 getParameterValues()
例如多选框:
hobby=sing&hobby=dance在底层会被封装成:
{
"hobby": ["sing", "dance"]
}所以:
- 单值参数可以用
getParameter() - 多值参数必须考虑
getParameterValues()
4.3 POST 中文乱码问题
这是课堂里最常见的坑之一。
解决方式:
@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() 会很啰嗦:
User user = new User();
user.setUsername(req.getParameter("username"));
user.setPassword(req.getParameter("password"));更常见的做法是直接使用 BeanUtils 这类工具类。
如果是 Maven 项目,先引入依赖:
<dependency>
<groupId>commons-beanutils</groupId>
<artifactId>commons-beanutils</artifactId>
<version>1.9.4</version>
</dependency>如果不是 Maven 工程,也要先把对应的 commons-beanutils 相关 jar 包导入项目。
示例:
import org.apache.commons.beanutils.BeanUtils;
User user = new User();
BeanUtils.populate(user, req.getParameterMap());这样就能把参数名和 JavaBean 属性名对应起来,自动完成封装。
注意两点:
- 表单参数名要和对象属性名保持一致
BeanUtils.populate()底层依赖反射,但课堂里优先掌握工具类的使用方式即可
4.5 文件上传处理
文件上传和普通表单的区别在于:表单必须声明 multipart/form-data。
前端示例:
<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()获取上传文件
@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 和文件字段类型,而不是误选成 raw 或 x-www-form-urlencoded:

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

本章小结
本章核心概念:当请求参数越来越复杂时,要分清普通参数封装与文件上传这两条处理路径。
你现在应该掌握:
- 如何把请求参数更高效地封装为对象
- 为什么 POST 中文乱码要先设置编码
- 文件上传为什么必须用
multipart/form-data和@MultipartConfig
5. Response:向客户端输出什么
5.1 响应行:状态码设置
Response 最基础的能力之一,就是设置状态码:
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 响应头:告诉浏览器如何处理结果
通用写法:
resp.setHeader("key", "value");你可以把响应头理解成:
返回正文之前,先告诉浏览器“这份内容是什么、该怎么处理”。
5.3 响应体输出:字符流 vs 字节流
最常用的两个方法:
| 方法 | 适合场景 |
|---|---|
getWriter() |
文本、HTML、JSON |
getOutputStream() |
图片、文件、二进制数据 |
文本输出:
resp.setContentType("text/html;charset=UTF-8");
PrintWriter writer = resp.getWriter();
writer.write("<h1>Hello</h1>");
writer.write("<p>你好</p>");字节输出:
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:最重要的响应头之一
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 |
application/pdf |
它既决定浏览器如何解释响应内容,也常常顺手解决中文乱码问题。
5.5 Content-Disposition:文件下载
如果你希望浏览器把内容当“附件下载”,常用写法是:
resp.setHeader("Content-Disposition", "attachment;filename=report.pdf");
resp.setContentType("application/pdf");再配合字节流把文件写出去即可。
5.6 Location 与重定向
最底层的重定向可以这么写:
resp.setStatus(302);
resp.setHeader("Location", "https://www.example.com");更常用的是:
resp.sendRedirect("https://www.example.com");5.7 Refresh:定时刷新或定时跳转
resp.setHeader("Refresh", "1");
resp.setHeader("Refresh", "3;url=/login.html");这类写法现在不算主流核心能力,但课堂里经常作为了解性案例出现,知道它是响应头控制即可。
本章小结
本章核心概念:Response 的核心职责是“组织响应行、响应头和响应体”,而不是简单地“打印字符串”。
你现在应该掌握:
- 何时改状态码,何时改响应头
- 何时用
getWriter(),何时用getOutputStream() Content-Type、Content-Disposition、Location的典型用途
6. 反射与通用分发思路
6.1 为什么这一章会补反射
Request / Response 这章之所以常常顺带讲反射,是因为真实开发里很容易遇到这样一个问题:
一个 Servlet 里如果要分发多个业务方法,手写 if...else 很笨重。
反射恰好能帮助我们提升“通用分发”的能力。
6.2 获得 Class 对象的三种方式
Class<UserServiceImpl> c1 = UserServiceImpl.class;
UserServiceImpl service = new UserServiceImpl();
Class<? extends UserServiceImpl> c2 = service.getClass();
Class<?> c3 = Class.forName("com.cskaoyan.service.UserServiceImpl");在“通用性开发”里,最常用的是:
Class.forName(...)因为它适合从配置或字符串动态加载类。
6.3 通过反射调用字段和方法
字段示例:
Field field = clazz.getDeclaredField("username");
field.setAccessible(true);
field.set(instance, "zhangsan");方法示例:
Method method = clazz.getDeclaredMethod("login", String.class);
method.setAccessible(true);
Object result = method.invoke(instance, "admin");6.4 一个典型应用:通用请求分发器
比如请求:
/user/login
/user/register
/user/delete你可以取 URI 最后一段,再通过反射去调用同名方法:
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 分发器”的入门思路。
本章小结
本章核心概念:反射不是本章主角,但它能帮助我们把请求分发做得更通用。
你现在应该掌握:
- 为什么这章要补反射
- 反射如何服务于通用分发思路
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、转发 / 重定向、参数 / 请求体
- 为什么流不能混用
- 为什么文件上传不是普通表单处理
8. 本章总复盘
8.1 一条完整主线
把本章压缩成一条链路,就是:
浏览器发起 HTTP 请求
->
Tomcat 封装为 HttpServletRequest / HttpServletResponse
->
Servlet 从 Request 中读取方法、头、参数、请求体
->
业务逻辑处理
->
Servlet 通过 Response 设置状态码、响应头、响应体
->
浏览器按响应头解释并展示结果8.2 你现在最该记住的五件事
- Request 对应请求报文,Response 对应响应报文
- Request 偏输入,Response 偏输出
- 参数区、请求体、文件上传是三种不同输入来源
Content-Type、Location、Content-Disposition是高频响应头BeanUtils能简化参数封装,反射让通用分发更有扩展性
8.3 学完这一章后,下一章会自然衔接什么
学完 Request / Response 后,下一步最自然的方向是:
- 如何记住用户状态
- 为什么 HTTP 本身是无状态的
- Cookie 和 Session 如何参与会话管理
这正是下一章“会话技术”要继续展开的内容。
9. 实战练习
<!-- 实战练习内容已分离到 practices/markdown/06-request-response-practice.md -->
建议先复习本章重点:请求行 / 请求头 / 请求参数 / 请求体的读取方式,以及
Content-Type、重定向、文件下载这些 Response 输出能力,再进入练习。 练习建议按“获取请求信息 -> 参数处理 -> JSON 响应 -> 文件上传下载 -> 通用分发器”这个顺序完成。 配套练习文件:content/optimized/practices/markdown/06-request-response-practice.md