2019 2019年7月25日 8 min · 2.6k chars by yxc023
项目接口设计实践
前言
项目不同, 项目提供的接口功能也不同, 能否有一套比较通用切良好的规则, 来指导我们对项目接口的宏观设计和细节设计?
本文是我在项目中设计 api 接口的一些实践经验.
接口
宏观战略
接口组织结构
一个项目中, 接口组织结构应该是良好干净的. 接口不是想写哪里写哪里, 接口需要有功能分类, 业务分类, 协议分类, 然后分门别类的组织在不同的文件夹里.
按照功能作用分::
- 系统间服务接口
- 前端接口
按照业务分::
- 业务模块 A
- 业务模块 B
按照协议分::
- dubbo
- http
很多人多多少少想过这些分类, 但很少想过要把接口按照这些组织好. 我给出一个接口的组织目录
application
+-- api // 面向服务的接口
| +-- dubbo // dubbo 协议的接口
| | +-- order // 按照业务化分, order 模块的接口
| | +-- product
| | \-- module C
| \-- http // http 协议的接口
+-- controller // 面向前端的接口
| +-- web // ajax 请求接口
| | +-- order // 按照业务化分
| | \-- product
| +-- page // 页面请求接口
| +-- manage // 管理用的 ajax 和 页面接口.
| +-- web
| \-- page
\-- callback // 接收回调的接口
- 首先按照面向服务, 面向前端进行化分.
** 验证方式不同. 面向前端的接口通常要认证登录状态; 面向服务的接口通常是接入验证. ** 使用者不同. 前端接口调用者是用户, 请求的数据通常更贴近展示, 面向展示设计接口, 当发生异常时, 需要返回一些用户可读的信息; 服务接口使用者是系统代码, 请求的数据更贴近领域模型, 根据自身领域来暴露服务, 发生异常时, 需要返回明确的状态码, 来供调用方程序判断.
- 在面向服务的接口中, 提供了不同协议的接口. 不同协议的接口中, 又按照不同的模块化分.
- 在面向前端的接口中, 分为
web和page两种, 分别应对 ajax 请求和页面请求. - 在面向前端的接口中,
manage包里为管理后台的接口. 因为管理后台通常跟前台的请求要求的身份权限不同. - 独立出
callback类型的接口, 这种接口往往按照回调发起方的接口规范来.
以上, 我们通过接口的受众和类型, 制定了一套接口的组织形式.
目的是保持清晰, 避免混用. 因为我们经常会在接口层做一些通用的操作, 比如日志, 性能打点, 认证等, 对于不同的功能和作用的接口是不同的, 做划分之后, 我们可以分别处理.
上面的目录划分的真实代码示例, 可以在 https://github.com/yxc023/blueprint/tree/master/blueprint-application/src/main/java/com/yangxiaochen/blueprint[blueprint project] 找到.
细节战术
通用
- 读接口无副作用, 不要返回业务异常或错误码
获取信息的时候, 有就是有, 没有就是没有. 比如, 当调用
getOrder(orderId)接口时, 没有数据就返回null.而不要返回一个错误叫做
订单不存在.
- 面向服务的写接口声明会发生的各种异常和错误码
让异常类型中包含
code,message,tip,level,data. 让异常能够传递更多信息.see link:https://github.com/yxc023/expressive-exception[A More Expressive Exception Library, 更有表达力的异常类库]
- 面向服务的接口设计应该以自身业务另约需要暴露的功能为出发点, 而不是尊奉需求方的要求.
站在自身角度来思考我能够提供什么服务, 标准能力.
比如作为订单管理系统, 提供
创建订单,通用查询订单的 api 接口.但是对于使用方想要
查询女性用户创建的订单, 这种显然就不是一种标准能力.
- 设计完之后, 要站在使用者的角度试用自己的接口, 看是否有问题.
为了保证自己设计和提供的标准服务 api 不是闭门造车, 能够符合使用者的要求, 要站在使用者角度, 来尝试使用自己的接口, 看是否符合预期.
- 接口给出粗粒度的数据
跟项目里的方法接口不同, 在对外服务接口上要提供粗粒度的数据. 有时我们的数据模型存放在多个表里, 或者多个实体形成了一个聚合. 那么在返回数据时, 要想想一下使用者拿到数据后的使用, 提供足够的数据, 避免接口太零碎需要多次访问.
通过
GraphQl可以一定程度解决一些接口数据层复杂度的问题.
面向服务
http
- 服务接口 path 为
/api/** - 通过 header 传递额外信息. 比如 appId, timestamp, 签名信息等.
- 写接口使用
POST, 接收参数类型为application/json. - 对
POST的写接口进行验签时, 由于 content 是 json 类型, 格式层次复杂, 不能把每个字段拿出来加入到签名中做校验, 所以把 content 的内容做哈希算法签名, 来做为一个校验字段, 而不用使用 content 里单独的字段来做. - 使用统一的
ApiResult对象封装结果返回.ApiResult包含code,message,tip,data等字段. - 写接口的返回值中的
code应为字符串类型, 用于更有表达力的表明各种异常状态. - response status code 使用. 参考 https://tools.ietf.org/html/rfc7231#section-6.1[RFC 7231] ** 正常和业务异常 - 200 ** 参数校验错误 - 400 ** 验签未通过 - 401 ** 验签通过但是无权限使用接口 - 403 ** 意料外异常 - 500 ** 限流, 熔断, 拒绝服务 - 503
dubbo
- dubbo 接口的设计理念: 尽量使 rpc 调用看上去跟调用本地方法一样.
结果无需再做封装, 成功就是成功了, 失败通过异常类传递.
- 读接口直接返回数据, 无需额外封装. 读接口不抛业务异常, 有异常就认为 bug.
[source, java]
OrderDTO getByOrderId(Long orderId);
- 写接口返回数据无需额外封装. 业务异常信息通过
Exception抛出, 并包含异常code.
[source, java]
Long createOrder(OrderCreateParam orderCreateParam) throws ApiException;
声明要抛出的异常.
- dubbo 接口实现中, 要做异常全局处理, 转化为
ApiException. 并将ApiException放到发布的 api 包中. 否则无法在 dubbo 客户端对异常反序列化.
实现 dubbo 的 filter 来做全局的异常处理, 将系统内的异常, 转化为 api 包中的
ApiException.
- dubbo 发布的 api 包中, 应包含接口用到的常量, 数据对象, 异常 code 常量.
下面的例子中这个 module 就要作为一个 api jar 包发布出去.
see link:https://github.com/yxc023/blueprint/tree/master/blueprint-api-dubbo/src/main/java/com/yangxiaochen/blueprint/api/dubbo[api jar]
包含了
ExceptionCodes,constant,dto等对象.
- dubbo 接口不应返回枚举类型以及包含枚举类型的对象. 应该转成字符串或数字常量返回. 避免客户端因为服务端枚举类变化导致反序列化的失败.
比如在项目业务逻辑中,
Order中的orderStatus字段是OrderStatusEnum类型.那么在接口输出时,
OrderDTO中要变为orderStatus: Integer|String和orderStatusName: String.
- dubbo 发布的 api 包中, 应该配套 source 包. 源码中应该有足够的注释.
在使用者最容易看到的地方提供足够的说明.
- dubbo 的认证信息可以包含在
attachment里.
和诸多协议一样, dubbo 协议也有自己的 “header”, 那就是
attachment, 请求附加信息可以使用attachment传输.
面向前端
- 前端接口 path 为
/web/**. - 前端页面 path 为
/page/** - 前端接口的设计理念: 为展示而生, 能够为让前端直接做显示而不用做一些逻辑判断. 但是不能完全脱离业务模型.
- 使用统一的
Result对象封装结果返回.Result包含code,message,tip,data等字段. - 异常要统一包装成数据返回, 需要返回人类可读的 tip. 绝大多数异常情况接口不需要返回特定错误码.
- response status code 使用. 参考 https://tools.ietf.org/html/rfc7231#section-6.1[RFC 7231]
** 正常和业务异常 - 200
** 参数校验错误 - 200 - tip: 参数错误: ** 验签未通过 - 401 - 且返回 header 中包含重定向 location, 供前端同学跳转登录. ** 验签通过但是无权限使用接口 - 403 - tip: 你没有相关权限. ** 意料外异常 - 200 - tip: 发生内部错误, 工程师已经收到正在修复. 有问题请联系 XXX. ** 限流, 熔断, 拒绝服务 - 200 - tip: 当前系统繁忙, 请稍候再试
callback
- callback 接口 path 为
/callback/** - callback 接口尊奉回调发起方的接口格式要求.
评论