📌 一句话总结:URI 是什么?
URI (统一资源标识符)是一个结构化字符串 ,用于唯一标识一个资源(比如一个网页、一个 WebSocket 端点、一个文件)。在 IM 中,它就是你客户端连接的**"目的地"**,例如 ws://example.com/chat 或 https://api.example.com/login。
🔗 URI 和 Spring 中其他类的关系(回顾历史)
你之前已经接触过这些 Spring 类:
HttpMethod:表示请求的"动作"(GET、POST)。HttpHeaders:表示请求的"元数据"(认证头、内容类型)。HttpRequest:表示一个完整的 HTTP 请求 ,它包含了方法、URI、头等。
这里的 URI 就是 HttpRequest 的地址部分 。在你的 HandshakeInterceptor 中,你通过 request.getURI() 拿到这个对象,然后可以做很多事情:
- 读取路径(如
/ws) - 读取查询参数(如
?token=xxx) - 甚至判断是否指向了正确的端点。
所以,URI 是 HttpRequest 的组成部分 ,而 HttpRequest 又在你的拦截器里被用来做校验和路由。
🧩 URI 的结构(与你的 IM 实际场景对应)
一个典型的 WebSocket 连接地址可能是:
ws://yourdomain.com/chat?token=eyJhbGci...
对应的 URI 对象会解析为:
| 组件 | 值 | 说明 |
|---|---|---|
scheme |
ws |
协议(WebSocket) |
host |
yourdomain.com |
主机名 |
port |
未指定(默认80) | 端口 |
path |
/chat |
路径(端点) |
query |
token=eyJhbGci... |
查询字符串(常用来传 JWT) |
fragment |
null |
片段(#后部分,不常使用) |
在你的拦截器中,你可以这样提取 token:
java
URI uri = request.getURI();
String query = uri.getQuery(); // "token=eyJhbGci..."
Map<String, String> params = Arrays.stream(query.split("&"))
.map(p -> p.split("="))
.collect(Collectors.toMap(p -> p[0], p -> p[1]));
String token = params.get("token");
当然,更推荐从 Authorization 头取,但 URI 的 query 提供了一种备选方案。
🛠️ 核心方法一览(IM 开发常用)
| 方法 | 作用 | 示例 |
|---|---|---|
getScheme() |
获取协议名 | "ws" |
getHost() |
获取主机名 | "yourdomain.com" |
getPort() |
获取端口(-1表示未指定) | -1 |
getPath() |
获取路径 | "/chat" |
getQuery() |
获取查询字符串 | "token=xxx" |
getFragment() |
获取片段 | null |
toString() |
返回完整 URI 字符串 | "ws://yourdomain.com/chat?token=xxx" |
resolve(String) |
基于当前 URI 解析相对 URI | 可用于拼接端点 |
normalize() |
规范化路径(去除 . 和 ...) | 用于路径处理 |
特别注意 :getQuery() 返回的是原始 查询字符串,你需要自己解析(或用 URLEncodedUtils 等工具)。getPath() 返回的路径也是原始形式,但不会解码 %20 等转义字符(如果需要解码,可以用 getPath() 再调用 URLDecoder.decode)。
📦 构造 URI:几种方式
你不必 手动创建 URI 对象(Spring 会帮你解析请求中的 URI),但在客户端发起连接时,你需要构建一个 URI 字符串。你可以用:
- 直接字符串 :
new URI("ws://example.com/chat?token=xxx") - 多参数构造 :
new URI("ws", "example.com", "/chat", "token=xxx", null)
这些构造方法会自动检查非法字符并转义。
🧠 在 IM 中的典型应用场景
场景 1:WebSocket 握手拦截器(你已经见过)
- 通过
request.getURI()获取路径,判断是否匹配/ws或/chat。 - 通过
uri.getQuery()或uri.getPath()提取参数,做额外校验(例如白名单检查)。
场景 2:构造重定向地址
- 用户登录成功后,服务端可能返回一个
Location头,指向ws://...的 WebSocket 端点。HttpHeaders.setLocation(URI)就接受URI对象。
场景 3:日志与监控
- 打印请求的完整 URI,方便追踪问题。
场景 4:REST API 路由
- 控制器中通过
@RequestMapping匹配路径,底层也是基于 URI 的解析。
🆚 与 URL 类的区别(简单提一下)
URI 只负责解析和标识 ,不涉及网络操作;而 URL 可以打开连接(openConnection())。在 IM 后端,我们几乎只用 URI,因为不需要用 Java 标准库直接发起 WebSocket 连接(我们用的是 Spring 的 WebSocket 客户端或 STOMP)。
💡 总结:URI 在你的 IM 项目中扮演什么角色?
- 它是 地址解析器:将客户端的连接字符串拆解成 scheme、host、path、query 等部分。
- 它是 参数提取器:在握手阶段,你可以从 query 中获取 token,作为备用认证方式。
- 它是 路由匹配器 :通过 path 判断该请求应该由哪个 WebSocket 处理器处理(比如区分
/chat和/admin)。 - 它是 日志记录器:完整 URI 能帮助你快速定位请求来源。
因此,URI 是 HttpRequest 中不可或缺的"地址"组件,和 HttpMethod(动作)、HttpHeaders(元数据)一起,共同构成了一个完整的 HTTP 请求。理解这个类,你就能更清楚地知道 Spring 在拦截器中为你提供了哪些信息,从而编写更健壮的认证和路由逻辑。
HttpRequest
它虽然只有短短的几行,但在整个 Spring Web 体系中处于核心枢纽 的位置。它就像是把 HttpMethod(动词)、URI(地址)和 HttpHeaders(面单信息)组装成一张完整快递单的那个"包装盒"。
下面我带你逐行拆解,并在最后串联它在你的 IM 握手流程中的实际作用。
1. 包声明与导入(它的"家庭住址")
java
package org.springframework.http;
- 它属于
org.springframework.http包。这个包是 Spring 中所有 HTTP 协议相关抽象 的核心所在地(包括HttpMethod、HttpHeaders、MediaType等)。
java
import java.net.URI;
import java.util.Map;
java.net.URI:导入 Java 标准库中的 URI 类(就是你上一个问题详细分析的那个类)。这表示HttpRequest会用它来存储请求的"目标地址"。java.util.Map:导入 Map 接口,用于存储请求级别的附加属性。
2. 类注释(设计意图说明书)
java
/**
* Represents an HTTP request message, consisting of a
* {@linkplain #getMethod() method} and a {@linkplain #getURI() URI}.
*
* @author Arjen Poutsma
* @since 3.1
*/
- 核心含义 :该类代表一个 HTTP 请求消息。它由"方法"(Method)和"URI"组成。
- 深层解读 :这是对 HTTP 请求最精简的抽象。一个标准的 HTTP 请求,必须包含"我要做什么"(GET/POST)和"我要访问谁"(/api/login)。这个接口就是锁定了这两个最核心的要素。
3. 接口声明与继承关系
java
public interface HttpRequest extends HttpMessage {
-
public interface:这是一个接口,定义了一套规范,供ServerHttpRequest(你拦截器里拿到的对象)等具体实现类去遵循。 -
extends HttpMessage(最关键的一点) :
注意,这里继承了HttpMessage。虽然在这段代码里看不到,但在 Spring 源码中,HttpMessage接口定义了一个方法:javaHttpHeaders getHeaders();这意味着 :任何实现了
HttpRequest的对象,同时自动拥有getHeaders()方法。因此,一个完整的HttpRequest实际上包含了:方法(Method) + 地址(URI) + 请求头(Headers)。
4. 核心方法一:getMethod()
java
/**
* Return the HTTP method of the request.
* @return the HTTP method as an HttpMethod value
* @see HttpMethod#valueOf(String)
*/
HttpMethod getMethod();
- 字面含义:返回这个请求的 HTTP 方法。
- 结合历史 :这个方法返回的是
HttpMethod对象(你之前详细分析过的那个枚举/类)。 - 在你的 IM 中的作用 :在你的
HandshakeInterceptor中,你正是通过request.getMethod()拿到它,然后判断if (method != HttpMethod.GET),从而严格拒绝非 GET 方法的 WebSocket 升级请求,堵死恶意攻击路径。
5. 核心方法二:getURI()
java
/**
* Return the URI of the request (including a query string if any,
* but only if it is well-formed for a URI representation).
* @return the URI of the request (never {@code null})
*/
URI getURI();
- 字面含义:返回请求的 URI(包含查询字符串,且永远不会为 null)。
- 结合历史 :这个方法返回的是
java.net.URI对象。 - 在你的 IM 中的作用 :
- 获取路径 :你可以通过
uri.getPath()判断请求是发给/ws(WebSocket)还是/api(普通接口)。 - 获取参数 :如果你选择不在 Header 里传 Token,而选择放在 URL 里(如
ws://host/chat?token=xxx),你可以用uri.getQuery()提取出来。这为你的认证方式提供了备选方案(虽然 Header 方式更安全,但了解这个可以帮你读懂别人的代码)。
- 获取路径 :你可以通过
6. 核心方法三:getAttributes()(Spring 6.2 新增)
java
/**
* Return a mutable map of request attributes for this request.
* @since 6.2
*/
Map<String, Object> getAttributes();
-
字面含义 :返回一个可变的请求属性 Map。
-
区别于 Session :这里的 Attributes 是请求级别的(Request Scope)。它只在当前这一次 HTTP 请求的生命周期内有效,请求结束就销毁。
-
在你的 IM 中的作用 :这是一个"临时便签本"。在你处理 WebSocket 握手时(
beforeHandshake还没结束),你可以把解析出来的userId临时塞进这个 Map:javarequest.getAttributes().put("tempUserId", 123);然后,在同一个请求链的后续处理(比如调用
afterHandshake)中,你可以再取出来做最后的日志记录。不过,要把数据传给 WebSocket 长连接,你还是得用HandshakeInterceptor参数里的那个attributes参数 ,因为那个参数里的数据会被复制进WebSocketSession。
🚀 大串联:在你的 IM 软件中,它是如何协同工作的?
你现在已经学完了 IM 通信链路 中的所有"HTTP 零件",让我们把它们串成一条完整的链:
- 客户端发起请求 :字符串
"GET /ws?token=123 HTTP/1.1"加上头Authorization: Bearer xxx。 - Spring 底层解析 :
- 把
"GET"变成HttpMethod.GET对象。 - 把
"/ws?token=123"变成java.net.URI对象。 - 把头信息变成
HttpHeaders对象。 - 把这三者打包成一个
ServerHttpRequest对象(即HttpRequest接口的实现)。
- 把
- 你的拦截器介入 (
beforeHandshake):request.getMethod()→ 取HttpMethod,校验是否为 GET。request.getHeaders()→ 取HttpHeaders,拿出 JWT Token 校验(继承自HttpMessage)。request.getURI()→ 取URI,校验路径是否为/ws。
- 校验通过 :你通过拦截器参数里的
attributes塞入用户名。 - 长连接建立 :Spring 把拦截器
attributes里的数据搬运到WebSocketSession中。
总结一句话 :HttpRequest 接口就像一个**"数据集装箱"**,把 HttpMethod(动作标签)、URI(地址标签)、HttpHeaders(详情面单)牢牢绑在一起,交给了你的拦截器。你的拦截器打开这个集装箱,逐一检查里面的零件,全部合格后才批准"升级"为 WebSocket 电话线。理解了这个接口,你就彻底打通了 "HTTP 请求解析" 与 "WebSocket 握手决策" 之间的所有环节。🎉