qinghuan
← 从零建立基础 / 全部笔记

单元 04 / 从零建立基础

P0 · 必会主线L0 · 零基础

看懂 HTTP 请求、JSON 与 API 错误

接模型前先看清请求地址、身份、正文和响应,再处理超时、错误与格式不合格的情况。

本篇目录 · 按需要跳转

读完这篇,你能做什么

能读取请求响应示例,解析 JSON,并对四类错误输出明确且脱敏的诊断。

学习理由、准备要求与知识点

为什么学它

模型调用首先是远程接口调用,必须区分网络、HTTP、解析与业务四层失败。

先读这些内容

知识点清单

  • API 契约
  • HTTP 方法与地址
  • 请求头与请求体
  • 状态码和响应正文
  • JSON 基本类型
  • 序列化与反序列化
  • 认证凭证与业务参数
  • 超时和错误分层

运行前先确认完整算法使用 Java 17 标准库;JDK 源码基线为 OpenJDK 17 GA。示例是内存中的机制模型,外部服务接入需要另外配置和验证。

01先弄懂它是什么

程序要把学习问题发给另一个程序,双方先得约好地址、请求方法、身份信息、正文格式和错误含义,这就是 API 约定。HTTP 负责传递请求和响应,JSON 是正文可以使用的一种数据格式,它们各有用途。网络连上了,只说明通信具备条件,还要检查服务端是否真正完成了业务处理。

Java 对象不能原样变成网络传输内容,要先序列化成约定格式。对方收到字节后,再按字符编码解码、解析 JSON,并映射成 DTO。JSON 只有对象、数组、字符串、数字、布尔值和 null 等基本结构;一个字段叫 id,不会自动保证唯一,数字能解析出来,也不代表金额或数量范围合法,这些都要由业务代码检查。

HTTP状态码、响应头和正文要一起阅读。认证失败、限流、服务端错误与连接超时属于不同层级;返回200但正文含业务拒绝也可能是接口自己的约定。令牌放在哪个头、允许哪些字段、是否分页,都应来自明确契约,不能靠客户端猜测。

文中这几个词,先认一认

序列化
把程序里的对象转换成可传输或保存的形式。本篇使用 JSON 文本,再编码为 UTF-8 字节。
消息边界
一条完整业务消息从哪里开始、在哪里结束。网络一次传来的数据块,可能只是其中一部分。
契约
通信双方约好的字段、类型、状态和错误含义,决定怎样正确发送与处理数据。

还有陌生词?到术语小抄里看一个具体例子 ↗

02看看它怎么工作

一次响应可能分多次到达,不能对每块字节直接调用JSON解析器并认为它是完整文档。甚至一个中文字符的UTF-8编码也可能跨块,字节解码器应保留未完成字节。本篇算法从已经正确解码的字符块开始,只演示连接JSON对象的边界识别,不处理字节解码。

边界识别维护括号栈、是否位于字符串内、前一个字符是否转义。在字符串内部,花括号只是内容;在字符串外,开括号入栈,匹配的闭括号出栈。最外层对象闭合后才能输出完整文本。这个状态跨块保留,因此分块点落在引号或反斜线之后也不改变最终结果。

找到边界仍不等于JSON合法。文本{bad}具有成对花括号,却不符合对象语法;算法会先把它作为一条候选消息交给真正的JSON库,再由JSON库拒绝。把划分消息边界(分帧)、解析JSON语法、转换数据结构和校验业务规则分开,才能知道错误出在哪一步,也方便逐层测试。

正在加载流程图…

图中状态只描述本篇可执行机制;业务调用仍需由外层应用负责。
查看流程图源码(Mermaid)
flowchart LR
  B["网络字节块"] --> D["增量 UTF-8 解码"]
  D --> F["状态机识别帧"]
  F --> J["JSON 语法解析"]
  J --> T["DTO 和业务校验"]
  T --> O["成功或分层错误"]

按顺序看,每一步做了什么

  1. 按API文档构造方法、地址、头和JSON请求正文。
  2. 序列化为UTF-8字节,配置连接及整体请求边界。
  3. 增量解码收到的字节,再识别完整消息边界。
  4. 把完整JSON交给成熟解析器并映射DTO。
  5. 结合状态码、字段校验和业务规则决定成功或错误。

03对照实现代码

OpenJDK 17 HttpRequest 源码 (新标签页)↗

OpenJDK 17 GA,jdk-17+35 · HttpRequest.BodyPublishers.ofString

下方为原创 Java 教学片段,调用已核验的 JDK API 说明局部机制;不是该类原样源码,也不实现神经网络。完整程序见算法模块。

核心机制代码 · java
String json = "{\"question\":\"什么是索引\"}";
// ofString 默认以 UTF-8 编码;它不负责验证字符串是否是合法 JSON。
HttpRequest.BodyPublisher body = HttpRequest.BodyPublishers.ofString(json);
// 请求构建、认证、发送与响应解析是另外的调用步骤。

ofString(String)的真实实现转到指定UTF-8的重载,再创建字符串请求体发布器。它处理正文编码和发布,不负责理解question字段。

BodyPublisher存在不说明请求已经发送。构造请求还需URI、方法和头,再由HttpClient执行;构建时异常和网络执行异常应该分层处理。

Content-Type说明媒体类型,不会使任意字符串自动变成JSON。正文应来自可靠序列化器,手工拼接用户输入容易漏掉引号、反斜线和控制字符转义。

本篇Java桥梁是HTTP客户端入口和字符分帧状态机。它没有实现TLS、HTTP协议栈或完整JSON解析器,真实服务集成仍应使用成熟库和明确接口契约。

代码先调用谁,再调用谁

  1. DTO交给JSON序列化器
  2. BodyPublishers.ofString编码正文
  3. HttpRequest构造请求元数据
  4. HttpClient发送并接收响应
  5. 解码、解析及业务校验

04动手写一个例子

跨字符块的JSON对象分帧

输入已正确解码的字符块列表,其中顶层只允许连续JSON对象。输出每个完整对象文本;检测括号不匹配或截断,但不验证完整JSON语法。保存Main.java后以Java17编译和运行自测。

输入 1
字符块{"a": 与 1}{"b":2}
输出 1
两个帧:{"a":1}、{"b":2}
输入 2
空白字符块
输出 2
空列表

栈存储当前尚未闭合的对象或数组起始符,因此遇到闭合符时可以同时检查类型和嵌套顺序。仅计算花括号数量会漏掉数组与对象交叉闭合。

字符串状态优先于括号判断,转义状态优先于结束引号判断。遇到反斜线后,下一个字符作为转义内容处理,然后清除标记,不把其中的引号视为字符串结束。

每个字符只进入当前帧一次。外层栈清空时立即输出并重置缓冲区;所有块读完仍有未闭合状态则报截断。没有消息结束信号前,短暂空读不能被当成截断。

为什么这样写是对的处理任意字符前缀后,栈和字符串标记完整描述当前未结束帧的结构状态;已输出列表只包含完整闭合的顶层对象,字符顺序保持不变。

时间 O(C)额外空间 O(C+d)

C为全部字符数,d为最大嵌套深度。保留全部输出文本需要O(C),若逐帧交付则工作内存为最大帧长度加O(d);真实实现还应限制最大帧与嵌套深度。

保存为 Main.java,运行 javac --release 17 -encoding UTF-8 Main.java,再运行 java Main。程序包含固定输入自测,断言失败时会抛出错误。

展开收起代码 · Main.java · 完整可运行程序Java · 39 行

长行可在代码区域内左右滚动,按 Tab 可把焦点移入代码区。

Main.java · 完整可运行程序 · Java
import java.util.*;
public class Main {
    static List<String> frames(List<String> chunks){
        List<String> out=new ArrayList<>(); Deque<Character> stack=new ArrayDeque<>();
        StringBuilder b=new StringBuilder(); boolean in=false,escape=false;
        for(String chunk:chunks)for(char c:chunk.toCharArray()){
            if(stack.isEmpty()){
                if(Character.isWhitespace(c))continue;
                if(c!='{')throw new IllegalArgumentException("object expected");
            }
            b.append(c);
            if(in){
                if(escape)escape=false; // 转义后的引号不能结束字符串。
                else if(c==92)escape=true; // 92 是反斜线字符。
                else if(c=='"')in=false;
                continue;
            }
            if(c=='"'){in=true;continue;}
            if(c=='{'||c=='[')stack.push(c);
            else if(c=='}'||c==']'){
                if(stack.isEmpty()||stack.pop()!=(c=='}'?'{':'['))throw new IllegalArgumentException("mismatch");
                if(stack.isEmpty()){out.add(b.toString());b.setLength(0);}
            }
        }
        if(in||!stack.isEmpty())throw new IllegalArgumentException("truncated");
        return out;
    }
    static void check(List<String> expected,List<String> actual){if(!expected.equals(actual))throw new AssertionError(actual);}
    static void rejects(String text){try{frames(List.of(text));throw new AssertionError();}catch(IllegalArgumentException expected){}}
    public static void main(String[] args){
        check(List.of("{\"a\":1}","{\"b\":2}"),frames(List.of("{\"a\":","1}{\"b\":2}")));
        check(List.of("{\"x\":\"}\"}"),frames(List.of("{\"x\":\"}","\"}")));// 字符串中的括号。
        check(List.of("{\"x\":\"a\\\"b\"}"),frames(List.of("{\"x\":\"a\\","\"b\"}")));
        check(List.of(),frames(List.of("  ","\n")));
        rejects("{\"a\":[}"); rejects("{\"a\":1");
        check(List.of("{bad}"),frames(List.of("{bad}")));// 分帧器不冒充语法解析器。
        System.out.println("PASS 7 tests");
    }
}

这些特殊情况也要试一试

  • 字符串内花括号不影响结构栈。
  • 反斜线和转义引号跨块时必须保持状态。
  • 顶层数组或标量不在本题契约内,不能误称支持全部JSON流。
进阶选读 · 后续再读:受资源约束的流式协议解析

场景攻击者发送无限长字符串或极深嵌套,客户端缓冲区持续增长。

做法在解码和分帧两层分别设置字节、字符、嵌套与整体时间限制;取消时释放缓冲与下游任务,并将半成品标记为不完整。

复杂度扫描仍为O(C),受控工作空间O(B+d),B为最大允许帧长;达到限制时明确失败,不能继续无限累积。

好处与代价缓冲越大越能容纳合法大结果,但内存占用和拒绝服务风险越高;分页与分段DTO常比单个巨大JSON更可控。

05项目里怎么用

HTTP重试必须考虑方法与业务语义。超时可能发生在服务端已经写入之后,重新发送创建请求会重复行动;应使用服务端支持的幂等键或查询结果,而不是看到超时就循环发送。认证失败通常需要修正身份,参数错误需要修正输入,退避无法自动解决这两类问题。

日志保留请求标识、耗时、状态与安全截断后的错误摘要,不直接记录认证头或整份敏感正文。客户端错误页可能是HTML而不是JSON,应先检查媒体类型与状态再决定如何解析,避免把真实网关错误掩盖成一个含糊的JSON解析异常。

可以先用内存里的响应样本测试客户端:正常内容、缺字段、多字段、错误状态、内容截断和慢响应都各准备一份。它能证明客户端如何处理这些已知情况,但没有验证真实网络、凭证和远端权限。正式集成还要检查实际发出和收到的内容,并保留不含敏感信息的请求关联标识。

容易踩的坑

  • 把一个网络块等同于一个JSON文档。
  • 将括号匹配成功当作完整JSON合法性证明。
  • 手工拼接用户输入,遗漏字符串转义。
  • 超时后一律重试写请求,忽略结果未知。

几种做法,怎么选

表格有四列,窄屏可左右滑动查看代价和适用条件。

方案优势代价与限制适用条件
整份响应缓冲解析简单,容易得到完整DTO大响应占内存且展示较晚有明确响应上限的接口
逐帧解析可早处理并限制单帧内存需维护边界和取消状态流式事件与长任务
成熟序列化库正确处理类型与转义仍需业务验证和配置正式API输入输出

06检验是否读懂了

先自己想、自己写,再展开答案核对。场景题是假设的练习情境;遇到还没学过的内容,可以做完基础后再回来。

先用自己的话解释

01JSON与HTTP是什么关系?

展开参考答案与推导

HTTP是通信协议,JSON是正文表示形式之一;HTTP可以传文本、图片等,JSON也能存到文件。

02状态码成功为什么还要校验正文?

展开参考答案与推导

传输层或接口层成功不保证字段符合业务规则,正文也可能使用约定的业务错误结构,必须按契约解释。

03分帧器为何接受{bad}?

展开参考答案与推导

它只找对象边界与检查结构闭合,不检查键名引号和冒号语法;该文本随后必须由JSON解析器拒绝。

换个情况,还会处理吗

01中文字符偶尔乱码,应先改模型提示吗?

展开参考答案与推导

先检查字节到字符的增量解码和编码一致性;网络块可能切断UTF-8字符,逐块独立解码会造成替换字符。

02服务端返回HTML错误页怎么办?

展开参考答案与推导

保留状态、媒体类型和脱敏摘要,归类为网关或服务错误,不强行映射业务DTO;再结合请求标识排查。

03创建操作超时,能直接重发吗?

展开参考答案与推导

先查幂等机制与操作结果。超时不说明服务端未执行;缺少安全重试条件时应保留结果未知并核对。

写代码试一试

01添加嵌套数组测试

输入 / 条件
{"a":[{"b":1}]}
输出 / 预期
一个完整帧

提示 两种起始括号入同一栈。

展开参考答案与推导

每个闭合符匹配栈顶类型,最外层对象退出后输出;不能在内部对象闭合时提前输出。

02怎样限制超长输入?

输入 / 条件
最大帧10字符,输入超过10
输出 / 预期
拒绝并停止累积

提示 在追加字符时检查。

展开参考答案与推导

对单帧缓冲计数,超过上限立即失败;还应限制字符解码前的字节量,避免先分配巨大字符串再检查。

03为什么最后一块结束才检查截断?

输入 / 条件
第一块{"a":,第二块1}
输出 / 预期
完整帧

提示 块结束不等于消息结束。

展开参考答案与推导

中途保留状态等待后续字符;只有确认输入流结束仍有未闭合结构时才报告截断。

读完之后,独立完成

课后作业:自己做一遍

本篇共 10 道作业,先完成 8 道基础与综合任务,再按前置知识挑战进阶题。其中包含 1 道思维拓展,试着换个条件,看看原来的办法是否还管用。不附参考答案。

这次练什么

能读取请求响应示例,解析 JSON,并对四类错误输出明确且脱敏的诊断。

本篇重点加练

接口契约与错误层次决定客户端如何理解失败,需要区分观察证据和推测。

  • API 契约
  • 超时和错误分层

重点内容会多练几次。先说清楚怎么做,再试试空数据、出错或条件改变时会发生什么。

先自己做,卡住时记下问题,再回来查。问答题写清“为什么、什么时候适用”;动手题留下输入、输出和运行步骤,方便下次重做。

基础必做

  1. 作业 01原理问答

    为查询笔记和新增笔记各写一份接口契约,说明方法、地址、参数位置和重复请求行为,不把URL名称当作完整契约。

    提交内容
    一份独立书面回答或标注图,包含自造例子、概念边界及判断依据。
    怎样算完成
    • 逐项回应题目要求,区分事实、假设与个人判断。
    • 能用自己的例子解释适用条件,记录尚未弄懂的点。

    练到的知识点API 契约 · HTTP 方法与地址

  2. 作业 02代码推演

    给出状态码200但正文是业务错误、状态码401、连接超时三条记录,逐项标记请求头、响应头、正文和缺失的证据。

    提交内容
    手工推演记录,写明输入约定、中间状态、最终判断;可运行时附程序对照记录。
    怎样算完成
    • 推演包含中间过程,数据形状、单位、顺序或状态前后一致。
    • 主动检查至少一个边界或异常条件,能解释手算与运行差异。

    练到的知识点请求头与请求体 · 状态码和响应正文

  3. 作业 03原理问答

    为“没有收到响应”写一份故障分类表,列出客户端能够直接观察的事实与仍需服务器记录才能确认的事项。

    提交内容
    概念或合同说明、关系图与自造案例,明确事实、假设和适用条件。
    怎样算完成
    • 关键概念有明确边界,能用案例检验声明。
    • 未获证据支持的假设单独标注。

    练到的知识点API 契约 · 超时和错误分层

综合应用

  1. 作业 04面试场景

    将字符串"12"、数值12、null、空数组和缺失字段送入自定JSON契约,推演序列化与反序列化后应用应如何判断类型。

    提交内容
    场景分析与决策记录,包含信息缺口、约束、备选方案和验证计划。
    怎样算完成
    • 方案回应给定约束,列出至少一种替代方案及代价。
    • 每项关键结论说明所需证据,不能核实的部分明确标注。

    练到的知识点JSON 基本类型 · 序列化与反序列化

  2. 作业 05编程实践

    使用代码中的预制响应或本地文本样本,模拟认证缺失、参数非法、处理超时和服务异常。编写小型解析与分类脚本,凭证仅用占位值,提交响应正文、协议状态与错误类别;不要求实现HTTP服务器。

    提交内容
    可复现的最小程序或实验材料、自写测试、环境说明与运行记录;题目允许纸面实验时提交完整案例表。
    怎样算完成
    • 提交物按说明可以重现;测试覆盖正常、边界与失败场景。
    • 运行证据与断言能核验声明的行为,注明模拟与真实执行范围。

    练到的知识点认证凭证与业务参数 · 超时和错误分层

  3. 作业 06排错实验

    制作Content-Type不匹配与截断JSON的两类预制响应,使用小脚本核验正文能否解析,再结合协议记录分析失败可能位于哪一层;区分实际观察与尚需服务器证据的判断。

    提交内容
    最小故障输入、复现步骤、观测记录、定位依据及回归用例。
    怎样算完成
    • 其他人可按记录复现故障,并区分现象与已验证原因。
    • 定位过程有观测依据,回归用例可以再次暴露原问题。

    练到的知识点HTTP 方法与地址 · 请求头与请求体 · 序列化与反序列化

  4. 作业 07编程实践

    为本地预制的接口v1、v2记录编写同一组客户端验收测试:自行指定两版的字段类型、错误码与超时语义,让部分合同保持、部分改变;检查哪些使用方式仍兼容,不要求实现服务器。

    提交内容
    可复现的小程序或题目允许的离线实验、自写测试、输入和运行记录。
    怎样算完成
    • 输入、环境和运行过程可重现,测试覆盖声明的重点条件。
    • 验证目标由合同确定,模拟结果不冒充真实服务证据。

    练到的知识点API 契约 · 超时和错误分层

  5. 作业 08面试场景

    同一查询接口在浏览器能成功、脚本超时。制定逐层比对清单,限定只比较方法、地址、头、参数与时限,不把网络现象直接归因为业务代码错误。

    提交内容
    场景评审记录,包含约束、备选方案、未决信息和验证计划。
    怎样算完成
    • 方案回应题目约束,取舍能对应到可核验的事实。
    • 给出失败或条件变化时的检查方式。

    练到的知识点API 契约 · 超时和错误分层

进阶挑战 · 掌握前置后选做

  1. 作业 09设计评审

    在掌握基础后设计一个统一错误响应格式,保留协议状态与业务错误的区分,并评审它是否能支撑前端重试和问题定位。

    提交内容
    设计评审文档或对照实验报告,包含前置条件、方案取舍、验收证据与尚未验证的范围。
    怎样算完成
    • 比较使用一致条件与事先声明的标准,记录失败与未知结果。
    • 说明方案的适用前提、未覆盖风险及后续验证方法。

    练到的知识点API 契约 · 状态码和响应正文 · 超时和错误分层

思维拓展

先写出你的猜想,再用例子或实验验证。答案可以不同,但要说清依据、适用情况,以及还有什么没弄懂。

  1. 作业 10思维拓展 · 跨域迁移

    将协议错误与业务错误分层的思想迁移到本地命令行工具,设计退出码、标准输出与业务结果之间的合同,并给出可区分的样本。

    提交内容
    原场景与目标场景的对应表、可迁移的机制、不能迁移的前提及验证设计。
    怎样算完成
    • 逐项核对两种场景的前提,不只比较相似术语。
    • 至少一个不能直接迁移的边界有具体检验方法。

    练到的知识点API 契约 · 超时和错误分层

完成后,再检验一次

给每题标记“独立完成 / 查阅后完成 / 尚未完成”。查过资料或没做出的题,记下卡在哪里。隔一天换一组输入再试,能解释结果和出错原因,再把这个知识点标为掌握。

继续阅读

先看能帮你完成当前练习的部分。例子跑通后,再读更深入的解释。

打开资料库,找下一份学习材料 ↗