第四章 接口设计原则:五大核心约束
说起RESTful接口,很多新人会以为「把URL写成名词、用HTTP方法做CRUD」就算完了。但在我看来,真正的RESTful架构得遵守五个硬约束——统一接口、无状态、可缓存、分层系统、按需代码。这五个东西就是Roy Fielding博士在里定下的规矩,缺一个就不是完整的REST。
我在做第一个水印平台时,一开始压根没在乎这些原则。结果呢?接口越写越乱,缓存不好做,客户端和服务端耦合得死死的。后来痛定思痛,一条一条对规范修,才把系统救回来。今天就跟你聊聊这五个原则,每个我都会结合自己的经历来讲,希望能帮你少踩坑。
4.1 统一接口
统一接口说白了就是:所有资源都通过同一套协议和格式来交互。你想想看,如果有10个接口,每个接口请求方式、参数结构、返回内容都随心情写,那调用方得多崩溃?
我自己习惯的做法是:
- 资源用名词:/watermarks、/watermarks/{id}
- 操作靠HTTP方法:GET查、POST增、PUT全覆盖改、PATCH部分改、DELETE删
- 固定数据格式:所有响应体统一用JSON,错误包也按统一结构(code + message + data)
- 使用标准超媒体:响应里带上相关链接,让客户端自己发现下一步操作
GET /watermarks/delete?id=xxx。结果客户端以为这是幂等查询,刷新页面直接把所有水印全删了……统一接口的意义就在这儿——规约清楚,出事的概率就低。
4.2 无状态
无状态原则要求:服务端不存任何客户端上下文。每次请求必须包含所有必要信息,服务端按请求本身就能独立处理。说白了,服务端就是个「记忆只有7秒」的鱼。
有人会问:「那登录状态怎么办?」嗯,这里要注意——无状态不是说不让你有或Session,而是说这些凭证信息要带在请求里(比如JWT放在Authorization头),服务端不主动维护会话状态。
我曾经接手过一个老项目,用户登录后服务端会把用户角色存到内存里,结果负载均衡一扩容,用户在A机器登录,请求落到B机器就直接返回401。改成无状态后,每次请求都带,服务端只做验证不存状态,问题就没了。
无状态的好处很明显:水平扩展超轻松,任意节点都能处理任意请求;故障恢复也快——挂掉一台,其他节点顶上就行,不用管会话同步。
4.3 可缓存
可缓存原则是说:要在响应中明确标识这个资源能不能缓存、能缓存多久。客户端和中间代理拿到响应后,根据Cache-Control、ETag、Last-Modified等头来决策。
你想想看,水印接口里大量「查询水印列表」「获取水印模板」这类读操作,数据变化频率低,不缓存简直是浪费带宽。我建议:
- 对GET请求,默认加
Cache-Control: max-age=3600或更久 - 对需要实时数据的关键接口,用
Cache-Control: no-cache配合ETag做协商缓存 - POST/PUT/DELETE 等写入操作,服务端校验若资源有变,可以主动清除或失效缓存
Cache-Control: private 或 no-store 来控制。
4.4 分层系统
分层原则要求:整个系统拆成若干层,每一层只知道自己相邻的层。客户端不必知道身后是有一台服务器,还是有一堆网关、CDN、缓存层、微服务集群。
我做过的一个水印服务架构大概这样:
- 客户端层(App/Web)— 只跟API网关通信
- API网关层 — 负责鉴权、限流、请求转发、统一日志
- 业务服务层 — 处理水印生成、校验、管理等核心逻辑
- 持久化层 — 数据库+对象存储
这么分层的好处:哪一层出问题,替换那一层就行,其他层不用改。比如我要把数据库从MySQL换成PostgreSQL,业务服务层的接口不变,上层完全感知不到。
但注意——分层不能滥用。我见过有人分了七八层,每层只传个参数,导致一次请求产生十几倍网络开销。分层的度很重要。
4.5 按需代码
按需代码是唯一的可选原则。它允许服务端在返回响应时,顺带发送一段可执行的脚本(比如JavaScript),客户端拿到后直接运行,以此扩展自身功能。
这个原则在水印接口里用得不多,但也不是没用。举个例子,水印预览功能——服务端返回一个处理图片的小脚本,客户端在浏览器里直接执行,不用刷新页面就能看到水印效果。我在一个移动端项目里就用过类似方式,把水印定位算法以JavaScript片段下发给客户端,客户端实时计算,体验很好。
不过我得说一句:按需代码适合可控的、同构的环境(比如自家App里嵌入WebView)。在开放API里慎用——传来一段不可信代码,安全风险很高。
五大原则关系图
从SVG图能看到,RESTful API是中心,五个原则像五个支柱一起支撑着它。每个原则都不孤立,比如可缓存往往依赖统一接口(统一的响应头格式),分层系统需要无状态才能水平伸缩。
各原则速览对比
| 原则 | 核心要求 | 对水印接口的意义 | 我常用的实践 |
|---|---|---|---|
| 统一接口 | 资源命名+HTTP方法+数据格式标准化 | 客户端无需适配不同写法 | 统一错误格式、超媒体链接 |
| 无状态 | 每次请求自包含,服务端不存上下文 | 方便容器化部署、弹性伸缩 | JWT传身份,不依赖Session |
| 可缓存 | 明确响应可缓存性及有效期 | 减少水印列表/模板重复查询的带宽 | Cache-Control + ETag 配合 |
| 分层系统 | 每层只知相邻层,组件可独立替换 | 业务层与存储分离,升级数据库不影响上层 | API网关 → 业务服务 → 存储 |
| 按需代码 | (可选)服务端下发可执行脚本 | 水印预览、实时渲染可以下放 | JS脚本+沙盒执行,严格校验 |
总结一下:这五个原则里,前四个是强制约束,第五个按需代码是可选项。但不管是强制的还是可选的,它们共同构成了RESTful架构的规矩。遵守了,你的接口就经得起大规模、高并发的考验;不遵守,短期看可能「快」,长期就是一座屎山。
我个人建议,在设计水印接口的一开始,就把这五个原则写在规范文档最前面。每写一个接口、每加一个功能,就问自己:「这个设计违反了什么原则?」坚持下来,你会感谢自己。