123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362 |
- // tag::main[]
- == Restful API 开发规范
- 推荐简单业务场景 / 开放接口使用 Restful 风格,业务复杂场景 / 内部接口可借鉴 Restful 的优点,不一定完全遵守(抽象所有资源),以更容易理解业务。
- === 统一响应体格式
- 返回值统一为json格式,最外层只包含 `code`、`msg`、`data` 三个字段,接口返回包含数据内容时,仅使用 `data` 字段。
- ----
- {
- "code":"错误码,成功为0",
- "msg":"返回的(错误)描述信息,不能有堆栈信息",
- "data":{数据}
- }
- ----
- ==== 响应举例
- ===== 空响应-成功:
- ----
- {
- "code":"0",
- "msg":"",
- "data":null
- }
- ----
- ===== 空响应-出错:
- ----
- {
- "code":"0xaabc100f",
- "msg":"username can't be null",
- "data":null
- }
- ----
- ===== 单值响应:
- ----
- {
- "code":"0",
- "msg":"",
- "data":{
- "username": "cnlym",
- "nickName": "产码机器"
- }
- }
- ----
- ===== 多值响应(分页)
- ----
- {
- "code":"0",
- "msg":"",
- "data":{
- "total":11, //总条数
- "pageNo": 1, //当前页
- "pageSize":20, //当前分页记录数
- "list": [{ //分页内容
- "userId": 1,
- "name": "admin"
- },{
- "userId": 2,
- "name": "leader"
- }]
- }
- }
- ----
- ===== 多值响应(不分页)
- ----
- {
- "code":"0",
- "msg":"",
- "data":{
- "total":11,
- "list": [
- { // 列表内容
- "userId": 1,
- "name": "admin"
- },
- {
- "userId": 2,
- "name": "leader"
- }
- ]
- }
- }
- ----
- == 统一 api 文档约束
- 为了便于对接,组件 `doc` 目录下须包含以下文件
- * api.json
- ** 统一使用 swagger json 文档
- *** 需要指定 info-title, description,version,contact
- *** `basePath` 对应 context 值,格式为:`/<appId>`
- * api.md
- ** api文档,包含如下,不描述则采用本文的推荐设计:
- *** 简介
- *** 认证
- *** 鉴权规则
- *** 安全传输与加密算法
- *** 返回值规则
- *** 空响应
- *** 单值响应
- *** 多值响应(分页)
- *** 多值响应(不分页)
- * ext.md
- ** swagger 返回值无法描述泛型等,额外标注说明
- * guidance.md
- ** 编程指引
- * appendix.md
- ** 附录,错误码、数据字典、翻译项、复杂参数解释
- * <appId>-api-sdk.jar
- ** 接口sdk
- === Restful 请求方法含义
- [cols="1,4"]
- |===
- | 类型 | 说明
- | GET | 从服务器取出资源
- | POST | 在服务器新建一个资源,修改一个资源或者删除一个资源
- | PUT | 在服务器更新资源(客户端提供改变后的完整资源)
- | DELETE | 从服务器删除资源
- | HEAD | 获取资源的元数据
- | PATCH | 在服务器更新资源(客户端提供改变的属性)
- | OPTIONS | 获取信息,关于资源的哪些属性是客户端可以改变的
- |===
- === 常用 HTTP 状态码
- [cols="1,3"]
- |===
- | HTTP状态码 | 说明
- | 200 OK | 服务器成功返回用户请求的数据。处理成功时默认该值。
- | 201 Created | 修改数据成功(返回之前已经入库)
- | 202 Accepted | 表示一个请求已经进入后台排队(异步任务/如批量异步删除)
- | 204 No Content | 表示请求成功但无返回值,可用于修改,删除
- | 301 Moved Permanently | 资源的URI已被永久更新
- | 302 Found | 资源URI重定向,与301类似(临时)
- | 304 Not Modified | 资源未更改,客户的缓存资源是最新的, 请客户端使用缓存(缓存),ES-ik插件的动态更新用到了这个
- | 400 Invalid Request | 用户发出的请求有错误,是无效请求,如字段映射不正确,参数不正确都可以用这个
- | 401 Unauthorized | 需要先进行认证(登录)
- | 403 Forbidden | 服务器拒绝执行,一般是由于权限不够,也可能ip进了黑名单等
- | 404 Not Found | 用户发出的请求是针对不存在的记录
- | 405 Method Not Allowed | 不允许执行目标方法,如只允许POGT却使用GET,响应中应该带有 Allow 头,内容为对该资源有效的 HTTP 方法
- | 406 Not Acceptable | 用户请求的格式不可得,如服务器返回 `abc.txt`,浏览器期望返回数据的MIME类型没有 `txt`
- | 410 Gone | 用户请求的资源被永久删除,且不会再得到
- | 422 Unprocesable entity | 参数格式正确,但语义错误(创建资源时,校验未通过),浏览器将它与400处理方式相同,因此也可以使用400
- | 429 Too Many Request | 请求过多,触发限流时,可用于反爬虫,418(触发彩蛋)也可用于反爬虫
- | 500 Internal Error | 服务器发生错误,用户将无法判断发出的请求是否成功
- | 503 Service Unavailable | 服务不可用状态,多半是因为服务器问题,例如CPU占用率大,等等
- |===
- ==== 丢失更新问题
- HTTP 201 可以包含一个ETag响应头字段,指示刚刚创建的请求变量的实体标签的当前值。ETag头字段可以在以后的条件请求中使用,以防止“丢失更新”问题。
- 当多人编辑资源而不了解彼此的更改时,会发生丢失的更新问题。在这种情况下,最后一个更新资源的人“获胜”,之前的更新将丢失。ETag可以与If-Match标头结合使用,让服务器决定是否应该更新资源。如果ETag不匹配,则服务器通过412 (Precondition Failed)响应通知客户端。
- 数据库中添加dataVersion,每次修改+1,可以使用该字段来避免并发更新问题。
- === 请求头约束
- ==== 命名规则
- 单词首字母大写,单词与单词间使用中划线(`-`)分割。如:`User-Agent`
- ==== 请求头保留字段
- [cols="1,1,1,3"]
- |====
- | 参数名 | 类型 | 必选 | 说明
- | *User-Agent* | string | √ | 终端类型
- | *Token* | string | √ | 接口认证使用:身份认证信息,采用 base64 编码。
- | X-Sid | string | × | 秘钥协商使用:安全会话 ID。
- | X-Dk | string | × | 秘钥协商使用:使用协商密钥加密的 `数据密钥` 。
- | User-Id | string | × | 用户 ID
- | *Trace-Id* | string | √ | 用于标识一笔业务的唯一序号,UUID 格式
- | *Span-Id* | string | √ | 用于标识某一阶段调用序号,32位字母或数字
- | Biz-Id | string | × | 业务标识
- | *X-B3-TraceId* | hex string | √ | UUID 格式
- | *X-B3-SpanId* | hex string | √ | 64 位值,采用小写 16 进制字符显示
- | *X-B3-ParentSpanId* | hex string | √ | 64 位值,采用小写 16 进制字符显示
- | X-B3-Sampled | boolean | × | `0` – 不采样, `1` – 采样
- | X-B3-Flags | string | × | `1` – debug,要记录本次所有调用链信息
- |====
- === 请求体约束
- 除文件和表单数据外,`POST`、`PUT` 等带请求体的方法,请求体统一为 `JSON` 格式,
- === 请求参数要求
- 参数须提供 `取值范围` 或 `格式限制` 或 `枚举限制`
- ==== 时间字段和格式
- * 当查询条件需要使用起止时间时,参数名统一采用 `beginTime` 和 `endTime`。
- * 采用 `ISO8601` 格式,使用带 Time-zone 的标准时间格式,如:`2020-7-13T00:00:00.000+08:00`;
- ===== 字符串和编码
- * 字符串的所有操作统一使用 UTF-8 编码。
- * 长度限制如下
- [cols="2,1"]
- |====
- | 场景 | 推荐长度
- | ID、标识、名称、别名 | 64, 128
- | 描述、备注、文件名(全路径) | 512, 1024
- | 详情 | 2048
- | 文本、文件 | 不限制
- |====
- ===== 常用字段约束
- 统一常用的参数名称
- ====== 分页查询参数
- demo: 每页20条记录,查询第一页,先按照 `username` 正序排序,再按照 `phoneNo` 逆序排序
- ----
- {
- "pageNo":"1",
- "pageSize":"20",
- "conditions": [
- {
- "property": "username",
- "like": "cn"
- },{
- "property": "level",
- "equal": "vip"
- }
- ],
- "orderBy":[
- {
- "property": "username",
- "direction": "asc"
- },{
- "property": "phoneNo",
- "direction": "desc"
- }
- ]
- }
- ----
- |====
- | 用途 | 参数名 | 举例
- | 查询页码 | pageNo | 1
- | 分页大小 | pageSize | 20
- | 排序参考 | sortBy | ["name"]
- | 排序规则 | order | "asc"
- |====
- 分页查询参数名称
- ==== 参数字段顺序
- * 标识性字段(id、name)
- * 必填字段(sex)
- * 常用字段(phone)
- * 描述性字段(note)
- * 通用型字段(updateTime)
- ==== 统一错误码格式
- 见 link:errorCode.html#错误码输出约束[错误码规范-错误码输出约束]
- === 版本号设计
- 软件中任何事物都有变的可能性,api 接口也是这样的,因此需要在 api 的请求路径中添加版本号。如 `/api/xxService/v2/**`。
- == 安全传输
- 参考 link:../security/negotiate.html[密钥协商]。
- == 编码注意事项
- * 定义统一返回值类 `BaseResponse`,且该类只能在接口层使用
- * 所有 Controller 层接口函数统一返回 `BaseResponse`,或使用全局返回值自动包装
- * 入参,返回值尽量不要有 `Map` 类
- * Controller 层进行 `DTO` 转换,不允许将 `DTO` 传递倒业务层
- * Controller 不要出现 Request,Response 这类对象
- * 一般不需要在这里打印日志,异常处理,使用统一的日志打印
- * 注意防枚举(id有顺序)、防重放
- ==== `Ajax` **VS** `Restful RPC api` 接口
- * 虽然两者都是 HTTP协议 JSON 格式
- * Restful 是无状态的,属于 RPC 的一种形式,职责为服务间通信。校验主要为 token 认证。
- * Ajax 是前后端局部刷新增加用户体验的。校验包含各种网络攻击。
- ==== `Restful with HTTP status` **VS** `统一返回 200,响应种自定义码值`
- Shoulder 种采用方案二,自定义返回值状态码,响应种 200 表示接口正常返回,4xx 表示客户端错误,前端开发先排差,5xx 表示服务端异常,后端开发先排差。
- ===== 方案一:只以HTTP状态码来表示状态
- * 200时候返回内容就是数据,最多有个分页
- > 分页也可以像 https://docs.github.com/en/free-pro-team@latest/rest#client-errors[github 那样] 放到header,响应内容更整洁
- * 只有在抛出Exception异常(即使业务逻辑上有问题,也抛出APIException异常)才返回像下面这样的响应:
- ----
- HTTP/1.1 405 Method Not Allowed
- Content-Type: application/json
- {"status_code": 405, "message": "Method 'DELETE' not allowed."}
- ----
- ===== 方案二:所有接口都返回200
- 响应体里包含:自定义码值、信息、数据
- {
- "status_code: 1000
- "message": "xxxxx"
- "result": {
- "id": 1
- ...
- }
- }
- ===== 两种方案优缺点比较
- * 方案一
- ** 优点
- *** 作为服务端更倾向于方案一,因为大多框架和各家提供的API都是这么干的,其实也很简单(先看状态码,然后直接根据状态码决定后续动作)
- *** 使用本身通讯协议作为语义,更符合该种协议的约定
- *** 有利于中间层对请求进行缓存
- *** 客户端进行相应封装后,代码都好维护,结构整洁
- ** 缺点
- *** 可能有奇葩运营商对某些HTTP状态码进行一些自定义行为(https可规避),如非 200 可能有广告
- *** 可能有业务异常较多,是 HTTP status 无法描述的,或不足以描述的,如登录失败 401 可能是账号不存在(引导注册),也可能是密码错误(清空密码框重新输入)
- *** 可移植性较差,当不是 HTTP 通信时,需要推翻所有客户端逻辑
- *** 某些技术欠佳、不负责任的客户端开发会认为,只要不是200一定是后端问题,认为接口不稳定
- *** 需要所有服务端开发者都能完全理解并实现 RESTful API,且客户端开发人员也能理解
- *** 非 200 响应,可能会被浏览器拦截处理
- * 方案二
- ** 优点
- *** 一些客户端开发希望是方案二,因为这样会让客户端的处理逻辑变简单(200以外全去捕获异常,200时再看status_code做不同处理)
- *** API响应码值语义分层明确,HTTP status 是为协议层的使用的,API 中可以自定义语义状态,互不影响
- *** 早些年,普遍都是这样做,符合习惯,兼容旧系统
- *** 一些客户端/前端/APP开发不懂 HTTP 协议,不知道状态码是什么,这样做更直白
- *** 一些客户端框架,响应不是200则抛异常,要么显示捕获、要么就得深入了解框架的设计,且该框架支持扩展
- ** 缺点
- *** 即使自定义了码值,仍然复用了 HTTP status 的 200
- *** 服务器端不一定真能保证总是返回200的状态码,一些框架中原始就抛出 401 / 403 / 404 / 500 等异常,需要服务端开发改造来规避
- // end::main[]
|