lemon_sjdk头像

从字节流到业务逻辑:Spring Web 请求的完整抽象链路

引言:一次 HTTP 请求的“变形记”

当你在浏览器输入 https://im.example.com/chat 并按下回车,一条消息从物理网线进入服务器,经历了无数次“变形”:从光纤中的光脉冲,到网卡上的电信号,再到操作系统内核中的 TCP 数据段,最终变成 Java 对象,被你的业务代码优雅地处理。在这个过程中,Spring 框架定义了一系列接口,它们像流水线上的工人,各司其职,将原始的字节流一步步“加工”成可操作的高层抽象。

今天,我们就以 ServerHttpRequest 为核心,沿着它的继承链和依赖关系,逐一解析这些接口的设计意图、协作方式,以及它们在整个 Web 请求处理中的位置。


一、三层抽象:从 HttpMessage 到 ServerHttpRequest

让我们沿着继承链,自上而下地理解这些接口的设计哲学。

1.1 HttpMessage:一切 HTTP 消息的“地基”

public interface HttpMessage {
    HttpHeaders getHeaders();
}

这是最基础的接口,只做一件事:提供 HTTP 头的访问。无论是请求还是响应,无论客户端还是服务端,任何 HTTP 消息都包含头部(Headers)。HttpHeaders 就是我们之前深入分析过的那个类——它封装了所有标准 HTTP 头(Content-Type、Authorization、Upgrade 等),并提供类型安全的访问方法。

设计意图:将“头部”这个共性抽取到最高层,避免在每一个子接口中重复定义。

1.2 HttpInputMessage:带“身体”的 HTTP 消息

public interface HttpInputMessage extends HttpMessage {
    InputStream getBody() throws IOException;
}

在 HttpMessage 的基础上,增加了**消息体(Body)**的访问能力。通过 getBody() 返回一个 InputStream,允许你读取原始的请求体字节流。

设计意图:区分“只有头”和“既有头又有体”的消息。例如,GET 请求通常没有体(虽然规范允许),而 POST 请求则必然有体。但 HttpInputMessage 本身不关心是请求还是响应——它既可以是服务端收到的请求体,也可以是客户端收到的响应体。

1.3 HttpRequest:应用层的“请求面单”

public interface HttpRequest extends HttpMessage {
    HttpMethod getMethod();
    URI getURI();
    Map<String, Object> getAttributes();
}

这是我们之前详细分析过的接口。它新增了三个核心方法:

  • getMethod():请求的“动词”(GET、POST 等)。
  • getURI():请求的“地址”(路径 + 查询参数)。
  • getAttributes():请求级别的属性容器(临时存放数据,生命周期随请求结束)。

设计意图:将 HTTP 请求的路由信息(方法 + URI)独立出来,使之可以被 Spring 的 DispatcherServlet 和 HandlerMapping 等组件使用。

1.4 ServerHttpRequest:站在“服务器”视角的完整请求

public interface ServerHttpRequest extends HttpRequest, HttpInputMessage {
    @Nullable Principal getPrincipal();
    InetSocketAddress getLocalAddress();
    InetSocketAddress getRemoteAddress();
    ServerHttpAsyncRequestControl getAsyncRequestControl(ServerHttpResponse response);
}

这是今天我们分析的核心接口。它继承了 HttpRequest(获取方法、URI、头)和 HttpInputMessage(获取请求体),并额外添加了四个服务器端特有的能力:

  • getPrincipal():获取当前认证用户(java.security.Principal)。还记得我们之前讨论的 Principal 吗?它代表“你是谁”。在 WebSocket 握手中,你验证 JWT 后得到的用户名,就可以封装成 Principal 并返回。
  • getLocalAddress():服务器本地地址(本机 IP + 端口)。用于判断请求到达了哪个网卡或端口。
  • getRemoteAddress():远程客户端地址(客户端 IP + 端口)。这就是我们之前详细分析的 InetSocketAddress!用于 IP 黑名单、日志记录等。
  • getAsyncRequestControl():获取异步请求控制器,用于开启 Servlet 3.0+ 的异步处理模式。

设计意图:HttpRequest 是通用的,可同时用于客户端和服务端;而 ServerHttpRequest 专门服务于服务器端,增加了与服务器运行环境相关的信息(本地地址、远程地址、用户身份、异步控制)。


二、异步处理:ServerHttpAsyncRequestControl

public interface ServerHttpAsyncRequestControl {
    void start();
    void start(long timeout);
    boolean isStarted();
    void complete();
    boolean isCompleted();
}

这是 ServerHttpRequest 中一个特殊的存在——它不是数据容器,而是行为的控制器。

它的作用:在 Servlet 3.0+ 规范中,请求可以被“异步化”。当你的业务逻辑需要长时间处理(如等待数据库查询、调用远程服务)时,你可以:

  1. 调用 start() 开启异步模式,释放当前线程,避免阻塞 Servlet 线程池。
  2. 在另一个线程中完成业务逻辑后,调用 complete() 结束异步处理并返回响应。
  3. 如果超时未完成,容器会自动处理超时。

在 IM 中的潜在应用:虽然 WebSocket 长连接本身是异步的,但在一些 REST API 场景中(如上传大文件、轮询新消息),使用异步请求控制可以极大提升服务器的吞吐量。


三、完整类图与协作关系

让我们把所有讨论过的类放到一张“网络请求处理全景图”中:

┌─────────────────────────────────────────────────────────────────┐
│ 客户端 (App/浏览器)                                            │
└───────────────────────┬─────────────────────────────────────────┘
                        │ HTTP GET /ws (含 Upgrade 头)
                        ▼
┌─────────────────────────────────────────────────────────────────┐
│ 物理层/传输层 (网卡 + TCP/IP)                                 │
│ 数据封装成 InetSocketAddress (IP + 端口)                      │
└───────────────────────┬─────────────────────────────────────────┘
                        ▼
┌─────────────────────────────────────────────────────────────────┐
│ 应用层容器 (Tomcat/Netty)                                      │
│ 将字节流解析为 ServerHttpRequest 对象                          │
│   ├── 继承 HttpMessage → getHeaders()  (含 JWT)               │
│   ├── 继承 HttpInputMessage → getBody() (请求体,可能为空)     │
│   ├── 实现 HttpRequest → getMethod() (GET) + getURI() (/ws)   │
│   ├── getRemoteAddress() → InetSocketAddress (客户端 IP)       │
│   ├── getLocalAddress() → InetSocketAddress (服务端 IP)        │
│   └── getPrincipal() → 认证后的用户 (由拦截器填充)             │
└───────────────────────┬─────────────────────────────────────────┘
                        ▼
┌─────────────────────────────────────────────────────────────────┐
│ HandshakeInterceptor (你的代码)                                │
│  1. request.getMethod() → 检查是否为 GET                       │
│  2. request.getHeaders().getFirst("Authorization") → 提取 JWT  │
│  3. 验证 JWT → 生成 Principal                                  │
│  4. request.getRemoteAddress() → 检查 IP 黑名单                │
│  5. 通过后,将 Principal 存入 WebSocketSession                 │
└─────────────────────────────────────────────────────────────────┘

四、所有讨论过的类:一张“身份对照表”

类/接口所属包核心职责类比在 IM 中的角色
HttpMessageorg.springframework.http提供 HTTP 头快递面单的“备注栏”提取 Authorization 头
HttpInputMessageorg.springframework.http提供 HTTP 头 + 请求体带货物的快递包裹读取 POST 请求的 JSON 体
HttpRequestorg.springframework.http提供 方法 + URI + 头完整快递面单(含收件人地址)判断 GET /ws
ServerHttpRequestorg.springframework.http.server服务器端完整请求 + 客户端地址 + 用户身份 + 异步控制带物流追踪信息的快递面单获取客户端 IP、认证用户、开启异步
SocketAddressjava.net抽象地址基类“地址”这个概念本身父类,提供多态
InetSocketAddressjava.netIP + 端口的具体地址快递上的收件人门牌号getRemoteAddress() 的返回值
URIjava.net资源路径(不含 IP)快递面单上的**“几号楼几单元”**getURI().getPath() 得到 /ws
HttpMethodorg.springframework.httpHTTP 动词(GET/POST)快递的**“加急/普通”**标签getMethod() 校验是否为 GET
HttpHeadersorg.springframework.httpHTTP 头集合快递面单的所有栏目存放 Authorization、Upgrade
Principaljava.security用户身份(逻辑名)快递的收件人姓名代表认证后的用户名
ServerHttpAsyncRequestControlorg.springframework.http.server异步控制开关快递的**“延时派送”**按钮处理长耗时请求,释放线程

五、设计模式与架构智慧

5.1 单一职责原则

  • HttpMessage 只管头。
  • HttpInputMessage 只管头 + 体。
  • HttpRequest 只管路由信息。
  • ServerHttpRequest 只管服务器环境信息。
  • 每个接口只做一件事,但通过继承组合出完整功能。

5.2 接口隔离原则

  • 客户端和服务器端的请求接口分开(HttpRequest vs ServerHttpRequest),避免客户端被迫实现 getRemoteAddress() 等无用方法。

5.3 依赖倒置原则

  • 你的拦截器依赖 ServerHttpRequest 接口,而不是具体的 Tomcat 或 Netty 实现类。这使得你的代码可以在不同 Servlet 容器间无缝迁移。

六、在 IM 中的实战串联

让我们将所有这些类放入一个真实的 WebSocket 握手场景中:

  1. 客户端:发送 GET /ws HTTP/1.1,头包含 Authorization: Bearer eyJ... 和 Upgrade: websocket。
  2. Tomcat/Netty:解析字节流,创建 ServerHttpRequest 对象:
    • 通过 HttpHeaders 存储所有头。
    • 通过 URI 存储 /ws。
    • 通过 HttpMethod 存储 GET。
    • 通过 InetSocketAddress 存储客户端 IP 和端口。
  3. 你的 HandshakeInterceptor:
    • request.getMethod() → HttpMethod.GET,校验通过。
    • request.getHeaders().getFirst("Authorization") → 提取 JWT。
    • 验证 JWT 后,得到一个用户名 "zhangsan"。
    • 实现一个简单的 Principal 实现,getName() 返回 "zhangsan",并通过某种方式(如 Spring Security)将其绑定到请求。
    • request.getRemoteAddress() → 获取 InetSocketAddress,检查黑名单。
  4. Spring WebSocket:握手成功后,将 Principal 和 InetSocketAddress 等信息存入 WebSocketSession。
  5. 后续消息处理:在 @MessageMapping 方法中,通过 Principal principal 参数直接获取用户名,通过 WebSocketSession 获取客户端地址。

七、总结:从接口设计中学习架构思想

通过分析 ServerHttpRequest 及其父接口的继承链,我们可以学到:

  1. 分层抽象:从 HttpMessage(头)→ HttpInputMessage(+体)→ HttpRequest(+路由)→ ServerHttpRequest(+服务器信息),每一层都在前一层的基础上增加一个明确的职责,而不是一次性将所有方法堆砌在一个接口中。

  2. 关注点分离:Principal 代表身份,InetSocketAddress 代表物理位置,URI 代表逻辑资源,HttpHeaders 代表元数据——它们分别对应安全层、传输层、应用层和会话层,各司其职。

  3. 可扩展性:通过接口而非具体类编程,使得 Spring 可以同时支持 Servlet 容器(基于阻塞 I/O)和 Netty(基于非阻塞 I/O),而你的业务代码无需修改。

  4. 异步编程支持:ServerHttpAsyncRequestControl 的引入,体现了现代 Web 框架对高并发的深刻理解——通过释放线程,用更少的资源处理更多的请求。

现在,当你再次阅读 HandshakeInterceptor 中的 beforeHandshake(ServerHttpRequest request, ...) 方法时,你将清晰地看到:这个参数背后承载了从物理网线、TCP 连接、HTTP 协议解析到用户身份认证的全部信息。理解这整个链路,你就真正掌握了 Spring Web 框架的精髓。


附录:ServerHttpRequest 完整代码及逐行注释

/*
 * Copyright 2002-present the original author or authors.
 * SPDX-License-Identifier: Apache-2.0
 */
package org.springframework.http.server;

import java.net.InetSocketAddress;
import java.security.Principal;
import org.jspecify.annotations.Nullable;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpRequest;

/**
 * Represents a server-side HTTP request.
 *
 * 设计目标:作为服务器端 HTTP 请求的最高抽象,整合路由信息、消息体、服务器环境信息和用户身份。
 * 它同时继承了 HttpRequest(提供方法+URI+头)和 HttpInputMessage(提供请求体),
 * 并额外增加了服务器特有的功能。
 *
 * @author Arjen Poutsma
 * @author Rossen Stoyanchev
 * @since 3.0
 */
public interface ServerHttpRequest extends HttpRequest, HttpInputMessage {

    /**
     * Return a {@link java.security.Principal} instance containing the name of the
     * authenticated user.
     *
     * 作用:获取当前请求的认证用户。如果未认证,返回 null。
     * 在 WebSocket 握手场景中,HandshakeInterceptor 在验证 JWT 后,
     * 可以通过 Spring Security 或手动方式将 Principal 绑定到这里。
     * 后续业务代码(如 @MessageMapping)可以直接通过参数获取。
     *
     * <p>If the user has not been authenticated, the method returns <code>null</code>.
     */
    @Nullable
    Principal getPrincipal();

    /**
     * Return the address on which the request was received.
     *
     * 作用:返回服务器本地地址(本机 IP + 端口)。
     * 用于判断请求到达了哪个网络接口,例如区分内网和外网访问。
     *
     * 返回类型 InetSocketAddress 是我们之前详细分析的类,
     * 它封装了 IP 和端口,支持 IPv4/IPv6。
     */
    InetSocketAddress getLocalAddress();

    /**
     * Return the address of the remote client.
     *
     * 作用:返回远程客户端地址(客户端 IP + 端口)。
     * 这是实现 IP 黑名单、地理定位、访问日志等功能的基础。
     *
     * 重要:在反向代理(如 Nginx)环境下,获取的可能是代理的 IP,
     * 需结合 X-Forwarded-For 头获取真实 IP,这再次体现了 HttpHeaders 的重要性。
     */
    InetSocketAddress getRemoteAddress();

    /**
     * Return a control that allows putting the request in asynchronous mode so the
     * response remains open until closed explicitly from the current or another thread.
     *
     * 作用:获取异步请求控制对象,用于开启 Servlet 3.0+ 的异步处理。
     *
     * 使用场景:当一个 HTTP 请求需要长时间处理(如等待消息队列、进行复杂计算)时,
     * 可以调用 start() 释放当前线程,在另一个线程完成后再调用 complete() 返回响应。
     * 这极大提升了服务器的并发处理能力。
     *
     * @param response 对应的 ServerHttpResponse,用于在异步完成后写入响应
     */
    ServerHttpAsyncRequestControl getAsyncRequestControl(ServerHttpResponse response);

}

这段注释结合了历史对话中涉及的 Principal、InetSocketAddress、HttpHeaders、HttpMethod 等类的知识,将接口方法的实际用途与底层原理串联起来,帮助理解 Spring Web 请求处理的全貌。


📜 Servlet 3.0+ 规范有哪些?

Servlet 规范是 Java Web 开发的基础,3.0 版本(JSR 315)是一个重要的里程碑。它的核心升级是引入了异步处理支持。

  • 核心思想:在传统的 Servlet 模型中,一个请求会一直占用一个线程,直到业务处理完成并返回响应。如果业务逻辑很耗时(比如查数据库、调远程接口),这个线程就会被长时间占用,导致服务器能同时处理的请求数受限。
  • 异步处理:Servlet 3.0 允许我们将耗时的操作交给另一个线程去执行,而原来的 Servlet 线程可以立即返回给容器,去处理其他请求。
  • 如何实现:主要通过 ServletRequest.startAsync() 方法获取一个 AsyncContext 对象。通过这个对象,你可以控制异步任务的启动、超时和完成。

简单来说,异步 Servlet 是为了解决“请求线程被长时间占用”的问题,提升服务器的吞吐量。

🆔 Principal 对象代表了 IM 软件中的用户对象吗?

是的,但它更像是一个“身份证”,而非完整的“用户档案”。

在 Java 的安全体系中,java.security.Principal 接口代表一个可被认证的“主体”。在我们之前的讨论中,它被定义为:

public interface Principal {
    String getName(); // 核心方法:获取主体的名称
    // ... 其他方法
}
  • 在 IM 中的对应:当你通过 JWT 认证一个用户后,这个用户就对应一个 Principal 对象。principal.getName() 返回的通常是这个用户的唯一标识(如用户名或用户ID)。
  • 它不是完整的用户对象:一个完整的“用户对象”通常包含昵称、头像、个性签名等更丰富的属性。Principal 只负责身份识别,不负责承载业务数据。在 Spring Security 中,你通常会获得一个 Authentication 对象,它实现了 Principal 接口,并可以通过 getPrincipal() 方法获取到包含更多用户信息的 UserDetails 对象。

⏳ ServerHttpAsyncRequestControl 怎么使用?

ServerHttpAsyncRequestControl 是 Spring 对 Servlet 3.0 异步支持的封装,通过 ServerHttpRequest.getAsyncRequestControl(ServerHttpResponse response) 方法获取。

它提供了一种更声明式的方式来控制异步请求。一个典型的使用流程如下:

  1. 获取控制器:在 Spring 的拦截器或控制器中,通过 ServerHttpRequest 对象获取 ServerHttpAsyncRequestControl 实例。
  2. 启动异步模式:调用 start() 方法开启异步处理。此时,当前的 Servlet 线程会立即返回,但客户端的连接会保持打开状态。
  3. (可选)设置超时:调用 start(long timeout) 可以设置一个超时时间。如果在这个时间内没有调用 complete(),请求会自动超时。
  4. 完成处理:在另一个线程中完成业务逻辑后,必须调用 complete() 方法来结束异步处理,并将响应返回给客户端。否则,连接会一直保持到超时。
  5. 检查状态:你可以通过 isStarted() 和 isCompleted() 方法来检查当前异步处理的状态。

🔗 Servlet 和 WebSocket 的区别和联系是什么?

这个问题触及了即时通讯的核心。

  • 联系:WebSocket 的握手过程依赖于 HTTP 协议,因此它需要运行在支持 WebSocket 的 Servlet 容器(如 Tomcat)之上。Java 的 Servlet 规范从 3.1 版本开始,原生支持 WebSocket。
  • 区别:
特性Servlet (HTTP)WebSocket
通信模式单向、请求-响应。客户端发起请求,服务器响应。全双工、双向。连接建立后,客户端和服务器可以随时向对方发送消息。
连接状态无状态。每次请求都是独立的,服务器不会保持连接状态。有状态。一旦建立连接,就会保持一个持久化的 TCP 连接。
适用场景传统的 Web 应用、RESTful API。实时性要求高的应用,如聊天、游戏、股票行情等。
协议层次应用层协议,基于 TCP。应用层协议,基于 TCP,但设计上更接近一个“薄层”。

🤝 TCP 连接是什么意思?

TCP(Transmission Control Protocol,传输控制协议)是互联网的核心协议之一,它提供可靠的、面向连接的数据传输服务。可以把它想象成一次电话通话:

  1. 建立连接(三次握手):
    • 客户端:“喂,能听到吗?”(发送 SYN)
    • 服务端:“能听到,你能听到我吗?”(回复 SYN-ACK)
    • 客户端:“能听到,我们开始吧。”(回复 ACK)
      完成这三次交互,一个可靠的连接就建立了。
  2. 数据传输:连接建立后,双方就可以像通电话一样,可靠地、按顺序地交换数据。
  3. 关闭连接(四次挥手):通话结束后,双方通过四次交互来优雅地关闭连接,释放资源。

WebSocket 和 HTTP 都建立在 TCP 连接之上。HTTP 是“发短信”,每次都要重新建立连接;而 WebSocket 是“打电话”,一次握手,持久通话。

🏗️ Servlet 容器在 IM 软件里的作用是什么?

在你的 IM 软件中,Servlet 容器(如 Tomcat、Jetty)扮演着基石和运行时环境的角色。它的核心作用体现在:

  1. 网络通信与协议解析:容器接管了复杂的网络 I/O 操作。它监听端口,接收来自客户端的 TCP 连接,并将原始的 HTTP 字节流解析成 Java 对象(如 HttpServletRequest)。
  2. 管理 Servlet 生命周期:它负责加载、初始化、调用和销毁你编写的 Servlet 或 Spring MVC 的 DispatcherServlet。
  3. 提供 WebSocket 支持:对于支持 WebSocket 的容器(如 Tomcat 7.0+),它同样负责处理 WebSocket 的握手请求和后续的数据帧传输。
  4. 提供多线程处理能力:容器维护着一个线程池,每当有请求到来,它会分配一个线程去处理。你的业务代码就是在这个线程中运行的。

简单说,没有 Servlet 容器,你的 Spring Boot 应用就无法接收和处理任何来自网络的请求。

📨 HTTP 协议是解析的?

HTTP 协议的解析是 Servlet 容器的核心职责之一。这个过程可以概括为:

  1. 建立连接:客户端(如浏览器)与服务器上的 Servlet 容器(如 Tomcat)建立 TCP 连接。
  2. 接收原始数据:容器通过这个 TCP 连接,接收客户端发送过来的原始字节流。这些字节流就是按照 HTTP 协议格式编码的文本。
  3. 协议解析:容器内部有一个称为“连接器(Connector)”的组件。它的工作就是解析这些原始字节流。它会按照 HTTP 协议的规范,将字节流拆解成:
    • 请求行:包含方法(GET、POST)、URI(路径)、协议版本。
    • 请求头(Headers):包含各种键值对,如 Host、Content-Type、Authorization。
    • 请求体(Body):如果有的话,就是 POST 请求中的表单数据或 JSON 等。
  4. 封装成对象:解析完成后,容器会将所有这些信息封装到一个 HttpServletRequest 对象中。
  5. 交给应用处理:最后,容器会调用你的 Spring MVC 的 DispatcherServlet,并将这个封装好的 HttpServletRequest 对象传给它。接下来,就是 Spring MVC 的流程了(路由、参数绑定、执行业务逻辑等)。

💎 总结

所有这些问题都指向了你 IM 软件的技术栈核心。从操作系统底层的 TCP 连接,到运行在它之上的 Servlet 容器;从容器解析出的 HTTP 协议,到容器支持的 WebSocket 长连接;从代表用户身份的 Principal 对象,到优化线程利用的 ServerHttpAsyncRequestControl——这一切共同构成了一个现代 Java Web 应用,尤其是即时通讯应用的完整图景。

转载自 CSDN-专业IT技术社区

原文链接:https://blog.csdn.net/qq_24992377/article/details/163312199

文章来源转载

评论

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--