25 七月 2019

1. 前言

项目不同, 项目提供的接口功能也不同, 能否有一套比较通用切良好的规则, 来指导我们对项目接口的宏观设计和细节设计?

本文是我在项目中设计 api 接口的一些实践经验.

2. 接口

2.1. 宏观战略

2.1.1. 接口组织结构

一个项目中, 接口组织结构应该是良好干净的. 接口不是想写哪里写哪里, 接口需要有功能分类, 业务分类, 协议分类, 然后分门别类的组织在不同的文件夹里.

按照功能作用分
  • 系统间服务接口

  • 前端接口

按照业务分
  • 业务模块 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                    // 接收回调的接口
  • 首先按照面向服务, 面向前端进行化分.

    • 验证方式不同. 面向前端的接口通常要认证登录状态; 面向服务的接口通常是接入验证.

    • 使用者不同. 前端接口调用者是用户, 请求的数据通常更贴近展示, 面向展示设计接口, 当发生异常时, 需要返回一些用户可读的信息; 服务接口使用者是系统代码, 请求的数据更贴近领域模型, 根据自身领域来暴露服务, 发生异常时, 需要返回明确的状态码, 来供调用方程序判断.

  • 在面向服务的接口中, 提供了不同协议的接口. 不同协议的接口中, 又按照不同的模块化分.

  • 在面向前端的接口中, 分为 webpage 两种, 分别应对 ajax 请求和页面请求.

  • 在面向前端的接口中, manage 包里为管理后台的接口. 因为管理后台通常跟前台的请求要求的身份权限不同.

  • 独立出 callback 类型的接口, 这种接口往往按照回调发起方的接口规范来.

以上, 我们通过接口的受众和类型, 制定了一套接口的组织形式.

目的是保持清晰, 避免混用. 因为我们经常会在接口层做一些通用的操作, 比如日志, 性能打点, 认证等, 对于不同的功能和作用的接口是不同的, 做划分之后, 我们可以分别处理.

上面的目录划分的真实代码示例, 可以在 blueprint project 找到.

2.2. 细节战术

2.2.1. 通用

  1. 读接口无副作用, 不要返回业务异常或错误码

    获取信息的时候, 有就是有, 没有就是没有. 比如, 当调用 getOrder(orderId) 接口时, 没有数据就返回 null.

    而不要返回一个错误叫做 订单不存在.

  2. 面向服务的写接口声明会发生的各种异常和错误码

    让异常类型中包含 code, message, tip, level, data. 让异常能够传递更多信息.

  3. 面向服务的接口设计应该以自身业务另约需要暴露的功能为出发点, 而不是尊奉需求方的要求.

    站在自身角度来思考我能够提供什么服务, 标准能力.

    比如作为订单管理系统, 提供 创建订单, 通用查询订单 的 api 接口.

    但是对于使用方想要 查询女性用户创建的订单, 这种显然就不是一种标准能力.

  4. 设计完之后, 要站在使用者的角度试用自己的接口, 看是否有问题.

    为了保证自己设计和提供的标准服务 api 不是闭门造车, 能够符合使用者的要求, 要站在使用者角度, 来尝试使用自己的接口, 看是否符合预期.

  5. 接口给出粗粒度的数据

    跟项目里的方法接口不同, 在对外服务接口上要提供粗粒度的数据. 有时我们的数据模型存放在多个表里, 或者多个实体形成了一个聚合. 那么在返回数据时, 要想想一下使用者拿到数据后的使用, 提供足够的数据, 避免接口太零碎需要多次访问.

    通过 GraphQl 可以一定程度解决一些接口数据层复杂度的问题.

2.2.2. 面向服务

http
  1. 服务接口 path 为 /api/**

  2. 通过 header 传递额外信息. 比如 appId, timestamp, 签名信息等.

  3. 写接口使用 POST, 接收参数类型为 application/json.

  4. POST 的写接口进行验签时, 由于 content 是 json 类型, 格式层次复杂, 不能把每个字段拿出来加入到签名中做校验, 所以把 content 的内容做哈希算法签名, 来做为一个校验字段, 而不用使用 content 里单独的字段来做.

  5. 使用统一的 ApiResult 对象封装结果返回. ApiResult 包含 code, message, tip, data 等字段.

  6. 写接口的返回值中的 code 应为字符串类型, 用于更有表达力的表明各种异常状态.

  7. response status code 使用. 参考 RFC 7231

    • 正常和业务异常 - 200

    • 参数校验错误 - 400

    • 验签未通过 - 401

    • 验签通过但是无权限使用接口 - 403

    • 意料外异常 - 500

    • 限流, 熔断, 拒绝服务 - 503

dubbo
  1. dubbo 接口的设计理念: 尽量使 rpc 调用看上去跟调用本地方法一样.

    结果无需再做封装, 成功就是成功了, 失败通过异常类传递.

  2. 读接口直接返回数据, 无需额外封装. 读接口不抛业务异常, 有异常就认为 bug.

    OrderDTO getByOrderId(Long orderId);
  3. 写接口返回数据无需额外封装. 业务异常信息通过 Exception 抛出, 并包含异常 code.

    Long createOrder(OrderCreateParam orderCreateParam) throws ApiException;

    声明要抛出的异常.

  4. dubbo 接口实现中, 要做异常全局处理, 转化为 ApiException. 并将 ApiException 放到发布的 api 包中. 否则无法在 dubbo 客户端对异常反序列化.

    实现 dubbo 的 filter 来做全局的异常处理, 将系统内的异常, 转化为 api 包中的 ApiException.

  5. dubbo 发布的 api 包中, 应包含接口用到的常量, 数据对象, 异常 code 常量.

    下面的例子中这个 module 就要作为一个 api jar 包发布出去.

    see api jar

    包含了 ExceptionCodes, constant, dto 等对象.

  6. dubbo 接口不应返回枚举类型以及包含枚举类型的对象. 应该转成字符串或数字常量返回. 避免客户端因为服务端枚举类变化导致反序列化的失败.

    比如在项目业务逻辑中, Order 中的 orderStatus 字段是 OrderStatusEnum 类型.

    那么在接口输出时, OrderDTO 中要变为 orderStatus: Integer|StringorderStatusName: String.

  7. dubbo 发布的 api 包中, 应该配套 source 包. 源码中应该有足够的注释.

    在使用者最容易看到的地方提供足够的说明.

  8. dubbo 的认证信息可以包含在 attachment 里.

    和诸多协议一样, dubbo 协议也有自己的 "header", 那就是 attachment, 请求附加信息可以使用 attachment 传输.

2.2.3. 面向前端

  1. 前端接口 path 为 /web/**.

  2. 前端页面 path 为 /page/**

  3. 前端接口的设计理念: 为展示而生, 能够为让前端直接做显示而不用做一些逻辑判断. 但是不能完全脱离业务模型.

  4. 使用统一的 Result 对象封装结果返回. Result 包含 code, message, tip, data 等字段.

  5. 异常要统一包装成数据返回, 需要返回人类可读的 tip. 绝大多数异常情况接口不需要返回特定错误码.

  6. response status code 使用. 参考 RFC 7231

    • 正常和业务异常 - 200

    • 参数校验错误 - 200 - tip: 参数错误:

    • 验签未通过 - 401 - 且返回 header 中包含重定向 location, 供前端同学跳转登录.

    • 验签通过但是无权限使用接口 - 403 - tip: 你没有相关权限.

    • 意料外异常 - 200 - tip: 发生内部错误, 工程师已经收到正在修复. 有问题请联系 XXX.

    • 限流, 熔断, 拒绝服务 - 200 - tip: 当前系统繁忙, 请稍候再试

2.2.4. callback

  1. callback 接口 path 为 /callback/**

  2. callback 接口尊奉回调发起方的接口格式要求.

3. 文档