1. 现实问题:接口文档写了路径,却没有写清语义
前端说“调用文章接口失败”,后端需要知道是 URL、方法、请求头、JSON 正文、状态码还是代理缓存出了问题。HTTP 不是把 Java 方法换成字符串,它是一套由客户端和服务器交换的消息协议。方法表达意图,路径标识资源,状态码说明结果,头携带元信息,正文承载表示。
先设计文章详情和发布两个接口,再把每个字段放回 HTTP 的正确位置。这个模型会直接连接 DispatcherServlet、参数绑定、认证和异常处理。
2. 最小可运行示例:接口契约先于 Controller
GET /api/articles/42 HTTP/1.1
Accept: application/json
Authorization: Bearer <token>
HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: max-age=60
{"id":42,"title":"Web 请求链","status":"PUBLISHED"}
发布是状态变化,使用 POST /api/articles/42/publish 或 PATCH /api/articles/42,而不是用 GET 触发写入。不存在的文章返回 404,输入不合法返回 400 或 422(团队要统一),没有权限返回 401/403,创建成功可返回 201 和 Location。状态码不是装饰,它决定客户端重试、缓存和错误分支。
3. 调用链与对象变化
浏览器或 HTTP 客户端把方法、路径、头和正文编码成字节,通过连接发送;服务器先解析请求行和头,再按 Content-Type 解码正文。应用层把 42 从路径字符串转换为 long,把 JSON 字段转换为 DTO,Service 返回领域对象,最后序列化器把对象转换成 JSON 字节和状态码。
Cookie 是浏览器自动携带的状态片段,Authorization 是客户端主动发送的凭证头,二者的安全边界不同。Cache-Control 影响中间缓存,ETag 可以让客户端条件请求;这些头不会改变 Java 对象本身,却改变请求是否到达业务代码以及响应是否被复用。
4. 为什么这样设计
资源路径和动作语义分开能让接口更可预测;幂等方法与非幂等方法的区别决定重试是否安全。POST 通常不是幂等的,重复请求可能创建两条数据,所以支付、发布等动作要设计幂等键或状态条件。不要为了“看起来 REST”把每种操作都硬塞进一个 URL。
JSON 是表示格式,不是领域对象。DTO 应限制可接收字段、长度和类型,响应 DTO 只暴露稳定字段。错误响应应包含机器可读 code、用户可读 message 和 traceId,避免把堆栈直接返回给客户端。
5. 项目落点:从契约到测试
博客系统可以先写接口表:方法、路径、请求参数、成功状态、错误状态、是否需要认证、是否可缓存。Controller 测试验证 HTTP 层,Service 测试验证业务,Repository 测试验证 SQL;不要让一条端到端测试承担所有定位工作。
练习:为列表接口定义 GET /api/articles?status=&page=&size=,规定默认排序和最大 size;再为重复发布定义 409 或业务错误码。用 curl 发送空参数、未知 id、错误 Content-Type 和重复请求,保存原始请求/响应作为证据。
6. 易错排查
- 看到 404 就改数据库:先区分路由不存在、资源不存在和代理路径改写。
- 401/403 混淆:401 是缺少或无效认证,403 是已识别身份但无权限,团队要保持一致。
- GET 修改数据:浏览器预取、缓存和重试会放大副作用;写入使用明确的非安全方法。
- JSON 能解析但字段丢失:检查 Content-Type、字段名、DTO 可写性和未知字段策略。
7. 一页复习
HTTP 请求由方法、路径、头和正文组成,响应由状态码、头和正文组成。先定义资源与幂等语义,再写 Controller;认证、缓存、错误和重试都在 HTTP 契约里有位置。后面的 Spring 学习只是把这条协议链映射到 Java 对象和方法。
把详情接口写成请求状态表会更准确:客户端输入方法 GET、路径 id=42、Accept JSON;服务器输入路由匹配结果和数据库查询结果;输出可能是 200 与 ArticleView、404 与 NotFoundError、400 与路径转换错误。状态码、响应体和 Cache-Control 是一起设计的,不能只在 Controller 最后加一个 return。
项目文件可以在 web/article/ArticleController 只做绑定,在 application/article/ArticleQueryService 负责公开状态,在 web/error 统一响应,在 persistence/article 查询数据库。路径 id 到 long 的转换发生在 Controller 之前,作者权限和草稿状态发生在 Service,文章 JSON 字段由 DTO 控制。每层的输入输出类型不同,测试就能准确定位。
错误排查先保存原始请求和响应,再查代理改写、路由匹配、参数转换、权限和数据库。GET 返回 200 但数据为空,可能是 status 过滤或缓存,不一定是前端;重复 POST 可能是客户端重试,不一定是数据库重复;浏览器缓存了旧 JSON,curl 仍返回新值时,问题在 Cache-Control/ETag 而非 Service。每个判断都用同一 trace id 连接日志。
练习是设计文章列表和发布两个完整契约:写方法、路径、参数、成功/失败状态、幂等性和缓存策略;用 curl 重放合法、未知 id、错误方法、缺少认证和重复发布。把“可重试/不可重试”写出来,下一课 Axios 才能正确处理,而不是所有错误都弹出同一条提示。
HTTP 的头也要回到项目文件和层次:Authorization 由安全链读取,Content-Type 由消息转换器使用,Cache-Control/ETag 由缓存策略决定,X-Request-Id 或 traceparent 由观测链传递。Controller 不应该手动解析每个头,更不应该把认证头复制到日志;它只消费已经确认的身份和请求 DTO。
状态码背后是客户端后续动作:400 通常修正输入,401 获取或刷新凭证,403 改变权限,404 展示资源不存在,409 让客户端重新读取当前状态,429 按退避重试,5xx 记录服务故障。若所有情况都返回 200 加 success=false,浏览器、代理、监控和 SDK 都失去统一判断条件。
项目可以把契约写成 docs/api/article.md,把错误 code 与状态映射放 web/error,把幂等键存到 application/idempotency。发布动作输入 article id、actor 和 key,输出发布结果或冲突;读取动作输入路径和缓存条件,输出资源表示或 304。把读与写的副作用分开,缓存和重试才有稳定依据。
排错时用 curl 先绕过浏览器确认服务,再看浏览器 Network 的 preflight、Cookie 和缓存;代理改写会改变 path,context path 会让应用路由前缀不同,Content-Type 错会让正文根本没有绑定。文章存在但返回 404 还要查公开状态、软删除和租户过滤。练习是为一个详情请求保存原始报文、Controller 日志、Service 查询和最终响应四份证据。
验证清单:为详情、列表、发布各写一张契约表,逐项填 method、path、query/body、成功状态、失败状态、幂等性和缓存头;用 curl 发送合法 id、未知 id、非法方法、重复发布和缺少认证的请求,保存原始响应。再从浏览器重复一次,比较 Origin、Cookie、preflight 和缓存命中,确认浏览器差异没有被误判成数据库错误。复盘时把请求从字节到 DTO、从 Service 结果到 JSON 的转换点圈出,任何一个点没有明确输入输出就继续拆边界。
练习复盘:将文章详情的 GET、草稿发布的 POST、重复发布的 409 和未授权草稿的 401/403 写成四个可重放报文,固定请求头、路径、body、响应状态和错误 code。用 curl 验证服务器契约,再用浏览器验证 Origin、Cookie、缓存和 preflight 差异;每个结果都标记是 HTTP 层、Service 层还是数据库层负责。只有契约、幂等和缓存行为都可复现,后续 Axios 或 Spring MVC 的映射才有稳定输入。
进阶附录:ETag、条件请求与流式响应
ETag 让客户端发送 If-None-Match,内容未变时服务器返回 304;它不同于业务版本号,但可以由版本生成。大文件下载要用流式响应和 Range 语义,避免一次性把文件放进堆。缓存策略必须区分公开内容与用户私有内容。
本课按「HTTP 语义、REST 资源与现代 Java Web 基础」的学习范围组织,正文与示例均为本站原创整理。