点击蓝字,关注我们
李征
去哪儿网工程师
2017年2月加入去哪儿网。目前专注于领域服务治理、基于API治理的领域能力标准化。致力于通过领域化、模型化、可感知来解决业务复杂度。期望用DDD驱动,降低系统复杂度,提升团队效能。
对内 DDD,对外 API 是去哪儿网机票目的地事业群业务研发团队2020年 Q3 重点推出的业务重塑架构设计理念。在2020年 Q3,去哪儿网在过往的基础上,在 API 标准化这个领域做出了一些进步,这篇文章主要就是把这方面的经验和大家分享一下。
什么是对内 DDD,对外 API 呢,这个是我们业务研发领域内使用 DDD 作为领域设计、微服务设计的理念的实践原则,领域间使用 API 进行交互的一种通俗易懂的说法。
去哪儿网在 API 领域其实有不少成熟的工具,常用主要包括 wiki、YAPI 和 swagger。
wiki 是去哪儿网最为传统的 API 承载工具,公司较早出现的公共平台,如支付中心,所提供的标准 API 就全部是通过 wiki 的方式呈现出来的。wiki 的优点是自由度相对较高,不受到各种规范的约束,修改也比较随意,可以非常个性化的去满足某些接口阅读者的诉求,天然的安全性较好,非公司内员工没有权限进行访问。缺点就要更多一些,例如因为没有一定的接口规范进行约束,接口的定义方式五花八门,有各种非常个性化的约定方式,相同的约定方式,呈现方式也不一样,有的倾向于给出嵌套式的呈现,有的则倾向于给出模块化的呈现,接口定义与接口之间的同步全部依赖规范和工程师的自觉,极易出现 wiki 与代码不匹配的情况。
正因为如此,我们可以说从提供 API 的角度看,wiki 可以提供可读性较好的 API 但是 wiki 不能提供可信、可靠的 API。
在去哪儿网起到支撑作用的第二个解决方案是 YAPI,YAPI 兴起的背景是从2016年开始的前后端分离架构。前后端分离使得前端工程变得更加独立。YAPI 也在2018年成为了去哪儿网的开源项目。YAPI 作为前端同学开发的接口平台直到现在仍然是去哪儿网 API 解决方案中的基石之一。YAPI 的作用用一句话来表述就是“共同维护一份接口定义,并连接前后端”。
YAPI 与 wiki 的区别可以用下图来表示:
从上图可以看出,YAPI 通过支持打桩测试这个抓手,通过运行 API 接口,测试能否得到预期中的结果来倒逼文档与接口定义一致。这个实现方式十分的巧妙,但是也十分依赖测试环境的完善以及对 API 测试的硬性要求,如果这两者得不到保障,那么这么巧妙的一个模式最终造成的结果仍然是接口与接口文档的分类和不同步。与用 wiki 差别不大,只是 YAPI 拥有着更加符合 RestFul API 规范的接口管理平台,这点与 wiki 相比对 API 的定义是一种约束。
为了防止接口定义与接口不同步(文档与代码不同步),后端同学引入了用于与代码使用 annotation 方式绑定在一起的 swagger,并且在 swagger 的基础之上做了一些基于 maven 的扩展,使得通过 swagger 编写的规范能够通过 maven 命令和发布等工程状态变化将接口的变化更新到 YAPI 平台,解决接口文档与接口实现不匹配的问题。
swagger -> YAPI 取得了一定的效果,但是因为使用 swagger 来编写 API 文档的工程比较少,并且少有人知道有 swagger -> YAPI 这个工具,使得 swagger -> YAPI 这种方式并没有在公司内部推广开来使用。
2020年因为疫情的原因,去哪儿网开始了轰轰烈烈的“练内功”行动,这其中就包括核心业务领域的业务 DDD 重塑、硬件成本节约、API 内部实现重构等。
做这几件事情的时候我们面临了一些困难:
1、DDD 重构需要与领域外通过接口进行调用,那么一个领域与外部领域之间提供多少接口比较合适呢?10个?30个?50个?如果提供的 API 过多,是否意味着 API 不够标准,质量不够高呢?
2、硬件成本中包括实体机&虚拟机节点成本和离线日志、实时日志成本,那么实体机虚拟机节点多少是比较合适的?系统的离线日志、实时日志产出多少又是合适的?这部分很依赖系统 API 的数量以及 API 访问量的提供。
3、API 内部功能重构后,哪些下游访问了这个 API?在使用诸如 QueryDiff 等工具对接口本身进行回归之后,出于保险起见需要对哪些下游系统进行回归测试呢?这方面就很依赖于 API 的上下游关系的治理,而想实现基于治理结果的自动化测试则依赖 API 的标准化。
通过上面几个例子我们看到,API 重新成为重要的技术改进点是整体上对内 DDD,对外 API 系统架构理念的需要,是硬件成本管理的需要,是平台化服务重构的需要。
API 标准化的理论基础来自于 Jeff Bezos 2002年提出的系统间接口化理念,后续这个理念被逐步充实成为了一种称为 SOA 成熟度的理念。
Bezos 是这样表述 Amazon 系统接口化的理念的:
2002年,贝索斯突然向全公司发布了一道指令。
-从今天起,所有的团队都要以服务接口的方式,提供数据和各种功能。
-团队之间必须通过接口来通信。
-不允许任何其他形式的互操作:不允许直接链接,不允许直接读其他团队的数据,不允许共享内存,不允许任何形式的后门。唯一许可的通信方式,就是通过网络调用服务。
-具体的实现技术不做规定,HTTP、Corba、PubSub、自定义协议皆可。
-所有的服务接口,必须从一开始就以可以公开作为设计导向,没有例外。这就是说,在设计接口的时候,就默认这个接口可以对外部人员开放,没有讨价还价的余地。
-不遵守上面规定,就开除。
而从上面 Jeff Bezos 的决策发展出来的 SOA 成熟度模型则对于 API 的标准化有着较为明确的要求,包括:
定义接口、方法、参数、类型及描述的详细规范
定义评判标准 、
开发工具插件支持:IDEA、eclipse 插件对规范的识别
发布系统对 API 的支持
从上面的一系列对 API 的要求总结下来,我们需要 API 具有如下特点:
规范易理解
组件易接入
语法易使用
执行易管理
平台易应用
还有一个总的原则:所有功能基于已有的去哪儿网基建,不重复造轮子。
由此得到了去哪儿网 API 标准化整体解决方案:其中,API 存储平台 YAPI 是现成的,稍加改造即可支持本次 service api 标准化的诉求,应用树管理平台也是现成的,去哪儿网的现有系统叫做 Portal,应用域管理平台 qtracer 也是现成的,只是针对本次需求对功能做了扩展,网关和开放平台也是现成的,这次做了二者的系统集成。可以说,只有 QDoc-annotation 和 QDoc-maven-plugin 是新创建的插件、工具。
首先我们要做的就是制定一套 API 书写规范,这套书写规范主要是限定我们针对 API 说明的注解或者注释如何进行组织,相当于一个业务系统 Domain 维度的实体对象关系设计。我们对于 API 大致设计了如下这些元素:
通过规范制定委员会规范了如下一些术语:
领域(domain/group)
针对某一项业务的总体系统边界,一个领域(domain)会包含多个应用(appcode),一个领域对外暴露的接口是通过 appcode 来暴露的,属于对外开放部分。注:领域可以映射到应用树的三级节点 / 四级节点。
应用(appcode)
应用特指 Qunar 体系下的 appcode,一个 appcode 即一个应用。存在一种特殊情况,一套代码(一个 git 工程)部署多个 appcode,我们认为是多个应用。
服务(service)
同一个 appcode 下,可以提供多个服务(service)。对于不同服务实现(dubbo / http)会有不同的表现形式。
对于 dubbo,很好理解,一个 service interface 就对应一个 service。
对于 http,基于 Spring MVC(restful)的思路,每个 service 对应一个 controller。(这样就要求不同的 controller 来区分一个功能组)。
接口(interface)
这是一个最细粒度的功能维度,是 service 的一个真子集。对于不同服务实现(dubbo/http)会有不同的表现形式。
对于 dubbo,就是一个 service 下提供的方法(method)。
对于 http,就是一个 controller 下提供的 RequestMapping(method)。
参数(parameter)
参数是接口的重要组成部分。对于不同服务实现(dubbo / http)会有不同的表现形式。
对于 dubbo,很好理解,基于 java 的方法签名的入参即可,同时包括 RpcContext 中的内容(除特殊情况,等同于 QTraceContext)。
对于 http,参数包含四部分,第一部分是由 URL 中的参数列表传递的,第二部分是放到 POST 的 http 数据中的。通常这两部分基于 Spring MVC,都是可以通过 @RequestParam 定义的。第三部分是 cookie 信息(request header),第四部分等同于 QTraceContext。
类型(type)
无论对于入参和出参而言,都需要用类型进行标识。类型是一个可描述的结构定义。对于不同服务实现(dubbo / http)会有不同的表现形式。
对于 dubbo,显而易见的,Java 的类型定义,即为这里的 type。同时需要说明一种特殊情况 Object,对于 Object 而言,其代表任意结构,这在 type 里是不允许的,必须标识出对应 Object 的实际类型(具体的类定义,或 json schema定义)。
对于 http,入参一般是基本类型、或类 JSON 类型(包括集合等,都以 json 描述),出参都以 json 描述。http 是一个弱类型定义系统,对于文档是不友好的,因此我们规定入出参类型以 json schema 方式定义。
规范术语后,我们开始制定与术语相对应的注解和注释,一期我们先规范了对应的注解:
领域(@QDomainDoc)
应用(@QAppcodeDoc) 服务(@QServiceDoc) 接口(@QInterfaceDoc) 这里面存在参数注解和返回值是否可以删除 参数(@QParamDocs)参数(@QParamDoc) Model参数(@QParamModel) Model属性的注解 (@QParamModelProperty)类型(@QTypeDoc)等同于 @RequestParam。目前未实现,后续支持 返回值状态码的描述(@QResponses) 返回值状态码的描述(@QResponse) 返回值数据描述说明 返回值数据(@QResponseDocs) 返回值数据(@QResponseDoc) 异常(@QExceptionDoc) 扩展(@QExtensionDoc)
相应的每一个注解我们也都会给出对应 annotation 参数使用说明:
服务(@QServiceDoc) 描述 service 的用途:大家可以看到,QDoc 给出的规范术语和 annotation 充分考虑到了去哪儿同学通常使用的工程上下文语境,例如 Domain、AppCode,都是去哪儿网工程语境中的常用词汇,一看到这些词就能想到是做什么用的,这些贴近工程师常用词汇的术语使得工程师对于 QDoc 规范的理解很顺畅,不需要看大段的说明就可以理解个大概,学习成本低,上手使用很快。这点是去哪儿网制定的规范与 swagger2.0,swagger3.0,smart-doc 等第三方 API 工具的很主要的区别,也是我们在支持 OpenAPI3.0 规范的前提下,采用自定义 API 术语的一个很重要的原因。
当完成了基于 annotation 的 API 规范化组件定义后,下一步就是对应工具、插件的开发以及工具的接入工作。
QDoc 为了方便业务线工程师的接入,最大限度的不让业务线开发工程师在接入 QDoc 的过程中有额外的开发量,采用了 jar 包加 maven 插件的方式,接入步骤十分简单:
服务接入步骤
接入 qdoc 服务需要在 pom 中引入对应的 maven 插件,来完成 qdoc 的发布;
如需采用 Annotation 方式撰写 API 文档,则,需要 qdoc-annotation 包以便完成编写;
具体依赖引入:(Maven Plugin)
com.qunar.fdgroupId> qdoc-maven-pluginartifactId> ${qdoc.maven.version}version>plugin>QDoc Annotation com.qunar.fdgroupId> qdoc-annotationartifactId> ${qdoc.annotation.version}version> providedscope>dependency>
QDoc 的语法分为两部分:一部分是 git 工程侧语法,一部分是代码 API 侧语法,两部分共同构成了去哪儿网标准化的 API。
Git 工程侧语法如下,QDomainDoc 和 QAppcodeDoc 都采用这种方式进行应用。
除 QDomainDoc 和 QAppcodeDoc 外的其他注解与 API 代码结合但非入侵式应用:
@QInterfaceDoc( type = "dubbo", define = "给用户发送短信验证码,dubbo接口", desc = "校验手机号是否为用户所有,给用户发送短信验证码", scene = "给用户发送短信验证码", notice = "内网使用", since = "发送短信验证码,产品需求引入", authors = "fanrong.zhao", url = "dubbo_send" ) @QParamDocs({
@QParamDoc(name = "paramV1", value = "第一个参数", paramType = "form", dataType = "String", notice = "必须是string类型的", paramExample = "username"), @QParamDoc(name = "paramV2", value = "第二个参数", required = false, paramType = "form", dataType = "Boolean", notice = "必须是boolean类型的", paramExample = "true") }) @QResponses({
@QResponse(code = -1, message = "系统异常"), @QResponse(code = 200, message = "成功") }) @QResponseDocs({
@QResponseDoc(bindValueName = "data", description = "第一个参数", bindValueType = "object", propertys = {
@QProperty(type = "String", name = "name", desc = "描述1"), @QProperty(type = "int", name = "age", desc = "描述2"), @QProperty(type = "com.qunar.fd.qdoc.qdocexample.vo.ExampleResultVO",name = "food",desc = "描述3") }), }) public ApiResponseV2 example0(@RequestParam String paramV1,@RequestParam boolean paramV2) {
return null; }
通过目前已经在使用的去哪儿网工程师同学们反馈,一个中等复杂度的 API,通过注解方式书写 API 只需要5分钟的时间。
去哪儿网 QDoc 工具在 API 同步方面主要支持两种方式,maven 命令同步与发布系统同步。maven 命令同步一方面可以满足 Design2doc 的诉求,另一方面也更为灵活,也继承了去哪儿网在 swagger-YAPI 方面的积累,通过发布系统同步是本次 QDoc 的主要工作。这方面的工作解决了接口与 Master 版本不同步的所有问题,包括接口创建、更新、回滚等。
我们可以看到:
服务开通步骤
发布系统开通展示
在去哪儿网应用树中,对应 appcode 下有服务列表,服务列表中【QDoc】,点击开通,即可完成应用树的集成开通。
CM 发布集成
在去哪儿网发布系统中,对应 appcode 下通过服务集市进行开通。
开通后,Portal 发布线上后,会自动触发文档的更新操作,这时,就可以在应用树中看到我们的文档了。
通过简单的3步,我们就完成了 QDoc 与发布系统的集成。
前面的工作完成得再好,如果没有交互良好的展示平台进行支撑,那么对于使用者来说也是十分痛苦的,应用费力度高的系统也是很难进行普及的。我们最终的选择是,YAPI 嵌入到 App 管理平台,与 Appcode 管理相绑定,给与团队管理者一站式的管理体验。
YAPI 可以参考开源版本 https://hellosean1025.github.io/yapi/但是,只有 appcode 维度的 API 管理只能方便工程师团队进行 API 的一站式管理,不能对 DDD 业务重塑中十分重要的 Domain 维度的 API 管理产生正向的帮助。 目前我们正在对 Domain 维度的 API 管理平台做着不断的优化:
当然,我们建立了 Domain 维度的 API 管理体系后,顺带可以做的就是通过去哪儿网成熟的网关体系来外放我们的 API:
介绍到这里,去哪儿网通过 QDoc 工具,从代码中的 API 注解到开放平台的领域 API 的全流程就介绍完毕了。那么在项目中我们遇到了哪些难点呢?
1、开发资源从哪里来
这个项目是去哪儿网机票目的地事业群业务研发 TC 发起的项目,没有直接的团队进行资源支持,所需要的资源跨 CM、YAPI 平台、工具开发、业务试点项目接入开发几大块,几乎涉及到了公司所有团队的工程师,项目采用了公司内开源项目管理的方式,成立项目组,单独立项,跨团队联合各个团队的资源完成项目,项目的完成对于项目管理人员也是不小的挑战。
2、Design First or Code First
作为项目的初始阶段,同时支持 Design First 和 Code First 两种方式是不可能的,我们通过调研发现,Design First 更加适合非 Domain 维度接口的制定,例如一个前后端联调接口,这类支撑类接口不具有通用性,会随着页面的变化而重新进行开发,通常不复用。Code First 更适合帮助 Domain 维度的长期维护支持的接口保持高质量。而我们这次做 API 标准化工作的初衷就是要帮助 DDD 业务重塑做好 Domain 维度接口的规范化维护工作。所以我们选择首先支持 Code First 的接口提供方式。
3、Annotation or Comments
在我们做 QDoc之前,在公司内有一定使用度的 API 标准化工具包括 swagger2.0,swagger3.0,smart-doc , 在这点上我们通过调研发现,使用注解方式的 swagger 相关工具的工程师要明显多于使用 smart-doc 类注释方式的工程师,尽管 annotation 的方式存在 API 代码上方 annotation 堆积过多,会产生代码不美观的问题,但是既然使用 annotation 注解方式的用户明显更多,我们决定 QDoc 优先支持注解方式。这并不代表之后 QDoc 不会支持注释方式。
4、支持完整的 OpenAPI3.0规范 or 支持 OpenAPI3.0规范的子集
这个问题来自于我们已经有了2018年开源的已经相当成熟的 YAPI 平台,那么我们是否满足 YAPI 的接口要求就已足够?是否不必完全满足 OpenAPI3.0规范的要求,我们的回答是否定的。平台是在不断发展的,YAPI 也会有老去的一天,当 YAPI 老去的时候我们也会在支持 OpenAPI3.0规范的其他平台中作出选择,目前看 knife4j 就是一个在我们视线之内的 API 平台。
DDD 与 API 最终会师。核心域、支撑域、通用域 API 均已接入。
通用域 API 化有利于实现通用域的平台化
支撑域 API 化降低了开发量
后续在 AppCode 维度的接口管理方面还要做以下几件事情:
完成 IDEA 接口合规插件;
支持注释模式;
支持客户端/服务端代码生成;
领域接口维度要调研:是否有比 YAPI 更好的替代方案,knife4j 一直在视野中;
还有更重要的一件事情就是通过 API 标准化的发展,反向促进 DDD 的思想在各业务领域产生全面的更加深入的认知。