Request & Response

 URL 与 URI

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

Request 和 Response,本质上就是把这报文的四块内容映射到 Java API 上。

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

请求报文示例

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 / 表单数据 / 文件数据真正提交的数据

响应报文示例

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 / 图片 / 文件字节流真正返回给客户端的内容

可以把它们理解成:

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

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

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

Request

请求行信息获取

请求行最常见的是:

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

对应 API:

信息方法说明
请求方法getMethod()GET / POST / PUT / DELETE
完整 URLgetRequestURL()包含协议、主机、端口、路径
URIgetRequestURI()只保留资源路径
上下文路径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());
    }
}

请求头信息获取

请求头常见内容:

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 都能取到值。

请求体读取

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() 去读整段文本:

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

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

客户端与服务器信息

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

信息方法返回值说明
服务器IPgetLocalAddr()String接收请求的服务器IP
服务器端口getLocalPort()int接收请求的服务器端口
客户端IPgetRemoteAddr()String发送请求的客户端IP
客户端端口getRemotePort()int发送请求的客户端端口

示例:

@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) 
        throws ServletException, IOException {
    
    String localAddr = req.getLocalAddr();
    int localPort = req.getLocalPort();
    String remoteAddr = req.getRemoteAddr();
    int remotePort = req.getRemotePort();
    
    System.out.println("请求来源: " + remoteAddr + ":" + remotePort);
    System.out.println("目标地址: " + localAddr + ":" + localPort);
}

 请求转发(了解)

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

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

特点:

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

例如:

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

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

Request参数与文件上传

请求参数获取

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

  • 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();

结合下面这张图理解它们各自“拿的是哪一层数据”:

getParameter、getParameterValues、getParameterNames 与 getParameterMap 的关系图

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

例如多选框:

hobby=sing&hobby=dance

在底层会被封装成:

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

所以:

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

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() 之前。

参数封装为对象

如果参数一多,手动 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() 底层依赖反射,优先掌握工具类的使用方式即可

文件上传处理

文件上传和普通表单的区别在于:表单必须声明 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

Postman 中 form-data 文件上传示意

进一步往底层看,multipart/form-data 请求体其实会把普通字段和文件字段分段封装。

Response

响应行:状态码设置

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

resp.setStatus(200);
resp.setStatus(302);

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

通用写法:

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

响应体输出:字符流 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);
}

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

特殊响应头:Content-Type

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

常见取值:

内容Content-Type
HTMLtext/html;charset=UTF-8
纯文本text/plain;charset=UTF-8
JSONapplication/json;charset=UTF-8
JPEG 图片image/jpeg
PNG 图片image/png
PDFapplication/pdf

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

Content-Disposition:文件下载

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

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

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

Location 与重定向

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

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

更常用的是:

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

Refresh:定时刷新或定时跳转

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

这些响应头已经是前端工作,知道它是响应头控制即可。

转发 vs 重定向

特性转发(forward)重定向(redirect)
URL变化不变改变
请求次数1次2次
数据共享共享Request不共享
跳转范围服务器内部任意URL
适用场景MVC内部跳转登录后跳转、跨域跳转

反射与通用分发思路

一个 Servlet 里如果要分发多个业务方法,手写 if...else 很笨重。反射恰好能帮助我们提升“通用分发”的能力。

获得 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(...)

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

通过反射调用字段和方法

字段示例:

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");

典型应用:通用请求分发器

比如请求:

/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 分发器”的入门思路。

总结

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

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

核心:

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

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇