• REST API规约


    设计原则

    API根URL

    如果预期系统非常庞大,则建议尽量将API部署到独立专用子域名(例如:“api.”)下;如果确定API很简单,不会进一步扩展,则可以考虑放到应用根域名下面(例如,“/api/”)。

    URI末尾不要添加“/”

    多一个斜杠,语义完全不同,究竟是目录,还是资源,还是不确定而多做一次301跳转?尽量保持URI结构简洁、语义清晰。

    禁止在URL中使用“_”

    目的是提高可读性,“”可能被文本查看器中的下划线特效遮蔽。建议使用连字符“-”替代下划线“”,使用“-”提高URI的可读性。

    禁止使用大写字母

    RFC 3986中规定URI区分大小写,但别用大写字母来为难程序员了,既不美观,又麻烦,同样的原则:建议使用连字符“-”连接不同单词。

    不要在URI中包含扩展名

    应鼓励REST API客户端使用HTTP提供的格式选择机制Accept request header。

    建议URI中的名称使用复数

    为了保持URI格式简洁统一,资源在URI中应统一使用复数形式,如需访问资源的一个实例,可以通过资源ID定位(@PathVariable)。

    如何处理关联关系?

    建议URI设计时只包含名词,不包含动词

    每个URI代表一种资源或者资源集合,因此,建议只包含名词,不包含动词。

    那么,如何告诉服务器端我们需要进行什么样的操作?CRUD? 答案是由HTTP动词表示。

    尽量减少对第三方开发人员的随意约束

    非常重要的一点:让第三方开发人员自己指定排序过滤器、返回结果集的约束条件;但强烈建议服务器端设置默认单页数量,否则,如果无限制,很可能造成服务器资源及网络资源过度消耗,响应缓慢,网络丢包等异常情况;同时,需要在API文档中明确默认约束条件。

    租户级URI设计时需要包含租户ID

    存在版本段时,租户ID放在版本段后面,不存在则放在版本段位置,后续Controller类中需要做好使用{organizationId}进行获取,另外平台级的API操作多租户,在参数中传递租户ID即可,不在URI中区分。

    平台级、租户级、项目级API类名规范

    微服务中可能会同时存在平台级、租户级、项目级的功能,为了便于管理和维护方便,建议在项目代码包结构加以区分,为了与猪齿鱼架构API层级对应,建议如下:
    租户级API类结构

    注意:HZERO相关产品开发只会用到平台级和租户级,一个功能的API如果没有同时存在平台级和租户级的API,可以不进行区分。如:

    另外,程序代码中需要进行申明
    API层级申明
    具体参考: API层级说明

    设计自检:
    1、哪些能是public的,比如登录
    2、哪些能是login的,比如查询菜单、公告等
    3、哪些租户级会用全局,主要针对查询功能,比如租户需要查全局币种,平台也要查,这是两个controller,可调用同一个查询方法,服务端代码实现复用

    HTTP响应设计

    当客户端通过 API 向服务器发起请求时,无论请求是成功、失败还是错误,客户端都应该获得反馈。HTTP 状态码是一堆标准化的数值码,在不同的情况下具有不同的解释。服务器应始终返回正确的状态码。
    完整状态码参见:Status Code Definitions

    2xx (成功类别)

    这些状态代码表示请求的操作已被服务器接收到并成功处理。

    3xx (重定向类别)

    4xx (客户端错误类别)

    这些状态代码表示客户端发起了错误的请求。

    5xx(服务器错误类别)

    表示服务器端发生异常。

    API版本管理

    总体建议

    1. 建议通过URI指定服务版本,版本采用字符“v”+数字主版本号,例如,/v1/xxxs
    2. 建议版本控制在资源层面,也即Controller维度
    3. 服务后端分包建议规则如下:
    1. API升级建议

    URL中指定版本

    1、URI上添加版本号:例如,https://api.example.org/v1/users
    2、参数中添加版本号: 例如,https://api.example.org/users?v=1.0

    好处:

    坏处:

    Action 命名规范

    类别

    Description Action Name HTTP Mapping HTTP Request Body HTTP Response Body
    查询所有 list GET N/A Resource* list
    获取单个资源 query GET N/A Resource*
    创建单个资源 create POST Resource Resource*
    更新单个资源 update PUT Resource Resource*
    删除单个资源 delete DELETE N/A Empty

    List

    List 方法接受一个 Collection id 和0或多个参数作为输入,并返回一个列表的资源。

    Query

    Query 方法接受一个 Resource name 和0或多个参数,并返回指定的资源。

    Create

    Create 方法接受一个 Collection id ,一个资源,和0或多个参数。它创建一个新的资源在指定的父资源下,并返回新创建的资源。

    Update

    Update 方法接受一个资源和0或多个参数。更新指定的资源和其属性,并返回更新的资源。

    Delete

    Delete 方法接受一个Resource Name 和0或多个参数,并删除指定的资源。

    自定义方法

    自定义的方法应该参考5个基本方法。应该用于基本方法不能实现的功能性方法。可能需要一个任意请求并返回一个任意的响应,也可能是流媒体请求和响应。

    可以对应a resource, a collection 甚至 a service。

    批量添加

    Description Action Name HTTP Mapping HTTP Request Body HTTP Response Body
    批量添加 batchCreate POST /batch-create Resource* list Resource IDS

    批量删除

    Description Action Name HTTP Mapping HTTP Request Body HTTP Response Body
    批量删除 batchDelete POST /batch-delete Resource IDS Empty

    更新单个资源中的属性

    Description Action Name HTTP Mapping HTTP Request Body HTTP Response Body
    更新资源的状态 updateAttribute POST /:attribute?value= N/A {“key”:"",“value”:""}
    更新用户的年龄 updateAge POST /v1/users/1/age?value=20 N/A {“key”:“age”,“value”:“20”}

    对资源执行某一动作

    Description Action Name HTTP Mapping HTTP Request Body HTTP Response Body
    对资源执行某一动作 customVerb POST /custom-verb N/A *
    取消某种操作 cancel POST /cancel N/A Boolean
    从回收站中恢复一个资源 undelete POST /v1/projects/1/undelete N/A Boolean
    检查项目是否重名 checkName POST /v1/projects/1/check?name= N/A

    查询某一资源的单个属性

    Description Action Name HTTP Mapping HTTP Request Body HTTP Response Body
    查询资源的某属性 queryAttribute GET /:attribute N/A {“key”:"",“value”:""}
    查询用户的年龄 queryAge GET /v1/users/1/age N/A {“key”:“age”,“value”:“25”}
    查询用户下的项目 queryProjects GET /v1/users/1/projects N/A {“key”:“projects”,“value”:[]}

    查询collection 的数量

    Description Action Name HTTP Mapping HTTP Request Body HTTP Response Body
    查询Collection 的数量 count GET /count N/A {“key”:"",“count”:""}
    查询组织的数目 count GET /v1/organizations/count N/A {“key”:“organizations”,“count”:“100”}
    查询用户下的所有项目数量 countProjects GET /v1/users/1/projects/count N/A {“key”:“projects”,“count”:“100”}

    复杂条件查询

    Demo

    @RestController("/v1/users")
    public class UserController {
    
        @GetMapping
        public ResponseEntity<?> list() {
            return Results.success(new ArrayList<User>());
        }
    
        @GetMapping("/{id}")
        public ResponseEntity<?> query(@PathVariable("id") String id) {
            return Results.success(new User(id));
        }
    
        @PostMapping
        public ResponseEntity<?> create(@RequestBody User user) {
            return Results.success(user);
        }
    
        @PutMapping
        public ResponseEntity<?> update(@RequestBody User user) {
            return Results.success(user);
        }
    
        @DeleteMapping
        public ResponseEntity<?> delete(@RequestBody User user) {
            return Results.success();
        }
    
        @PostMapping("/batch-create")
        public ResponseEntity<?> batchCreate(@RequestBody List<User> users) {
            return Results.success(users);
        }
    
        @PostMapping("/batch-delete")
        public ResponseEntity<>> batchDelete(@RequestBody List<User> users) {
            return Results.success();
        }
    
        @PostMapping("/age")
        public ResponseEntity<?> updateAge(@RequestBody User user) {
            return Results.success(user);
        }
    
        @PostMapping("/undelete")
        public ResponseEntity<?> undelete(@RequestBody User user) {
            return Results.success();
        }
    
        @PostMapping("/check")
        public ResponseEntity<?> checkName(@RequestParam("name") String name) {
            return Results.success();
        }
    
        @GetMapping("/{id}/age")
        public ResponseEntity<?> queryAge(@PathVariable("id") String id) {
            return Results.success(18);
        }
    
        @GetMapping("/{id}/name")
        public ResponseEntity<?> queryByUserIdAndName(@PathVariable("id") String id, @RequestParam("name") String name) {
            return Results.success(new User(id, name));
        }
    
        @GetMapping("/{id}/projects/count")
        public ResponseEntity<?> countProjects(@PathVariable("id") String id, @RequestParam("name") String name) {
            return Results.success(1);
        }
    
        @GetMapping
        public ResponseEntity<?> listByOptions(@RequestBody Map<String, Object> options) {
            return Results.success(new ArrayList<User>());
        }
    
    }
    

    数据返回及异常规范