一、RESTful API设计规范
参考知乎上的《RESTful API最佳实践》一文,总结的RESTful API设计规范如下:
1.URI
应该将API部署在专用域名之下:https://api.example.com
不用大写
用中杠-不用下杠_;
参数列表要encode;
URI中不应该出现动词,动词应该使用HTTP方法表示,但是如果无法表示,也可使用动词,例如:search没有对应的HTTP方法,可以在路径中使用search,更加直观;
URI中的名词表示资源集合,使用复数形式;
-
虽然/在URI中表达层级,但是避免为了追求REST导致层级过深,适当使用参数表示。
GET /comments/tid?tid=1&page=1
2.Request:通过标准http方法对资源CRUD
-
GET:查询资源
GET /comments //获取所有评论 GET /comments/tid/1 //获取文章tid为1的所有评论
-
POST:创建资源
POST /comments/tid/1 //为tid为1的文章创建评论
-
PUT:更新资源
PUT /comments/cid/like/1 //为cid为1的评论点赞
-
DELETE:删除资源
DELETE /comments/cid/1 //删除cid为1的评论
3.Response
采用JSON,不要使用XML
默认情况下JSON外层不需要嵌套大括号,API需要支持JSONP跨域访问或者客户端无法访问HTTP header才需要加上嵌套大括号
默认情况下不要过滤API输出中的空格,并且要支持gzip
4.API版本控制
- 在URI中存放:GET /v1/comments;
- 客户端在Accept Header中存放:Accept: application/vnd.github.v3+json,服务器自定义Header返回当前版本信息:X-GitHub-Media-Type: github.v3; format=json(GitHub在用);
- 以上两种方法根据情况选择,Github用的方式是REST中所要求的方式;
- 测试API和正式API要进行区分,方式通过如上两种方式实现。
5.速度限制
为了避免请求泛滥,给API设置速度限制很重要。为此 RFC 6585 引入了HTTP状态码429(too many requests)。加入速度设置之后,应该提示用户,至于如何提示标准上没有说明,不过流行的方法是使用HTTP的返回头。
下面是几个必须的返回头(依照twitter的命名规则):
- X-Rate-Limit-Limit :当前时间段允许的并发请求数
- X-Rate-Limit-Remaining:当前时间段保留的请求数。
- X-Rate-Limit-Reset:当前时间段剩余秒数
为什么使用当前时间段剩余秒数而不是时间戳?
时间戳保存的信息很多,但是也包含了很多不必要的信息,用户只需要知道还剩几秒就可以再发请求了这样也避免了clock skew问题。
6.缓存
HTTP提供了自带的缓存框架。你需要做的是在返回的时候加入一些返回头信息,在接受输入的时候加入输入验证。基本两种方法:
- ETag:当生成请求的时候,在HTTP头里面加入ETag,其中包含请求的校验和和哈希值,这个值和在输入变化的时候也应该变化。如果输入的HTTP请求包含IF-NONE-MATCH头以及一个ETag值,那么API应该返回304 not modified状态码,而不是常规的输出结果。
- Last-Modified:和etag一样,只是多了一个时间戳。返回头里的Last-Modified:包含了 RFC 1123 时间戳,它和IF-MODIFIED-SINCE一致。HTTP规范里面有三种date格式,服务器应该都能处理。
7.覆盖HTTP方法
一些HTTP客户端只支持GET和POST请求。为了能够加强这些客户端的访问能力,API需要能够覆盖HTTP方法。尽管这里没有任何强制的标准,但流行的做法是API会接收一个请求头X-HTTP-Method-Override,它的值可以是PUT、PATCH或者DELETE三者之一。
注意,用来覆盖HTTP方法的header只能在POST请求中被接受。GET请求永远不能修改服务器上的数据。
8.过滤信息
如果记录数量很多,服务器不可能都将它们返回给用户。API应该提供参数,过滤返回结果。
下面是一些常见的参数:
?limit=10:指定返回记录的数量
?offset=10:指定返回记录的开始位置。
?page=2&per_page=100:指定第几页,以及每页的记录数。
?sortby=name&order=asc:指定返回结果按照哪个属性排序,以及排序顺序。
?animal_type_id=1:指定筛选条件
9.错误处理
就像HTML的出错页面向访问者展示了有用的错误消息一样,API也应该用之前熟悉易读的格式来提供有用的错误消息。错误的表现形式应该跟其他资源保持一致,只是用一些自己的字段。
API应该一直返回合理的HTTP状态码。API错误一般情况下分成两类:代表客户端错误的400系列状态码和代表服务端错误的500系列状态码。API至少把所有400系列错误统一用易读的JSON格式来展示。如果可能(比如,如果负载均衡和反向代理能够创建自定义错误内容的话),500系列的状态码也这么弄。
JSON错误内容应该为开发者提供一些东西 - 有用的错误消息,唯一的错误码(通过它可以在文档中找到更多错误细节),可能的话提供错误细节描述。用JSON格式来输出错误看起来这样:
{
"code" : 1234,
"message" : "Something bad happened :(",
"description" : "More details about the error here"
}
对于PUT、PATCH和POST的请求进行的校验错误需要嵌套多个字段。最佳做法是用固定的错误码来表示校验失败,然后在额外的errors字段中提供错误的细节,像这样:
{
"code" : 1024,
"message" : "Validation Failed",
"errors" : [
{
"code" : 5432,
"field" : "first_name",
"message" : "First name cannot have fancy characters"
},
{
"code" : 5622,
"field" : "password",
"message" : "Password cannot be blank"
}
]
}
10.HTTP状态码
HTTP定义了很多有意义的状态码,你可以在你的API中使用。这些状态码可以帮助API消费者用来路由它们获取到的响应内容。整理了一个你肯定会用到的状态码列表:
- 200 OK - 对成功的GET、PUT、PATCH或DELETE操作进行响应。也可以被用在不创建新资源的POST操作上
- 201 Created - 对创建新资源的POST操作进行响应。应该带着指向新资源地址的Location header)
- 204 No Content - 对不会返回响应体的成功请求进行响应(比如DELETE请求)
- 304 Not Modified - HTTP缓存header生效的时候用
- 400 Bad Request - 请求异常,比如请求中的body无法解析
- 401 Unauthorized - 没有进行认证或者认证非法。当API通过浏览器访问的时候,可以用来弹出一个认证对话框
- 403 Forbidden - 当认证成功,但是认证过的用户没有访问资源的权限
- 404 Not Found - 当一个不存在的资源被请求
- 405 Method Not Allowed - 所请求的HTTP方法不允许当前认证用户访问
- 410 Gone - 表示当前请求的资源不再可用。当调用老版本API的时候很有用
- 415 Unsupported Media Type - 如果请求中的内容类型是错误的
- 422 Unprocessable Entity - 用来表示校验错误
- 429 Too Many Requests - 由于请求频次达到上限而被拒绝访问
11.认证
RESTful API应该是无状态。这意味着对请求的认证不应该基于cookie或者session。相反,每个请求应该带有一些认证凭证。
如果一直使用SSL,认证凭证可以简单的使用随机生成的access token,把其做为HTTP Basic Auth中user name字段的值传给API。这么做的好处是可以通过浏览器访问 - 如果浏览器从服务器收到401 Unauthorized状态码,它将会弹出一个对话框让人输出认证凭证。
当然,这种基于token来进行基本认证的方法只能当用户从API管理后台拷贝了一个token到自己的代码中才行。如果搞不到token,只能使用OAuth 2来把安全token传递给第三方。OAuth 2使用Bearer token,并且也是基于SSL来保证传输安全。
支持JSONP的API可能需要第三种方法来实现认证,因为JSONP的请求没法发送HTTP Basic Auth凭证或者Bearer token。这种情况下,可以使用一个额外的查询参数access_token。注意:使用查询参数来传递token存在一个固有的安全隐患,因为大多数web服务器会在服务器日志中保存查询参数。
不管怎么样,以上三种方法是用来在API之间传输token的方法。实际传输的token可以是一样的。
12.使用SSL
一定要使用SSL。没有例外。如今,你的web API可以从任何有互联网的地方(像图书馆,咖啡馆,机场等等)被访问到。这些地方并不都是安全的。很多地方根本没有对网络连接进行加密,如果认证凭证被劫持的话,这样访问者很容易被窃听或者被冒充。
一直使用SSL的另一个优势是,加密的连接简化了用户认证的工作 - 你可以使用简单的access token,而不需要对每个API请求进行签名。
需要注意的一件事是以非SSL的形式访问API的URL。不要把请求跳转到它们的SSL版本上。直接抛出一个严重错误!
13.Hypermedia API
RESTful API最好做到Hypermedia,即返回结果中提供链接,连向其他API方法,使得用户不查文档,也知道下一步应该做什么。
比如,当用户向http://api.example.com的根目录发出请求,会得到这样一个文档。
{"link": {
"rel": "collection https://www.example.com/comments",
"href": "https://api.example.com/comments",
"title": "List of comments",
"type": "application/vnd.yourformat+json"
}}
上面代码表示,文档中有一个link属性,用户读取这个属性就知道下一步该调用什么API了。rel表示这个API与当前网址的关系(collection关系,并给出该collection的网址),href表示API的路径,title表示API的标题,type表示返回类型。
Hypermedia API的设计被称为HATEOAS。
在进行分页查询时可以返回下一页的URI,如果没有说明服务器已经取到最后一条数据了,客户端可以减少不必要的请求以及URI的构造,建议在分页的情况下使用。
二、构建一个RESTful API
首先,在Spring Boot中我们会使用到这些注解。
- @Controller: 修饰class,用来创建处理http请求的对象
- @RestController:Spring4之后加入的注解,原本在@Controller中返回json需要@ResponseBody来配合,如果直接用@RestController就不需要再配置,默认返回json格式,所以一般直接使用这个
- @RequestMapping:配置url映射
接下来,我们设计RESTful API接口如下:
请求类型 | URL | 功能说明 |
---|---|---|
GET | /girls | 查询所有女生 |
POST | /girls | 创建一个女生 |
GET | /gilrs/id | 根据id查询一个女生信息 |
PUT | /gilrs/id | 根据id更新一个女生信息 |
DELETE | /gilrs/id | 根据id删除一个女生信息 |
新建Girl实体类:
public class Girl {
private Integer id;
private String name;
private String cupSize;
//省略setter和getter..
}
创建GirlController类
@RestController
public class GirlController {
//创建线程安全的Map
static Map<Integer,Girl> girls = Collections.synchronizedMap(
new HashMap<Integer, Girl>()
);
@GetMapping(value = "/girls")
public List<Girl> getGirls(){
List<Girl> res = new ArrayList<Girl>(girls.values());
return res;
}
@PostMapping(value="/girls")
public String postUser(@ModelAttribute Girl girl) {
girls.put(girl.getId(), girl);
return "success";
}
@GetMapping(value="/girls/{id}")
public Girl getUser(@PathVariable Integer id) {
return girls.get(id);
}
@PutMapping(value="/girls/{id}")
public String putUser(@PathVariable Integer id, @ModelAttribute Girl girl) {
Girl u = girls.get(id);
u.setName(girl.getName());
u.setCupSize(girl.getCupSize());
girls.put(id, u);
return "success";
}
@DeleteMapping(value="/girls/{id}")
public String deleteUser(@PathVariable Long id) {
girls.remove(id);
return "success";
}
}
运行程序,就能够简单的实现了这个RESTful API的功能。可以在POSTMAN程序中进行简单的测试,但是我们会发现一个严重的问题,我们写的这个程序没有数据,所以接下来我们要来了解如何在Spring Boot中使用数据库。