《你不知道的 Java》💘 什么是好的 Web Api 设计 (第二章)

书接上文

查询的定义

依据上一章节内容的内容,REST LEVEL2 的查询只需要使用 GET 方法与 URI 定位到相应资源就可以了。但业务中往往涉及更多细节,比如查询的参数。

查询参数

我们所说的查询参数是指在 http method 为 GET 的查询 api 中,使用 urlencode 的传参方式在 URI 的末尾 ? 后面添加的相应参数。

如上所述,first-name 为你的查询参数对应的 key 值。通常我们会根据不同的业务信息,使用对应的 key 名称。不过,偶尔你也会看到下面这样的查询参数。

q 是 query 的缩写。为什么这里不使用 name 或某个业务 key 名称?因为 q 往往表示范围搜索。上述第一个示例表示搜索包含 jack 词组的所有用户列表。而第二个示例表示名称为 jack 的所有用户列表。

除了 instagram,类似比如 bing 也是这样做的。

https://www.bing.com/search?q=uriEncode(content)

当 uri 中存在无法直接使用的字符时,会用一种百分号编码的方式对字符进行处理。例如%E3%81 等。你可能会认为使用 ASCII 字符集就可以高枕无忧了。但是应该各位注意,%、&、+等字符也需要百分号编码。而空格符号在百分号编码中会被编码为+号。

查询参数与 URI 如何取舍

下面这两个示例,你觉得哪个更好?

A 与 B 的区别在于 A 的设计将查询参数放到了 URI,而 B 的设计保持了查询参数的原本位置。可能在目前市面上 B 的设计更为常见一点。
类似于这样的查询参数与 URI 取舍的问题,可以采用下面这 2 个决策帮助进行判断。

  • 参数是否表示唯一的资源所需的信息
  • 参数是否和资源相关

如果参数能够定位到互联网上唯一的资源,则应该将查询参数放到 URI 中。这是基于 URI 的根本含义——统一资源定位符含义来进行设计的。

若查询参数无法配合 URI 定位,或要做的事和资源本身(上例中即为 user)无关的话,则应该放到查询参数位置。

所以在满足上述条件的情况下, A 在学术上优于 B。

查询的别名

某日你接到一个新需求:已登录的用户可以通过 api 查询用户个人详情。针对
此需求很容易设计出以下的代码:

response

{
  "userId": "1987203",
  "userName": "userA",
  "userPassword": "12345"
}

回想一下你是不是设计过这样的端点?很可惜,你的代码实际上引发了一个巨大的安全隐患。 登录用户可以传入任何用户 id 即能查询到任意用户的数据。

针对这样的需求,在设计 api 时应该使用别名来代替用户 id 的传入,防止查询用户和查询自身这两个功能混淆,导致信息泄露的问题。

像上述示例中的 api,客户端访问以 me 或者 self 单词结尾的端点来获取个人信息。而针对其他用户的信息获取我们才采用下面这样的端点设计。

响应的设计

相对查询来说响应的设计更考究工程师的细节功底。

性别的设计

性别有两大主流设计。

  1. 使用数值来标识对应的性别。
  2. 使用 male、female。

实际上无论使用哪种都是可以接受的。但是你需要知道 14 年 facebook 的系统里性别就拥有了 50 种以上。(而川普上台让这一切又打回了原型 :rofl:)所以如果考虑多元化那使用 2 比 1 的可扩展性更强一些。

日期格式的设计

在 Http 协议的定义中,http header 中通常会使用 UTC 来标识 Http 的时间。因此在设计 api 时,也推荐使用时区 +00:00。相对于 epochtime 我更喜欢 RFC 3339 的日期格式,他更易读并且能够体现出时区的概念。而若要使用 UTC 时间的话,则在后面加上 Z 标识即可。

2017-11-11T13:00:12+00:00
2017-11-11T13:00:12Z
2017-11-11T10:00:00-00:00

-00:00 表示时区不明。这种表示很少见,但是正因为少见所以需要重点掌握。

整数的设计

计算机中 int 占用 4 个字节。如果使用 unsigned 无符号类型的话,则能表示 0 到 4294967295。如果你设计的是一个 SNS 网站,一定要避免用 int 作为返回值来标识用户的 id 或者其他信息,因为这点空间是完全不够的。将信息量较大的数据统统使用 long 甚至是 string 来表示。

分页查询的设计

相对位置分页

分页查询的重点在于分页参数,通常来说 limit=50&offset=100 很常见。但是由于查询往往会涉及到数据库的读操作,而在数据量很大的情况下基于 limit 与 offset 的查询组合非常可怕。如 limit=50000&offset=10000 这样的查询请求对服务器和客户端都是灾难。

绝对位置分页

有一种绝对位置分页的方法可作为你的备选项。它没有使用「从头开始第几条」的描述方法,而是指定了某个 ID 或者日期之前的条件。这样我们查询数据库时可以使用范围匹配来查询出数据返回到客户端,避免了使用数据库的分页功能从而造成性能隐患。

上面的端点用于请求查询17年8月15日之前注册的所有用户的信息列表。服务器端对于这样的请求 select * from user where time < 2017-08-15T00:00:00 使用即可解决问题,避免了从 1 开始计数的扫表操作。

有关分页的内容之前在下面的帖子种详细讨论过,这里不在赘述。

写在最后

  • 我是 Chuck1sn,一个长期致力于现代 Jvm 生态推广的开发者。
  • 您的回帖、点赞、收藏、就是我持续更新的动力。
  • 举手之劳的一键三连,对我来说是莫大的支持,非常感谢!
  • 关注我的账号,第一时间收到文章推送。

补充
小雨(pcb_77) 佬友提到了还有一种既符合标准又能解决复杂搜索请求的方法,我把佬友的回复粘贴到本帖中来,供大家查阅使用。

对于某种复杂的搜索请求,可以拆分成两个:

  1. 先使用 POST request 先行创建 query parameters. 甚至可以返回对应的查询请求 URL。这解决了某些特别复杂的查询逻辑,在前端拼装查询参数不便的问题。
  2. 接着,再由客户端调用上面得到的 url,这个时候就是直接的 GET 请求。
32 个赞

来啦,前排支持佬

2 个赞

太强了,大佬!

3 个赞

太强了佬

2 个赞

感谢分享

2 个赞

感谢分享

2 个赞

关于日期,目前一直用的 2025-01-01 00:00:00 这种格式与前端做的交互 :sweat_smile:

4 个赞

有一个疑问,就拿用户来说:
/user/info GET 表示获取用户信息
/user/info POST 注册用户

在浏览器 network 调试过程中,这两个请求都是 /user/info
难以区分。 点开每个请求才能看到 method 。那这种设计 是好还是不好呢?

3 个赞

浏览器当然可以看到方法,右键一下增加一个选项不就有了。

关键还是这里的思路问题哈,不应该从工具反推标准是否合适。应该是工具去适应标准。况且 RESTful 的标准这么久了,也不是啥复杂的玩意儿,正常工具肯定能提供,也应该提供的。

而且除了浏览器,还有很多其他的工具,都是可以看到 method 的。

2 个赞

太强了!学习一下

用这个相当于默认当地时区。纯本地化项目其实也可以,就是大家都知道是这个时间。但是这种时间字符串你如果传给别的平台,或者放到别的库里面去,特别如果这个库是海外全世界的库有可能就会有问题。

当然,纯本地化项目,没有太多第三方组件集成,默认就用这个时间也可以。

欢迎佬友又来资瓷啦。

支持一下

1 个赞

受教了佬,打算出书吗。

1 个赞

太强了大佬!

2 个赞

大佬 写的很棒tieba_001,佬我能发个文章补充下RESTful吗

1 个赞

哈哈,后续可能会考虑把你不知道的系列弄成一个小册子。这个系列还有很多文章这只是冰山一角。。。

目前的话主要是推广一些现代 Java 技术栈,希望国内的 Java 生态能够多尝试新东西。签名里面有对应的项目(开源的有 Github 仓库)佬有兴趣可以看看。

看到佬的分享,我pia一下进来直接点赞,之前买了佬的模板,我没有怎么用呢

1 个赞

好的佬,已关注,让49年入java的小弟多学习学习。

1 个赞

佬友,当然没问题,尽管发。