• 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文档中明确默认约束条件。

    HZERO 设计规范

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

    存在版本段时,租户ID放在版本段后面,不存在则放在版本段位置。Controller 方法中需通过 @PathVariable 获取租户ID参数。另外平台级的API操作多租户,在参数中传递租户ID即可,不在URI中区分。

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

    微服务中可能会同时存在平台级、租户级的功能,为了便于管理和维护方便,建议如下:

    注意:一个功能的API如果没有同时存在平台级和租户级的API,可以不进行区分

    对外提供接口的方法用 @Permission 注解标注

    Permission 注解包含如下参数:

    参数 说明
    code 权限编码,默认取方法名称
    level 接口层级,ResourceLevel.SITE(平台级)、ResourceLevel.ORGANIZATION(租户级)
    permissionLogin 是否登录可访问,设置为 true 时,只要用户登录就能访问,默认 false
    permissionPublic 是否公开接口,设置为 true 时,不需要登录就能访问,默认 false
    permissionWithin 是否内部接口,设置为 true 时,只允许服务内部调用,默认false
    tags 权限标签

    设计自检:

    服务合并API规范

    考虑到未来服务合并的可能性,尽量在开发 API 时,加上服务路由前缀,以避免服务合并时API冲突。

    例如合并 hzero-iam 服务和 hzero-platform 服务:

    @RequestMapping("/iam/v1/users")
    public class UserController {
    	
    }
    
    @RequestMapping("/hpfm/v1/companies")
    public class CompanyController {
    	
    }
    

    服务合并后,需禁止去掉服务路由前缀:

    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”}

    复杂条件查询

    Action 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>());
        }
    
    }
    

    数据返回及异常规范