作为开发者,我们每天都在和网络请求打交道。但你有没有想过:当 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 会自动编码大部分字符,但 + 等边界字符可能需要手动处理。

源码参考:

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 报文中。

源码参考:

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

建议在团队内明确约定,避免联调时踩坑。

源码参考:

数字精度

JavaScript 的 Number 类型是 IEEE 754 双精度浮点数,最大安全整数是 2^53 - 1(约 9007 万亿)。超过这个范围的 ID,在 JSON 序列化/反序列化过程中会丢失精度。解决方案是用字符串传递大整数。

时间格式

ISO 8601(如 2025-12-24T10:30:00Z)是最通用的时间格式,但要注意时区问题。有些后端返回的是 Unix 时间戳(秒或毫秒),有些是带时区的字符串,有些是 UTC 时间——联调前务必确认。

1.5 数据链路概览

一个典型的网络请求,数据会经历这样的链路:

HTTP 数据链路

理解这个链路有助于定位问题:编码问题在哪一层产生,就应该在哪一层解决。

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:)

  1. 直接拼接 URL 字符串时:如果你不用 URLComponents,而是手动拼接 URL,就需要自己编码
  2. URLComponents 编码不符合预期时:比如 + 号默认不编码,但后端会解析为空格,这时需要手动处理
  3. 使用 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)

常见配置点与易踩坑

  1. 数组编码格式items[]=1&items[]=2 vs items=1&items=2,不同后端框架有不同偏好
  2. 布尔值编码true/false vs 1/0
  3. 嵌套对象user[name]=张三 vs user.name=张三

Alamofire 的 URLEncodedFormParameterEncoder 提供了 ArrayEncodingBoolEncoding 选项来处理这些差异。

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 编解码
  • 调用业务逻辑,返回响应

用图来表示:

Spring 与 Jackson 协作流程

所以,当你遇到 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 内部会:

  1. 查找序列化器:根据对象类型,找到对应的 JsonSerializer
  2. 反射获取字段:通过 Java 反射机制,获取对象的所有字段和 getter 方法
  3. 逐个序列化:对每个字段调用对应的序列化器,生成 JSON 片段
  4. 组装输出:将所有片段组装成完整的 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);
}

源码参考:DefaultSerializerProvider.java#L321-L342

反序列化流程

当调用 readValue() 时,Jackson 内部会:

  1. 解析 JSON:将 JSON 字符串解析成 token 流
  2. 查找反序列化器:根据目标类型,找到对应的 JsonDeserializer
  3. 创建对象实例:调用目标类的无参构造函数(这就是为什么 Jackson 需要无参构造函数)
  4. 填充字段:根据 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);
}

内部处理流程:

  1. Spring 检查请求的 Content-Type,发现是 application/json
  2. 选择 MappingJackson2HttpMessageConverter 来处理
  3. 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());
}

源码参考:AbstractJackson2HttpMessageConverter.java#L357-L408

@ResponseBody 的处理流程

当方法返回值需要转换为 JSON 时(@RestController 默认包含 @ResponseBody):

  1. Spring 检查客户端的 Accept 头,确定返回格式
  2. 选择 MappingJackson2HttpMessageConverter 来处理
  3. 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);
}

源码参考:AbstractJackson2HttpMessageConverter.java#L439-L487

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 排除等于默认值的字段

源码参考:JsonInclude.java#L113-L241 - Include 枚举

命名策略:驼峰 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 时,解码流程如下:

URL Query 解码流程

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

源码参考:Tomcat Parameters.java#L234-L463

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;
}

源码参考:RequestParamMethodArgumentResolver.java#L160-L190

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.java#L187-L202

为什么顺序很重要?

CharacterEncodingFilter 必须在参数解析之前执行,否则 Tomcat 会使用默认编码(通常是 ISO-8859-1)解码参数,导致中文乱码。Spring Boot 默认将这个 Filter 配置为最高优先级,所以一般不会遇到问题。

如果遇到 URL Query 中文乱码,检查以下几点:

  1. CharacterEncodingFilter 是否正确配置
  2. Tomcat 的 URIEncoding 配置(server.xml 中的 Connector 配置)
  3. 客户端是否使用 UTF-8 编码

3.6 常见问题排查

问题 1:客户端发送的 JSON 字段,后端收到是 null

可能原因:

  1. 命名不一致:客户端发 user_name,后端字段是 userName,且没配置 SNAKE_CASE
  2. 类型不匹配:客户端发字符串 "25",后端期望 int(Jackson 通常能自动转换,但复杂类型可能失败)
  3. 缺少无参构造函数: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 的编解码逻辑,包括:

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 编码的整体认识。下次遇到「乱码」、「字段丢失」这类问题时,希望你能知道该从哪里开始排查了。