api.adoc 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362
  1. // tag::main[]
  2. == Restful API 开发规范
  3. 推荐简单业务场景 / 开放接口使用 Restful 风格,业务复杂场景 / 内部接口可借鉴 Restful 的优点,不一定完全遵守(抽象所有资源),以更容易理解业务。
  4. === 统一响应体格式
  5. 返回值统一为json格式,最外层只包含 `code`、`msg`、`data` 三个字段,接口返回包含数据内容时,仅使用 `data` 字段。
  6. ----
  7. {
  8. "code":"错误码,成功为0",
  9. "msg":"返回的(错误)描述信息,不能有堆栈信息",
  10. "data":{数据}
  11. }
  12. ----
  13. ==== 响应举例
  14. ===== 空响应-成功:
  15. ----
  16. {
  17. "code":"0",
  18. "msg":"",
  19. "data":null
  20. }
  21. ----
  22. ===== 空响应-出错:
  23. ----
  24. {
  25. "code":"0xaabc100f",
  26. "msg":"username can't be null",
  27. "data":null
  28. }
  29. ----
  30. ===== 单值响应:
  31. ----
  32. {
  33. "code":"0",
  34. "msg":"",
  35. "data":{
  36. "username": "cnlym",
  37. "nickName": "产码机器"
  38. }
  39. }
  40. ----
  41. ===== 多值响应(分页)
  42. ----
  43. {
  44. "code":"0",
  45. "msg":"",
  46. "data":{
  47. "total":11, //总条数
  48. "pageNo": 1, //当前页
  49. "pageSize":20, //当前分页记录数
  50. "list": [{ //分页内容
  51. "userId": 1,
  52. "name": "admin"
  53. },{
  54. "userId": 2,
  55. "name": "leader"
  56. }]
  57. }
  58. }
  59. ----
  60. ===== 多值响应(不分页)
  61. ----
  62. {
  63. "code":"0",
  64. "msg":"",
  65. "data":{
  66. "total":11,
  67. "list": [
  68. { // 列表内容
  69. "userId": 1,
  70. "name": "admin"
  71. },
  72. {
  73. "userId": 2,
  74. "name": "leader"
  75. }
  76. ]
  77. }
  78. }
  79. ----
  80. == 统一 api 文档约束
  81. 为了便于对接,组件 `doc` 目录下须包含以下文件
  82. * api.json
  83. ** 统一使用 swagger json 文档
  84. *** 需要指定 info-title, description,version,contact
  85. *** `basePath` 对应 context 值,格式为:`/<appId>`
  86. * api.md
  87. ** api文档,包含如下,不描述则采用本文的推荐设计:
  88. *** 简介
  89. *** 认证
  90. *** 鉴权规则
  91. *** 安全传输与加密算法
  92. *** 返回值规则
  93. *** 空响应
  94. *** 单值响应
  95. *** 多值响应(分页)
  96. *** 多值响应(不分页)
  97. * ext.md
  98. ** swagger 返回值无法描述泛型等,额外标注说明
  99. * guidance.md
  100. ** 编程指引
  101. * appendix.md
  102. ** 附录,错误码、数据字典、翻译项、复杂参数解释
  103. * <appId>-api-sdk.jar
  104. ** 接口sdk
  105. === Restful 请求方法含义
  106. [cols="1,4"]
  107. |===
  108. | 类型 | 说明
  109. | GET | 从服务器取出资源
  110. | POST | 在服务器新建一个资源,修改一个资源或者删除一个资源
  111. | PUT | 在服务器更新资源(客户端提供改变后的完整资源)
  112. | DELETE | 从服务器删除资源
  113. | HEAD | 获取资源的元数据
  114. | PATCH | 在服务器更新资源(客户端提供改变的属性)
  115. | OPTIONS | 获取信息,关于资源的哪些属性是客户端可以改变的
  116. |===
  117. === 常用 HTTP 状态码
  118. [cols="1,3"]
  119. |===
  120. | HTTP状态码 | 说明
  121. | 200 OK | 服务器成功返回用户请求的数据。处理成功时默认该值。
  122. | 201 Created | 修改数据成功(返回之前已经入库)
  123. | 202 Accepted | 表示一个请求已经进入后台排队(异步任务/如批量异步删除)
  124. | 204 No Content | 表示请求成功但无返回值,可用于修改,删除
  125. | 301 Moved Permanently | 资源的URI已被永久更新
  126. | 302 Found | 资源URI重定向,与301类似(临时)
  127. | 304 Not Modified | 资源未更改,客户的缓存资源是最新的, 请客户端使用缓存(缓存),ES-ik插件的动态更新用到了这个
  128. | 400 Invalid Request | 用户发出的请求有错误,是无效请求,如字段映射不正确,参数不正确都可以用这个
  129. | 401 Unauthorized | 需要先进行认证(登录)
  130. | 403 Forbidden | 服务器拒绝执行,一般是由于权限不够,也可能ip进了黑名单等
  131. | 404 Not Found | 用户发出的请求是针对不存在的记录
  132. | 405 Method Not Allowed | 不允许执行目标方法,如只允许POGT却使用GET,响应中应该带有 Allow 头,内容为对该资源有效的 HTTP 方法
  133. | 406 Not Acceptable | 用户请求的格式不可得,如服务器返回 `abc.txt`,浏览器期望返回数据的MIME类型没有 `txt`
  134. | 410 Gone | 用户请求的资源被永久删除,且不会再得到
  135. | 422 Unprocesable entity | 参数格式正确,但语义错误(创建资源时,校验未通过),浏览器将它与400处理方式相同,因此也可以使用400
  136. | 429 Too Many Request | 请求过多,触发限流时,可用于反爬虫,418(触发彩蛋)也可用于反爬虫
  137. | 500 Internal Error | 服务器发生错误,用户将无法判断发出的请求是否成功
  138. | 503 Service Unavailable | 服务不可用状态,多半是因为服务器问题,例如CPU占用率大,等等
  139. |===
  140. ==== 丢失更新问题
  141. HTTP 201 可以包含一个ETag响应头字段,指示刚刚创建的请求变量的实体标签的当前值。ETag头字段可以在以后的条件请求中使用,以防止“丢失更新”问题。
  142. 当多人编辑资源而不了解彼此的更改时,会发生丢失的更新问题。在这种情况下,最后一个更新资源的人“获胜”,之前的更新将丢失。ETag可以与If-Match标头结合使用,让服务器决定是否应该更新资源。如果ETag不匹配,则服务器通过412 (Precondition Failed)响应通知客户端。
  143. 数据库中添加dataVersion,每次修改+1,可以使用该字段来避免并发更新问题。
  144. === 请求头约束
  145. ==== 命名规则
  146. 单词首字母大写,单词与单词间使用中划线(`-`)分割。如:`User-Agent`
  147. ==== 请求头保留字段
  148. [cols="1,1,1,3"]
  149. |====
  150. | 参数名 | 类型 | 必选 | 说明
  151. | *User-Agent* | string | √ | 终端类型
  152. | *Token* | string | √ | 接口认证使用:身份认证信息,采用 base64 编码。
  153. | X-Sid | string | × | 秘钥协商使用:安全会话 ID。
  154. | X-Dk | string | × | 秘钥协商使用:使用协商密钥加密的 `数据密钥` 。
  155. | User-Id | string | × | 用户 ID
  156. | *Trace-Id* | string | √ | 用于标识一笔业务的唯一序号,UUID 格式
  157. | *Span-Id* | string | √ | 用于标识某一阶段调用序号,32位字母或数字
  158. | Biz-Id | string | × | 业务标识
  159. | *X-B3-TraceId* | hex string | √ | UUID 格式
  160. | *X-B3-SpanId* | hex string | √ | 64 位值,采用小写 16 进制字符显示
  161. | *X-B3-ParentSpanId* | hex string | √ | 64 位值,采用小写 16 进制字符显示
  162. | X-B3-Sampled | boolean | × | `0` – 不采样, `1` – 采样
  163. | X-B3-Flags | string | × | `1` – debug,要记录本次所有调用链信息
  164. |====
  165. === 请求体约束
  166. 除文件和表单数据外,`POST`、`PUT` 等带请求体的方法,请求体统一为 `JSON` 格式,
  167. === 请求参数要求
  168. 参数须提供 `取值范围` 或 `格式限制` 或 `枚举限制`
  169. ==== 时间字段和格式
  170. * 当查询条件需要使用起止时间时,参数名统一采用 `beginTime` 和 `endTime`。
  171. * 采用 `ISO8601` 格式,使用带 Time-zone 的标准时间格式,如:`2020-7-13T00:00:00.000+08:00`;
  172. ===== 字符串和编码
  173. * 字符串的所有操作统一使用 UTF-8 编码。
  174. * 长度限制如下
  175. [cols="2,1"]
  176. |====
  177. | 场景 | 推荐长度
  178. | ID、标识、名称、别名 | 64, 128
  179. | 描述、备注、文件名(全路径) | 512, 1024
  180. | 详情 | 2048
  181. | 文本、文件 | 不限制
  182. |====
  183. ===== 常用字段约束
  184. 统一常用的参数名称
  185. ====== 分页查询参数
  186. demo: 每页20条记录,查询第一页,先按照 `username` 正序排序,再按照 `phoneNo` 逆序排序
  187. ----
  188. {
  189. "pageNo":"1",
  190. "pageSize":"20",
  191. "conditions": [
  192. {
  193. "property": "username",
  194. "like": "cn"
  195. },{
  196. "property": "level",
  197. "equal": "vip"
  198. }
  199. ],
  200. "orderBy":[
  201. {
  202. "property": "username",
  203. "direction": "asc"
  204. },{
  205. "property": "phoneNo",
  206. "direction": "desc"
  207. }
  208. ]
  209. }
  210. ----
  211. |====
  212. | 用途 | 参数名 | 举例
  213. | 查询页码 | pageNo | 1
  214. | 分页大小 | pageSize | 20
  215. | 排序参考 | sortBy | ["name"]
  216. | 排序规则 | order | "asc"
  217. |====
  218. 分页查询参数名称
  219. ==== 参数字段顺序
  220. * 标识性字段(id、name)
  221. * 必填字段(sex)
  222. * 常用字段(phone)
  223. * 描述性字段(note)
  224. * 通用型字段(updateTime)
  225. ==== 统一错误码格式
  226. 见 link:errorCode.html#错误码输出约束[错误码规范-错误码输出约束]
  227. === 版本号设计
  228. 软件中任何事物都有变的可能性,api 接口也是这样的,因此需要在 api 的请求路径中添加版本号。如 `/api/xxService/v2/**`。
  229. == 安全传输
  230. 参考 link:../security/negotiate.html[密钥协商]。
  231. == 编码注意事项
  232. * 定义统一返回值类 `BaseResponse`,且该类只能在接口层使用
  233. * 所有 Controller 层接口函数统一返回 `BaseResponse`,或使用全局返回值自动包装
  234. * 入参,返回值尽量不要有 `Map` 类
  235. * Controller 层进行 `DTO` 转换,不允许将 `DTO` 传递倒业务层
  236. * Controller 不要出现 Request,Response 这类对象
  237. * 一般不需要在这里打印日志,异常处理,使用统一的日志打印
  238. * 注意防枚举(id有顺序)、防重放
  239. ==== `Ajax` **VS** `Restful RPC api` 接口
  240. * 虽然两者都是 HTTP协议 JSON 格式
  241. * Restful 是无状态的,属于 RPC 的一种形式,职责为服务间通信。校验主要为 token 认证。
  242. * Ajax 是前后端局部刷新增加用户体验的。校验包含各种网络攻击。
  243. ==== `Restful with HTTP status` **VS** `统一返回 200,响应种自定义码值`
  244. Shoulder 种采用方案二,自定义返回值状态码,响应种 200 表示接口正常返回,4xx 表示客户端错误,前端开发先排差,5xx 表示服务端异常,后端开发先排差。
  245. ===== 方案一:只以HTTP状态码来表示状态
  246. * 200时候返回内容就是数据,最多有个分页
  247. > 分页也可以像 https://docs.github.com/en/free-pro-team@latest/rest#client-errors[github 那样] 放到header,响应内容更整洁
  248. * 只有在抛出Exception异常(即使业务逻辑上有问题,也抛出APIException异常)才返回像下面这样的响应:
  249. ----
  250. HTTP/1.1 405 Method Not Allowed
  251. Content-Type: application/json
  252. {"status_code": 405, "message": "Method 'DELETE' not allowed."}
  253. ----
  254. ===== 方案二:所有接口都返回200
  255. 响应体里包含:自定义码值、信息、数据
  256. {
  257. "status_code: 1000
  258. "message": "xxxxx"
  259. "result": {
  260. "id": 1
  261. ...
  262. }
  263. }
  264. ===== 两种方案优缺点比较
  265. * 方案一
  266. ** 优点
  267. *** 作为服务端更倾向于方案一,因为大多框架和各家提供的API都是这么干的,其实也很简单(先看状态码,然后直接根据状态码决定后续动作)
  268. *** 使用本身通讯协议作为语义,更符合该种协议的约定
  269. *** 有利于中间层对请求进行缓存
  270. *** 客户端进行相应封装后,代码都好维护,结构整洁
  271. ** 缺点
  272. *** 可能有奇葩运营商对某些HTTP状态码进行一些自定义行为(https可规避),如非 200 可能有广告
  273. *** 可能有业务异常较多,是 HTTP status 无法描述的,或不足以描述的,如登录失败 401 可能是账号不存在(引导注册),也可能是密码错误(清空密码框重新输入)
  274. *** 可移植性较差,当不是 HTTP 通信时,需要推翻所有客户端逻辑
  275. *** 某些技术欠佳、不负责任的客户端开发会认为,只要不是200一定是后端问题,认为接口不稳定
  276. *** 需要所有服务端开发者都能完全理解并实现 RESTful API,且客户端开发人员也能理解
  277. *** 非 200 响应,可能会被浏览器拦截处理
  278. * 方案二
  279. ** 优点
  280. *** 一些客户端开发希望是方案二,因为这样会让客户端的处理逻辑变简单(200以外全去捕获异常,200时再看status_code做不同处理)
  281. *** API响应码值语义分层明确,HTTP status 是为协议层的使用的,API 中可以自定义语义状态,互不影响
  282. *** 早些年,普遍都是这样做,符合习惯,兼容旧系统
  283. *** 一些客户端/前端/APP开发不懂 HTTP 协议,不知道状态码是什么,这样做更直白
  284. *** 一些客户端框架,响应不是200则抛异常,要么显示捕获、要么就得深入了解框架的设计,且该框架支持扩展
  285. ** 缺点
  286. *** 即使自定义了码值,仍然复用了 HTTP status 的 200
  287. *** 服务器端不一定真能保证总是返回200的状态码,一些框架中原始就抛出 401 / 403 / 404 / 500 等异常,需要服务端开发改造来规避
  288. // end::main[]