读完这篇,你能做什么
能写清订单查询接口的参数、响应和错误约定(接口契约),并用例子验证文档与实际行为一致。
学习理由、准备要求与知识点
为什么学它
接口约定连接前后端与测试,明确参数、错误和版本比只生成一个文档页面更重要。
先读这些内容
知识点清单
OpenAPI3.1- 路径参数
- Schema
- required 与 nullable
- 响应状态
- 错误结构
- 安全方案声明
- 契约与运行时校验
运行前先确认OpenAPI 3.1.1、JSON Schema 2020-12 与 Java 17。算法只覆盖文中固定字段,输入已按约定解析为 Map,不是完整 JSON Schema 引擎,未执行真实 HTTP 或授权集成。BigDecimal 源码核验 Microsoft 17.0.20.1,上游 jdk-17.0.20-ga。
01先弄懂它是什么
接口文档要让调用方知道:能传什么、会收到什么、失败怎么区分。OpenAPI 把路径、方法、参数、正文、响应和安全方案写成工具可读取的格式,但不会替所有服务端自动检查。比如声明 quantity 最小为 1,仍需运行时代码正确读取、校验并拒绝不合法数量。
本篇使用 OpenAPI 3.1.1,其中用 Schema,也就是一份可由工具读取的数据规则,说明字段的类型和限制;它与 JSON Schema 2020-12 体系相关。先分清四种情况:没传字段、传了 null、传了空字符串、传了数字 0。
required 只规定字段必须出现;能否为 null,还要看类型规则,不能照搬旧版的 nullable 写法。路径参数有额外要求:in: path 的参数必须声明 required: true。
契约不只包含成功 DTO。调用方还需要知道参数错误、未认证、权限不足、对象不可见、业务冲突和依赖失败如何表示。响应应包含媒体类型、字段类型和稳定错误结构,避免只写一个 description 就认为接口已定义完整。内部数据库列与外部 DTO 各有演进周期,不应自动把所有数据库字段暴露成响应。
文中这几个词,先认一认
- 契约
- 调用方与服务端关于输入、输出和失败处理的明确约定,可以据此核对实际行为。
- Schema 校验
- 检查数据的字段、类型和限制是否符合声明;不能替代业务状态或权限检查。
- 兼容性
- 新旧客户端和服务端组合使用时仍能按约定工作,要说明具体支持哪些组合。
02看看它怎么工作
先选定一个具体操作,列出路径、HTTP 方法与输入来源:path、query、header 或请求体。再定义必填字段和约束。创建订单示例要求 skuId 为正的 int64 范围整数、quantity 为一到一百的整数,note 可缺失或为 null,若是字符串则最多二十个码点,额外属性拒绝。这样的精确边界才能指导实现和测试。
相应 Schema 的关键声明是 type: object、required: [skuId, quantity] 和 additionalProperties: false;属性 skuId、quantity 使用 type: integer 及 minimum、maximum,note 使用 type: [string, 'null'] 与 maxLength: 20。
完整 OpenAPI 文档还需 openapi、info、paths、requestBody 和 responses 等上下文,本段只是用于解释约束的局部说明。
JSON Schema 的整数关注数学值,而不只看文本是否出现小数点,因此 1.0 可以是整数,1.5 不可以。Java 绑定层若直接用 longValue 截断,会把后一种错误输入变成看似合法的一。应明确解析器数字策略,使用精确转换或校验;format: int64 也不应被假定在每个工具中都自动执行同样的范围检查。
请求通过结构校验后,还需要业务规则和对象授权。例如订单编号是合法正数,并不代表当前用户可以读取该订单。OpenAPI security scheme 描述认证方式,实际凭据验证由服务执行,对象所有权还需结合业务数据检查。返回 404 隐藏不可见对象或返回 403 的选择应按产品安全策略一致实现。
契约验证应双向进行:非法请求被正确拒绝,合法响应也符合声明。针对缺失、null、边界和他人对象等用例检查真实状态码与结构,避免文档和实现各自通过却互不一致。新增响应字段常对宽容客户端兼容,但严格拒绝额外字段的客户端可能失败,因此不能脱离消费者行为宣称新增字段一定兼容。
正在加载流程图…
查看流程图源码(Mermaid)
flowchart TD
A["请求字节与媒体类型"] --> B["受限解析"]
B --> C["结构和数值校验"]
C --> D["认证与对象权限"]
D --> E["业务规则与操作"]
E --> F["按契约构造响应"]
C --> G["结构化错误"]
D --> G按顺序看,每一步做了什么
- 定义操作与每个参数来源。
- 区分存在性、
null、类型和范围。 - 写成功与失败响应结构。
- 实现绑定、校验和授权。
- 用真实请求核对双向契约并检查兼容性。
03对照实现代码
Microsoft OpenJDK 17.0.20.1;java.base/java/math/BigDecimal.java · java.math.BigDecimal#toBigIntegerExact()
以下为原创 Java 17 教学实现,不是原样 JDK 源码;只保留文中明确讨论的机制,省略优化、兼容及内部接口。
import java.math.*;
public final class SourceModel {
static long positiveId(BigDecimal value) {
if(value==null) throw new IllegalArgumentException("required");
if(value.compareTo(BigDecimal.ONE)<0 ||
value.compareTo(BigDecimal.valueOf(Long.MAX_VALUE))>0)
throw new IllegalArgumentException("range"); // 先限制表示范围。
BigInteger integer=value.toBigIntegerExact(); // 有非零小数部分时失败,不静默截断。
return integer.longValueExact(); // 整数也必须落在目标 Java 类型范围。
}
}本机 BigDecimal.toBigIntegerExact 使用不允许舍入的方式将 scale 调整为零,再返回整数表示。非零小数部分会导致 ArithmeticException,而诸如 1.0 这样的值可以精确转换。这与普通 longValue 可能丢弃小数部分的行为不同,正好对应接口整数绑定需要明确拒绝非整数的边界。
教学模型在精确转换之前检查正数和 long 上界,避免把超出业务范围的值继续当作候选编号。它是原创的 Java 数值校验片段,没有实现 JSON 解析器,也不是 OpenAPI 或 JSON Schema 引擎。输入已经被解析为 BigDecimal 的前提必须由接入层兑现,不能对任意 Number 随意强转。
限制最终数字不能超过 long,并不限制解析它要花多少资源。数字文本长度、指数范围和整个请求大小,应在解析阶段就限制,否则可能还没到业务校验就分配大量内存。精确转换保证数字没被悄悄截断,但不能独自处理所有超大输入问题。
完整算法用有限的固定字段展示存在性、类型、范围和额外字段检查,不声称覆盖 allOf、引用、条件 Schema、递归对象或所有格式。真实项目采用成熟校验器时,也应核对启用的方言与选项,并以相同边界案例验证绑定后的实际行为。
代码先调用谁,再调用谁
- 受限 JSON 解析为约定的 Java 值
- 区分键缺失和显式
null - 检查数字数学整性
- 精确转换与范围检查
- 形成字段错误或领域输入
04动手写一个例子
固定订单请求的确定性校验器
输入已解析的 Map:数字统一为 BigDecimal,字符串为 String,null 保留。按固定字段顺序返回错误路径与代码,未知字段按名字排序;不解析 JSON,不执行授权或下单。
- 输入 1
skuId=1.0,quantity=2,note=null- 输出 1
无错误
- 输入 2
quantity=1.5,缺 skuId- 输出 2
skuId:required、quantity:integer
首先使用 containsKey 判断必填属性,而不是只调用 get,因为 get 返回 null 可能代表缺失,也可能是明确传入 null。数字类型只接受本题约定的 BigDecimal,字符串形式的 '2' 不被自动转换,避免文档说数字而运行时却接受多个含糊表示。
对数字使用 stripTrailingZeros 后的 scale 判断是否为数学整数,再检查上下界。这样 1.0 与 1 都合法,1.5 被拒绝。校验不通过就记录该字段第一个明确错误,返回顺序固定,便于客户端测试和排障;是否聚合全部错误是接口契约的一部分。
note 独立处理可缺失与可空语义,非空时要求字符串并按码点数限制长度。未知属性统一收集并排序,防止客户端误拼字段名却被服务静默忽略。算法不生成业务对象,也不证明 skuId 存在或用户有权限,这些步骤在结构通过之后另行执行。
为什么这样写是对的每个已检查字段都按声明区分缺失、空值、类型和范围,错误顺序不依赖 Map 遍历顺序。
固定已知字段只检查有限次,未知字段排序提供稳定输出;码点计数和 BigDecimal 操作不能忽略输入长度。
保存为 Main.java,运行 javac --release 17 -encoding UTF-8 Main.java,再运行 java Main。程序包含固定输入自测,断言失败时会抛出错误。
展开收起代码 · Main.java · 完整可运行程序
长行可在代码区域内左右滚动,按 Tab 可把焦点移入代码区。
import java.math.*;
import java.util.*;
public class Main {
record Issue(String path,String code) {}
static final Set<String> KEYS=Set.of("skuId","quantity","note");
static void integer(Map<String,Object> body,String key,BigDecimal min,BigDecimal max,List<Issue> out) {
if(!body.containsKey(key)) {out.add(new Issue(key,"required")); return;} // 缺失不同于显式空值。
Object value=body.get(key);
if(!(value instanceof BigDecimal n) || n.stripTrailingZeros().scale()>0) {
out.add(new Issue(key,"integer")); return; // 接受数学整数 1.0,拒绝字符串和非零小数。
}
if(n.compareTo(min)<0 || n.compareTo(max)>0) out.add(new Issue(key,"range"));
}
static List<Issue> validate(Map<String,Object> body) {
Objects.requireNonNull(body);
List<Issue> out=new ArrayList<>();
integer(body,"skuId",BigDecimal.ONE,BigDecimal.valueOf(Long.MAX_VALUE),out);
integer(body,"quantity",BigDecimal.ONE,BigDecimal.valueOf(100),out);
Object note=body.get("note");
if(note!=null) {
if(!(note instanceof String s)) out.add(new Issue("note","string"));
else if(s.codePointCount(0,s.length())>20) out.add(new Issue("note","length")); // 按码点而非 char 数计数。
}
Set<String> unknown=new TreeSet<>();
for(String key:body.keySet()) if(!KEYS.contains(Objects.requireNonNull(key))) unknown.add(key);
for(String key:unknown) out.add(new Issue(key,"unknown")); // 未知属性稳定排序。
return List.copyOf(out);
}
static Map<String,Object> body(Object id,Object quantity) {
Map<String,Object> m=new HashMap<>(); m.put("skuId",id); m.put("quantity",quantity); return m;
}
static BigDecimal n(String s) {return new BigDecimal(s);}
static void eq(Object a,Object b) {if(!Objects.equals(a,b)) throw new AssertionError(a+" != "+b);}
public static void main(String[] args) {
eq(validate(body(n("1.0"),n("2"))),List.of());
eq(validate(Map.of()),List.of(new Issue("skuId","required"),new Issue("quantity","required")));
eq(validate(body(null,n("1"))),List.of(new Issue("skuId","integer")));
eq(validate(body(n("1"),n("1.5"))),List.of(new Issue("quantity","integer")));
eq(validate(body(n("1"),n("101"))),List.of(new Issue("quantity","range")));
Map<String,Object> nullable=body(n("1"),n("100")); nullable.put("note",null); eq(validate(nullable),List.of());
Map<String,Object> emoji=body(n("1"),n("1")); emoji.put("note","😀".repeat(20)); eq(validate(emoji),List.of());
emoji.put("note","😀".repeat(21)); eq(validate(emoji),List.of(new Issue("note","length")));
Map<String,Object> unknown=body(n("1"),n("1")); unknown.put("z",true); unknown.put("a",true);
eq(validate(unknown),List.of(new Issue("a","unknown"),new Issue("z","unknown")));
eq(validate(body("1",n("1"))),List.of(new Issue("skuId","integer")));
System.out.println("PASS 10");
}
}这些特殊情况也要试一试
- 缺失与显式
null产生不同的判断路径。 - 1.0 是整数,1.5 不是。
- note 允许空值,补充平面字符按码点计数。
- 未知字段拒绝并稳定排序。
进阶选读 · 跨版本契约回归
场景服务与多个客户端版本独立发布。
做法维护消费者使用的请求与响应样本,比较 Schema 变化并运行集成契约测试,关注字段收紧、枚举扩展和错误语义变化。
复杂度成本随版本组合和用例数增长;静态差异检查无法穷尽客户端实现行为。
好处与代价可以更早发现破坏性变化,但需要治理样本真实性和版本生命周期。
05项目里怎么用
接口文档应和实现一起评审,特别是错误码、必填字段、分页,以及时间金额写法。自动生成文档能减少手写不同步,但只看 DTO 仍可能漏掉对象权限、状态冲突或多个字段之间的规则。这些行为要用说明与请求测试表达,不能按注解多少判断接口是否完成。
拒绝未知字段能发现拼写错误,却要求客户端和服务端更紧密地协调更新;允许未知字段可能方便演进,也可能隐藏传错参数。请求与响应可以用不同策略。判断是否兼容,要看真实客户端怎样解析枚举、数字和 null,不能只比较是否多了一个字段。
响应错误格式应提供稳定机器码、合适的字段路径和可公开说明,内部堆栈留在服务端关联日志。认证与对象授权应在实际执行路径落实,并用不同身份访问同一资源验证。结构校验失败、权限失败和依赖故障应分别观察,避免全部转换为一个成功状态加错误文本,让调用方无法可靠处理。
解析、验证和执行业务各自需要资源上限。限制请求体、字段长度和嵌套深度,可以让不合法输入尽早停止;错误聚合也应有数量上限。这里的小型校验器用于推导规则,生产系统应选择支持所需方言的成熟组件,并保留针对实际绑定层的回归测试。
容易踩的坑
- required 与允许
null混为一谈。 - 整数绑定直接截断小数。
- security scheme 被误当成已执行授权。
- 只校验请求而让实际响应偏离 Schema。
几种做法,怎么选
表格有四列,窄屏可左右滑动查看代价和适用条件。
| 方案 | 优势 | 代价与限制 | 适用条件 |
|---|---|---|---|
| 契约先行 | 调用方可提前协作 | 需要持续同步实现 | 多团队接口 |
| 代码生成文档 | 与类型结构关联紧密 | 容易漏掉业务和权限语义 | 已有服务补充文档 |
| 严格字段校验 | 及早发现拼写与类型错误 | 演进时协调要求更高 | 明确版本和稳定消费者 |
06检验是否读懂了
先自己想、自己写,再展开答案核对。场景题是假设的练习情境;遇到还没学过的内容,可以做完基础后再回来。
先用自己的话解释
01required 是否意味着不能为 null?
展开参考答案与推导
不是。required 约束属性存在,null 是否允许由属性 Schema 的类型等规则决定。缺失、空值和空字符串需要分别约定与测试。
021.0 是否一定不符合 integer?
展开参考答案与推导
不是。JSON Schema 的整数按数学值判断,零小数部分的 1.0 可以符合整数约束。绑定层应避免把 1.5 静默截断,也要检查目标类型范围。
03OpenAPI 安全方案会自动保护接口吗?
展开参考答案与推导
不会普遍自动执行。文档描述认证方式,服务仍需要真实凭据验证、请求规则和对象权限检查,并用不同身份的请求证明行为。
换个情况,还会处理吗
01客户端发送 qty 却得到成功,如何处理?
展开参考答案与推导
先决定未知字段策略。若契约要求 quantity 且拒绝额外字段,应同时报告缺失 quantity 与未知 qty,避免静默使用默认值;实现和文档需要一致。
02新增响应枚举值一定兼容吗?
展开参考答案与推导
不一定。客户端可能使用穷尽分支或严格解析而失败。需要检查消费者处理未知枚举的约定和实际用例,再决定版本和发布策略。
03合法订单编号能查到他人订单怎么办?
展开参考答案与推导
这是对象授权缺失,结构校验不能解决。查询必须结合当前身份与访问策略,并用同一编号、不同用户的真实请求验证可见性。
写代码试一试
01区分缺失与空值。
- 输入 / 条件
{} 与 {quantity:null}- 输出 / 预期
required 与 integer
提示 containsKey 先判断。
展开参考答案与推导
缺失属性报告 required;属性存在但 null 不符合本题整数约束,报告 integer。仅用 get==null 会丢掉两类输入的区别。
02二十个表情是否超出二十长度?
- 输入 / 条件
😀 重复 20 次- 输出 / 预期
本题允许
提示 按码点计数。
展开参考答案与推导
每个表情占两个 Java char,但一个码点。使用 codePointCount 计算为二十;不能直接用 String.length 将合法输入误判为四十。
03如何防止 1.5 被绑定成 1?
- 输入 / 条件
BigDecimal 1.5- 输出 / 预期
拒绝
提示 精确转换而非截断。
展开参考答案与推导
使用 toBigIntegerExact 或等价数学整数检查,之后验证上下界;不要通过 longValue 截去小数后再声称原输入符合整数契约。
读完之后,独立完成
课后作业:自己做一遍
本篇共 10 道作业,先完成 8 道基础与综合任务,再按前置知识挑战进阶题。其中包含 1 道思维拓展,试着换个条件,看看原来的办法是否还管用。不附参考答案。
能写清订单查询接口的参数、响应和错误约定(接口契约),并用例子验证文档与实际行为一致。
本篇重点加练
Schema 的声明与运行时绑定可能不一致,必须检验数据在边界上的真实表示。
- Schema
- 契约与运行时校验
重点内容会多练几次。先说清楚怎么做,再试试空数据、出错或条件改变时会发生什么。
先自己做,卡住时记下问题,再回来查。问答题写清“为什么、什么时候适用”;动手题留下输入、输出和运行步骤,方便下次重做。
基础必做
- 作业 01原理问答
为“读取指定商户的一张订单”设计
OpenAPI3.1 路径,说明路径参数身份、编码和范围;区分示例值与 Schema 限制。- 提交内容
- 接口定义草案
- 怎样算完成
- 参数名称与路径一致
- 说明 ID 类型及合法范围
练到的知识点OpenAPI 3.1 · 路径参数
- 作业 02代码推演
更新昵称接口区分字段缺失、显式
null和空字符串。为请求 Schema 定义三种情况,并给出 Unicode、多余字段和长度边界样本。- 提交内容
- Schema 与请求矩阵
- 怎样算完成
- required 和类型约束分别表达
- 样本不能代替规则
练到的知识点Schema · required 与 nullable
- 作业 03设计评审
创建订单可能成功、重复提交、参数错误、权限不足或依赖故障。设计状态码和统一错误结构,评审客户端如何区分可重试情况。
- 提交内容
- 响应契约与客户端决策表
- 怎样算完成
- 错误结构能关联请求
- 不把业务结果未知伪装成确定失败
练到的知识点响应状态 · 错误结构
- 作业 04代码推演
同一金额字段可能收到科学计数法、大整数和小数文本,后端最终绑定为 Java 整数。定义契约中表示、范围和精度的独立约束。
- 提交内容
- 数值样本与契约表
- 怎样算完成
- 不把可解析等同于符合业务
- 注明序列化与绑定阶段
练到的知识点Schema
综合应用
- 作业 05面试场景
文档声明 Bearer 认证,但控制器未安装任何权限逻辑。列出从规范文件到运行时验证之间需要实际验收的行为。
- 提交内容
- 契约执行检查表
- 怎样算完成
- 区分声明与执行
- 包含未登录和对象归属不同的请求
练到的知识点安全方案声明 · 契约与运行时校验
- 作业 06编程实践
编写本地最小订单 API 及契约测试,自动发送自拟边界请求并核对状态、字段类型和错误结构;仅用测试身份。
- 提交内容
OpenAPI文件、实现和测试记录- 怎样算完成
- 请求与响应都核验
- 合同通过不被宣称为完整安全证明
练到的知识点OpenAPI 3.1 · 路径参数 · Schema · required 与 nullable · 响应状态 · 错误结构 · 安全方案声明 · 契约与运行时校验
- 作业 07设计评审
生成的 SDK 会删除未知字段,但手写 HTTP 客户端可以保留;服务端又对未知字段有不同配置。评审契约测试应覆盖的客户端路径。
- 提交内容
- 路径及校验责任矩阵
- 怎样算完成
- 真实服务器行为单独核实
- 不能仅测 SDK 发送成功
练到的知识点契约与运行时校验
- 作业 08编程实践
在本地最小 API 使用同一 Schema 生成边界请求,比较纯 JSON 校验与 Java 绑定后的校验,收集两者不一致的样本。
- 提交内容
- 代码、Schema 与差异报告
- 怎样算完成
- 包括 Unicode、数值范围及未知字段
- 差异有明确版本和配置
练到的知识点Schema · 契约与运行时校验
进阶挑战 · 掌握前置后选做
- 作业 09设计评审
客户端已经生成 SDK,服务拟把可选字段改必填并新增状态枚举。评审兼容性和灰度发布顺序,设计旧客户端验收。
- 提交内容
- 变更差异与兼容矩阵
- 怎样算完成
- 分别检查请求和响应方向
- 包含未知枚举处理
练到的知识点Schema · required 与 nullable · 响应状态 · 错误结构 · 契约与运行时校验
思维拓展
先写出你的猜想,再用例子或实验验证。答案可以不同,但要说清依据、适用情况,以及还有什么没弄懂。
- 作业 10思维拓展 · 跨域迁移
把 API 参数来源改成模型生成的工具调用,输入可多次自我修正。应怎样区分语法合法、契约合法和允许执行业务动作?
- 提交内容
- 工具边界契约
- 怎样算完成
- 包含重试和修改参数
- 模型声称已校验不能替代服务端证据
练到的知识点Schema · 契约与运行时校验
完成后,再检验一次
给每题标记“独立完成 / 查阅后完成 / 尚未完成”。查过资料或没做出的题,记下卡在哪里。隔一天换一组输入再试,能解释结果和出错原因,再把这个知识点标为掌握。
继续阅读
先看能帮你完成当前练习的部分。例子跑通后,再读更深入的解释。
- 必读规范英文
OpenAPI Specification 3.1.1 (新标签页)↗
阅读 Schema、Parameters、Responses 与 Security Scheme,形成可验证的接口契约。
- 实践参考文档英文
Spring Boot 3.5:Testing (新标签页)↗
阅读测试切片、完整应用测试与事务注意事项,选择真实覆盖目标的测试范围。
- 深入实践指南英文
OWASP:Authorization Cheat Sheet (新标签页)↗
阅读默认拒绝、每次请求校验和对象权限,建立权限矩阵及反例测试。
- 深入参考文档英文
OpenJDK 17u java.math.BigDecimal (新标签页)↗
核对不允许舍入的整数转换,连接 JSON 数字语义与 Java 绑定边界。
- 深入参考文档英文
JSON Schema 2020-12 Validation (新标签页)↗
核对 integer、required、长度及数值约束。
- 深入参考文档英文
Java 17 BigDecimal (新标签页)↗
比较精确转换和可能截断的数值转换。