全链路 HTTP 编解码:从客户端到服务端的数据流转
作为开发者,我们每天都在和网络请求打交道。但你有没有想过:当 iOS 调用 URLSession 发起一个 GET 请求时,参数里的中文是怎么处理的?后端的 Spring 框架又是如何解析这些参数的?Jackson 在其中扮演了什么角色?
这篇文章会从 HTTP 协议的编码规范讲起,深入客户端网络层的实现细节,再到服务端的 Jackson 与 Spring 请求处理。希望读完之后,你对「数据是怎么在客户端和服务端之间流转的」能有一个完整的认识。
1. 网络层编解码:HTTP 协议视角
本章聚焦 HTTP 的编码细节。如果你对 HTTP 协议本身(请求方法、状态码、Header 语义等)还不太熟悉,可以先阅读 计算机网络之五 - HTTP 与 HTTPS。
1.1 HTTP 协议本身不做编码
先澄清一个常见的误解:HTTP 协议本身不执行任何编码操作。
HTTP 只是一份规范文档(RFC 7230-7235 等),它规定了 “数据应该以什么格式传输”,比如 URL 中的中文要用 Percent-Encoding、Body 的格式由 Content-Type 指定。但它不会帮你把中文变成 %E4%B8%AD%E6%96%87,也不会帮你把对象序列化成 JSON——这些工作由各层软件完成。
1.2 编码职责分层
一个网络请求的编码工作,由多层软件协作完成:
| 层级 | 负责什么 | 谁来做 | 是否自动 |
|---|---|---|---|
| 应用层 | 对象 → JSON 字符串 | 你的代码调用 JSONEncoder / Gson | 手动调用 |
| HTTP 框架层 | URL 编码、HTTP 报文封装 | URLSession / OkHttp / Alamofire | 大部分自动 |
| 传输层 | TCP 分段、校验和 | 操作系统网络栈 | 完全自动 |
| 网络层 | IP 数据包封装 | 操作系统网络栈 | 完全自动 |
用图来表示:
关键点:JSON 序列化不是自动的。即使你用 Alamofire 这样的高层框架,也需要指定编码器(如 JSONParameterEncoder)。框架只是帮你简化了调用方式,底层仍然是手动触发的序列化。
1.3 三个编码场景详解
在一个 HTTP 请求中,数据可以出现在三个位置,每个位置的编码规则和实现方式不同:
| 位置 | 典型场景 | 编码规则 | 谁来做 |
|---|---|---|---|
| URL Query | GET 请求参数 | Percent-Encoding | HTTP 框架自动,边界情况需手动 |
| Header | 认证信息、自定义元数据 | ASCII(非 ASCII 需 Base64) | 手动处理 |
| Body | POST/PUT 请求体 | 取决于 Content-Type | 序列化手动,封装自动 |
URL Query 编码
URL 规范(RFC 3986)规定,Query 中只能包含特定的 ASCII 字符。对于中文、空格等字符,必须进行 Percent-Encoding(关于 URL 的完整结构,可参考 计算机网络之二 - URL 与 DNS):
原始:name=张三&city=北京
编码后:name=%E5%BC%A0%E4%B8%89&city=%E5%8C%97%E4%BA%AC
这里有个细节经常被忽略:空格的编码。在 Query 中,空格可以编码为 %20 或 +,但两者的语义略有不同。表单提交(application/x-www-form-urlencoded)通常用 +,而标准 URL 编码用 %20。
谁来做? iOS 的 URLComponents、Android 的 Uri.Builder 会自动编码大部分字符,但 + 等边界字符可能需要手动处理。
源码参考:
- iOS Alamofire:ParameterEncoder.swift#L174-L185
- Android:OkHttp - HttpUrl.kt#L1106-L1123
Header 编码
HTTP Header 本质上是 ASCII 文本。如果你想在 Header 里传中文,要么用 Base64 编码,要么用 RFC 5987 定义的扩展语法。实际开发中,建议 Header 只传 ASCII 安全的内容,复杂数据放 Body。
谁来做? 需要你手动处理。框架不会自动帮你把中文 Base64 编码。
Body 编码
Body 的编码方式决定了应该设置什么 Content-Type,服务端会根据它来选择解码方式:
application/json:UTF-8 编码的 JSON 文本application/x-www-form-urlencoded:类似 URL Query 的键值对multipart/form-data:文件上传常用,每个 part 有独立的编码
谁来做? JSON 序列化需要你手动调用(JSONEncoder / Gson),之后 HTTP 框架会自动设置 Content-Type 并封装到 HTTP 报文中。
源码参考:
- iOS Alamofire:ParameterEncoder.swift#L186-L193
- Android Retrofit:RequestBuilder.java#L195-L202
1.4 常见编码陷阱
空值处理
当参数值为 null 或空字符串时,不同的 HTTP 客户端库和后端框架处理方式差异很大:
- 有的框架会省略整个键值对
- 有的会保留键但值为空:
key= - 有的会传字符串
"null"
以 Alamofire 为例,它提供了三种 nil 编码策略:
public struct NilEncoding {
public static let dropKey = NilEncoding { nil } // 省略整个键值对
public static let dropValue = NilEncoding { "" } // 保留键,值为空
public static let null = NilEncoding { "null" } // 编码为字符串 "null"
}
而 Gson 默认不序列化 null 字段,需要显式调用 serializeNulls() 才会输出:
// 默认行为:{"name":"张三"}(age 字段被省略)
// 调用 serializeNulls() 后:{"name":"张三","age":null}
new GsonBuilder().serializeNulls().create();
建议在团队内明确约定,避免联调时踩坑。
源码参考:
- iOS Alamofire:URLEncodedFormEncoder.swift#L318-L325
- Android Gson:GsonBuilder.java#L235-L239
数字精度
JavaScript 的 Number 类型是 IEEE 754 双精度浮点数,最大安全整数是 2^53 - 1(约 9007 万亿)。超过这个范围的 ID,在 JSON 序列化/反序列化过程中会丢失精度。解决方案是用字符串传递大整数。
时间格式
ISO 8601(如 2025-12-24T10:30:00Z)是最通用的时间格式,但要注意时区问题。有些后端返回的是 Unix 时间戳(秒或毫秒),有些是带时区的字符串,有些是 UTC 时间——联调前务必确认。
1.5 数据链路概览
一个典型的网络请求,数据会经历这样的链路:
理解这个链路有助于定位问题:编码问题在哪一层产生,就应该在哪一层解决。
2. iOS 端:URLSession 与 Alamofire 的编解码逻辑
2.1 URLSession 的默认行为
URL Query 的组装
使用 URLComponents 构建 URL 时,系统会自动进行 Percent-Encoding:
var components = URLComponents(string: "https://api.example.com/users")!
components.queryItems = [
URLQueryItem(name: "name", value: "张三"),
URLQueryItem(name: "age", value: "25")
]
// 结果:https://api.example.com/users?name=%E5%BC%A0%E4%B8%89&age=25
但这里有个坑:URLQueryItem 对某些字符的编码可能不符合预期。比如 + 号默认不会被编码,但在某些后端框架中会被解析为空格。如果遇到这类问题,可能需要手动调用 addingPercentEncoding(withAllowedCharacters:)。
避免二次编码
URLComponents 在设置 queryItems 时会自动进行 Percent-Encoding,所以你传入的值应该是原始值,而不是已编码的值:
// ✅ 正确:传入原始值,URLComponents 自动编码
components.queryItems = [URLQueryItem(name: "name", value: "张三")]
// 结果:name=%E5%BC%A0%E4%B8%89
// ❌ 错误:传入已编码的值,会导致二次编码
let encoded = "张三".addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)!
components.queryItems = [URLQueryItem(name: "name", value: encoded)]
// 结果:name=%25E5%25BC%25A0%25E4%25B8%2589(% 被编码成了 %25)
那什么时候需要手动调用 addingPercentEncoding(withAllowedCharacters:)?
- 直接拼接 URL 字符串时:如果你不用
URLComponents,而是手动拼接 URL,就需要自己编码 URLComponents编码不符合预期时:比如+号默认不编码,但后端会解析为空格,这时需要手动处理- 使用
percentEncodedQuery属性时:这个属性接受已编码的字符串,不会再次编码
处理特殊字符
URLQueryItem 遵循 RFC 3986,但实际开发中有几个字符需要特别注意:
| 字符 | URLQueryItem 是否编码 | 潜在问题 |
|---|---|---|
+ |
❌ 不编码 | 后端可能解析为空格 |
& |
✅ 编码为 %26 |
- |
= |
✅ 编码为 %3D |
- |
# |
✅ 编码为 %23 |
- |
% |
❌ 不编码 | 如果后面跟的不是有效十六进制,可能解析失败 |
| 空格 | ✅ 编码为 %20 |
- |
最常踩坑的是 + 号,因为它在 application/x-www-form-urlencoded 规范中代表空格,而 URLQueryItem 不会编码它。如果你的参数值本身包含 +,需要手动处理:
// 问题场景:value 包含 + 号
components.queryItems = [URLQueryItem(name: "formula", value: "1+1=2")]
// 结果:formula=1+1=2(+ 未编码,后端可能解析为 "1 1=2")
// 解决方案:手动替换 + 为 %2B
let safeValue = "1+1=2".replacingOccurrences(of: "+", with: "%2B")
components.percentEncodedQuery = "formula=\(safeValue)"
// 结果:formula=1%2B1=2(后端正确解析为 "1+1=2")
或者封装一个更通用的方法:
extension String {
var urlQueryEncoded: String {
var allowed = CharacterSet.urlQueryAllowed
allowed.remove("+") // 将 + 从 "允许字符" 中移除,强制编码
return self.addingPercentEncoding(withAllowedCharacters: allowed) ?? self
}
}
// 使用
components.percentEncodedQuery = "formula=\("1+1=2".urlQueryEncoded)"
Body 的 JSON 编码
URLSession 本身不提供 JSON 序列化,需要配合 JSONEncoder:
struct User: Codable {
let name: String
let age: Int
}
let user = User(name: "张三", age: 25)
let jsonData = try JSONEncoder().encode(user)
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.httpBody = jsonData
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
JSONEncoder 有几个常用配置:
keyEncodingStrategy:驼峰转下划线(.convertToSnakeCase)dateEncodingStrategy:时间格式outputFormatting:是否格式化输出(调试用)
Content-Type 的默认处理
URLSession 不会自动设置 Content-Type,需要手动指定。忘记设置的话,服务端可能无法正确解析请求体。
2.2 Alamofire 的默认编码行为
Alamofire 对参数编码做了很好的封装,根据请求方法自动选择编码方式:
| HTTP 方法 | 默认编码方式 | 参数位置 |
|---|---|---|
| GET, HEAD, DELETE | URL Encoding | URL Query |
| POST, PUT, PATCH | JSON Encoding | Request Body |
参数编码策略
Alamofire 提供了三种内置编码器:
// URL 编码 - 参数放在 URL Query
AF.request(url, parameters: params, encoder: URLEncodedFormParameterEncoder.default)
// JSON 编码 - 参数序列化为 JSON Body
AF.request(url, method: .post, parameters: params, encoder: JSONParameterEncoder.default)
// 表单编码 - multipart/form-data
AF.upload(multipartFormData: { formData in
formData.append(imageData, withName: "avatar", fileName: "avatar.jpg", mimeType: "image/jpeg")
}, to: url)
常见配置点与易踩坑
- 数组编码格式:
items[]=1&items[]=2vsitems=1&items=2,不同后端框架有不同偏好 - 布尔值编码:
true/falsevs1/0 - 嵌套对象:
user[name]=张三vsuser.name=张三
Alamofire 的 URLEncodedFormParameterEncoder 提供了 ArrayEncoding 和 BoolEncoding 选项来处理这些差异。
3. 服务端:Jackson 与 Spring 的编解码
在看完客户端如何处理编解码之后,让我们看看服务端是如何处理的。理解全链路有助于排查联调问题。
本章基于 Java 17 与 Spring Boot 3.3.6。即使你没有 Java 开发经验,也能读懂核心逻辑。
3.1 Spring 与 Jackson:谁负责什么
先澄清一个概念:Spring 本身不处理 JSON 编解码,这个工作由 Jackson 完成。
Jackson 是 Java 生态中最流行的 JSON 库,类似于:
- iOS 的
Codable+JSONEncoder/JSONDecoder - Android 的
Gson - JavaScript 的
JSON.stringify/JSON.parse
Spring 是 Web 框架,它的职责是:
- 接收 HTTP 请求,提取参数
- 调用 Jackson 进行 JSON 编解码
- 调用业务逻辑,返回响应
用图来表示:
所以,当你遇到 JSON 相关的问题时,大多数情况下应该去看 Jackson 的文档和配置。
3.2 Jackson 核心机制
Jackson 的核心是 ObjectMapper 类,它提供了两个最重要的方法:
ObjectMapper mapper = new ObjectMapper();
// 序列化:Java 对象 → JSON 字符串
String json = mapper.writeValueAsString(user);
// 反序列化:JSON 字符串 → Java 对象
User user = mapper.readValue(json, User.class);
序列化流程
当调用 writeValueAsString() 时,Jackson 内部会:
- 查找序列化器:根据对象类型,找到对应的
JsonSerializer - 反射获取字段:通过 Java 反射机制,获取对象的所有字段和 getter 方法
- 逐个序列化:对每个字段调用对应的序列化器,生成 JSON 片段
- 组装输出:将所有片段组装成完整的 JSON 字符串
// 简化的序列化流程(源码位置:DefaultSerializerProvider.java#L320)
public void serializeValue(JsonGenerator gen, Object value) {
if (value == null) {
_serializeNull(gen);
return;
}
// 根据对象类型查找序列化器
Class<?> cls = value.getClass();
JsonSerializer<Object> ser = findTypedValueSerializer(cls, true, null);
// 执行序列化
_serialize(gen, value, ser);
}
反序列化流程
当调用 readValue() 时,Jackson 内部会:
- 解析 JSON:将 JSON 字符串解析成 token 流
- 查找反序列化器:根据目标类型,找到对应的
JsonDeserializer - 创建对象实例:调用目标类的无参构造函数(这就是为什么 Jackson 需要无参构造函数)
- 填充字段:根据 JSON 中的 key,找到对应的字段或 setter,设置值
// 简化的反序列化逻辑
public <T> T readValue(String content, Class<T> valueType) {
// 创建 JSON 解析器
JsonParser parser = _jsonFactory.createParser(content);
// 查找反序列化器并执行
return _readMapAndClose(parser, _typeFactory.constructType(valueType));
}
类型映射
Jackson 在序列化/反序列化时,会自动进行类型转换:
| JSON 类型 | Java 类型 |
|---|---|
"string" |
String |
123 |
int, long, Integer, Long |
12.34 |
double, float, Double, Float, BigDecimal |
true/false |
boolean, Boolean |
null |
null(引用类型) |
[...] |
List, Set, 数组 |
{...} |
Map, 自定义对象 |
3.3 Spring 如何调用 Jackson
Spring 通过 HttpMessageConverter 机制来处理请求和响应的转换。对于 JSON,使用的是 MappingJackson2HttpMessageConverter。
@RequestBody 的处理流程
当 Spring 遇到 @RequestBody 注解时:
@PostMapping("/users")
public User createUser(@RequestBody User user) {
return userService.save(user);
}
内部处理流程:
- Spring 检查请求的
Content-Type,发现是application/json - 选择
MappingJackson2HttpMessageConverter来处理 - Converter 内部调用 Jackson 的
ObjectMapper.readValue()进行反序列化
// AbstractJackson2HttpMessageConverter.java(简化)
@Override
protected Object readInternal(Class<?> clazz, HttpInputMessage inputMessage) {
JavaType javaType = getJavaType(clazz, null);
return readJavaType(javaType, inputMessage);
}
private Object readJavaType(JavaType javaType, HttpInputMessage inputMessage) {
ObjectMapper objectMapper = selectObjectMapper(clazz, contentType);
ObjectReader objectReader = objectMapper.readerFor(javaType);
// 调用 Jackson 进行反序列化
return objectReader.readValue(inputMessage.getBody());
}
@ResponseBody 的处理流程
当方法返回值需要转换为 JSON 时(@RestController 默认包含 @ResponseBody):
- Spring 检查客户端的
Accept头,确定返回格式 - 选择
MappingJackson2HttpMessageConverter来处理 - Converter 内部调用 Jackson 的
ObjectMapper.writeValue()进行序列化
// AbstractJackson2HttpMessageConverter.java(简化)
@Override
protected void writeInternal(Object object, HttpOutputMessage outputMessage) {
ObjectMapper objectMapper = selectObjectMapper(object.getClass(), contentType);
ObjectWriter objectWriter = objectMapper.writer();
// 调用 Jackson 进行序列化
objectWriter.writeValue(generator, object);
}
3.4 Jackson 常用配置
Jackson 是 Spring 默认的 JSON 库,它的行为可以通过注解或全局配置来调整。
空值处理:@JsonInclude
Jackson 默认会序列化所有字段,包括 null:
public class User {
private String name = "张三";
private String email = null;
}
// 默认输出:{"name":"张三","email":null}
如果你想省略 null 字段(和 Gson 默认行为一致),可以使用 @JsonInclude:
@JsonInclude(JsonInclude.Include.NON_NULL)
public class User {
private String name = "张三";
private String email = null;
}
// 输出:{"name":"张三"}
@JsonInclude 的常用选项:
| 选项 | 行为 |
|---|---|
ALWAYS |
始终包含(默认) |
NON_NULL |
排除 null 值 |
NON_EMPTY |
排除 null、空字符串、空集合 |
NON_DEFAULT |
排除等于默认值的字段 |
命名策略:驼峰 vs 下划线
Java 习惯用驼峰命名(userName),而很多 API 规范要求下划线(user_name)。Jackson 提供了 PropertyNamingStrategies:
// 全局配置(Spring Boot application.yml)
spring:
jackson:
property-naming-strategy: SNAKE_CASE
// 或者单个类配置
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
public class User {
private String userName; // 序列化为 "user_name"
}
时间格式
Jackson 默认把 Date 序列化为时间戳(毫秒),如果你想要 ISO 8601 格式:
// 全局配置
spring:
jackson:
date-format: yyyy-MM-dd'T'HH:mm:ss.SSSZ
time-zone: UTC
// 或者单个字段配置
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
private Date createTime;
3.5 URL Query 与字符编码
前面讲的都是 JSON Body 的编解码,由 Jackson 负责。但 URL Query 的解码和 Jackson 无关,这部分由 Servlet 容器(如 Tomcat)和 Spring 共同完成。
解码流程概览
当客户端发送 GET /users?name=%E5%BC%A0%E4%B8%89&age=25 时,解码流程如下:
Tomcat 的参数解码
Tomcat 在首次调用 request.getParameter() 时才会解析参数(延迟解析)。核心逻辑在 Parameters 类中:
// Tomcat Parameters.java(简化)
private void processParameters(byte[] bytes, int start, int len, Charset charset) {
int pos = start;
int end = start + len;
while (pos < end) {
// 找到 key=value 对
int nameStart = pos;
int nameEnd = -1;
int valueStart = -1;
int valueEnd = -1;
// 解析 key 和 value 的边界
while (pos < end) {
byte b = bytes[pos];
if (b == '=' && nameEnd == -1) {
nameEnd = pos;
valueStart = pos + 1;
} else if (b == '&') {
valueEnd = pos;
break;
}
pos++;
}
// 对 key 和 value 进行 URL 解码
// %E5%BC%A0 → 0xE5 0xBC 0xA0 → "张"(UTF-8)
String name = urlDecode(bytes, nameStart, nameEnd, charset);
String value = urlDecode(bytes, valueStart, valueEnd, charset);
addParameter(name, value);
}
}
Spring 的 @RequestParam 处理
Spring 通过 RequestParamMethodArgumentResolver 处理 @RequestParam 注解:
@GetMapping("/users")
public List<User> getUsers(
@RequestParam String name, // 自动解码为 "张三"
@RequestParam int age // 自动转换为整数 25
) {
// ...
}
Spring 内部的处理逻辑:
// RequestParamMethodArgumentResolver.java(简化)
@Override
protected Object resolveName(String name, MethodParameter parameter,
NativeWebRequest request) throws Exception {
HttpServletRequest servletRequest = request.getNativeRequest(HttpServletRequest.class);
// 调用 Servlet API 获取参数(此时 Tomcat 已完成解码)
String[] paramValues = servletRequest.getParameterValues(name);
if (paramValues != null) {
return paramValues.length == 1 ? paramValues[0] : paramValues;
}
return null;
}
CharacterEncodingFilter 的作用
Spring Boot 默认配置了 CharacterEncodingFilter,确保 Tomcat 使用 UTF-8 解码参数:
// CharacterEncodingFilter.java(简化)
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) {
String encoding = getEncoding(); // 默认 UTF-8
// 在 Tomcat 解析参数之前设置编码
if (isForceRequestEncoding() || request.getCharacterEncoding() == null) {
request.setCharacterEncoding(encoding);
}
if (isForceResponseEncoding()) {
response.setCharacterEncoding(encoding);
}
filterChain.doFilter(request, response);
}
为什么顺序很重要?
CharacterEncodingFilter 必须在参数解析之前执行,否则 Tomcat 会使用默认编码(通常是 ISO-8859-1)解码参数,导致中文乱码。Spring Boot 默认将这个 Filter 配置为最高优先级,所以一般不会遇到问题。
如果遇到 URL Query 中文乱码,检查以下几点:
CharacterEncodingFilter是否正确配置- Tomcat 的
URIEncoding配置(server.xml中的 Connector 配置) - 客户端是否使用 UTF-8 编码
3.6 常见问题排查
问题 1:客户端发送的 JSON 字段,后端收到是 null
可能原因:
- 命名不一致:客户端发
user_name,后端字段是userName,且没配置SNAKE_CASE - 类型不匹配:客户端发字符串
"25",后端期望int(Jackson 通常能自动转换,但复杂类型可能失败) - 缺少无参构造函数:Jackson 反序列化需要无参构造函数
问题 2:后端返回的 JSON 字段顺序和预期不同
Jackson 默认不保证字段顺序。如果有顺序要求,使用 @JsonPropertyOrder:
@JsonPropertyOrder({"id", "name", "email"})
public class User { ... }
问题 3:前后端空值约定不一致
这是联调中最常见的问题。建议团队统一约定:
| 场景 | 客户端行为 | 后端行为 |
|---|---|---|
| 字段不传 | 不包含该 key | 使用默认值或判断为 null |
| 传 null | "field": null |
接收为 null |
| 传空字符串 | "field": "" |
接收为空字符串(注意:不等于 null) |
3.7 与客户端编码的对应关系
| 客户端操作 | 后端处理 |
|---|---|
URLComponents 编码中文 |
@RequestParam 自动 URL 解码 |
JSONEncoder 序列化对象 |
Jackson 反序列化为 Java 对象 |
设置 Content-Type: application/json |
Spring 选择 MappingJackson2HttpMessageConverter |
Alamofire JSONParameterEncoder |
等同于上述组合 |
理解了这个对应关系,联调时遇到问题就知道该从哪边排查了。
4. 延伸阅读:JS Bridge 的编解码逻辑
如果你对 Native 与 H5 之间的 JS Bridge 通信机制感兴趣,可以阅读这篇独立的解析文章:
👉 iOS & Android & H5 三端 JS Bridge 的编解码逻辑与源码分析
这篇文章从源码层面拆解了 JS Bridge 的编解码逻辑,包括:
- 消息结构与回调机制
- iOS(marcuswestin/WebViewJavascriptBridge)的实现细节
- Android(wendux/WebViewJavascriptBridge)的实现细节
- H5 端的 JavaScript 注入与消息处理
- 三端的差异与注意点
5. 总结
5.1 全链路编解码概览
下图展示了一个完整的 HTTP 请求-响应周期中,数据经历的编解码过程:
5.2 按请求类型的编解码对照
不同的请求类型,编解码的方式和负责人不同:
GET 请求(参数在 URL Query)
| 阶段 | 位置 | 负责人 | 自动/手动 | 说明 |
|---|---|---|---|---|
| 客户端编码 | URL Query | URLComponents / Uri.Builder |
✅ 自动 | Percent-Encoding |
| 服务端解码 | URL Query | Tomcat(Servlet 容器) | ✅ 自动 | request.getParameter() 时解码 |
POST / PUT / PATCH 请求(JSON Body)
| 阶段 | 位置 | 负责人 | 自动/手动 | 说明 |
|---|---|---|---|---|
| 客户端编码 | Body | JSONEncoder / Gson |
⚠️ 手动调用 | 需显式调用序列化方法 |
| 服务端解码 | Body | Jackson | ✅ 自动 | @RequestBody 触发自动反序列化 |
| 服务端编码 | Body | Jackson | ✅ 自动 | @ResponseBody 触发自动序列化 |
| 客户端解码 | Body | JSONDecoder / Gson |
⚠️ 手动调用 | 需显式调用反序列化方法 |
POST 请求(Form URL-Encoded)
| 阶段 | 位置 | 负责人 | 自动/手动 | 说明 |
|---|---|---|---|---|
| 客户端编码 | Body | HTTP 框架(Alamofire / OkHttp) | ✅ 自动 | 类似 URL Query 的编码 |
| 服务端解码 | Body | Tomcat(Servlet 容器) | ✅ 自动 | 与 URL Query 处理方式相同 |
5.3 编解码责任速查表
| 数据位置 | 客户端编码 | 服务端解码 | 服务端编码 | 客户端解码 |
|---|---|---|---|---|
| URL Query | URLComponents | Tomcat | - | - |
| JSON Body | JSONEncoder/Gson | Jackson | Jackson | JSONDecoder/Gson |
| Form Body | HTTP 框架 | Tomcat | - | - |
| Header | 手动处理 | 手动处理 | 手动处理 | 手动处理 |
💡 记忆技巧:URL 相关的编解码由「URL 处理组件」负责(URLComponents、Tomcat),JSON 相关的由「JSON 库」负责(JSONEncoder、Jackson、Gson)。
5.4 统一编码规范的价值
通过这篇文章的分析,我们可以看到:编解码问题往往不是技术问题,而是约定问题。
无论是客户端与服务端的 HTTP 接口,只要各端约定好:
- 字符集统一用 UTF-8
- 时间格式统一用 ISO 8601 或时间戳
- 空值统一用
null还是省略 - 大整数统一用字符串
就能避免绝大多数联调问题。
至此,就是这篇文章的全部内容了。希望这篇文章能帮你建立起对 HTTP 编码的整体认识。下次遇到「乱码」、「字段丢失」这类问题时,希望你能知道该从哪里开始排查了。