第08讲:如何对示例应用进行微服务划分

在第 07 课时介绍了领域驱动设计的基本概念之后,本课时将介绍如何在微服务划分时应用领域驱动设计相关的思想。

微服务划分

在微服务架构应用的设计和实现中,如果要找出最重要的一个任务,那必定是非微服务划分莫属。微服务架构的核心是多个互相协作的微服务组成的分布式系统,只有在完成微服务的划分之后,才能明确每个微服务的职责,以及确定微服务之间的交互方式,然后再进行每个微服务的 API 设计,最后才是每个微服务的具体实现、测试和部署。


从上述流程中可以看到,微服务划分处在应用的设计和实现整个链条的第一环。链条中每一环的变动,都会对后面的环节产生影响,作为第一环的微服务划分,如果产生变动,则会影响后面全部的环节。你最不希望看到的就是,在微服务实现的过程中,发现有些功能应该被迁移到其他微服务中。如果发生这样的情况,那么会造成相关微服务的 API 和实现都需要进行修改。


当然了,在实际开发中,要完全避免对微服务划分的改动也是不现实的。在微服务划分阶段,花费足够多的精力来进行分析,所获得的收益绝对是巨大的。

微服务与界定的上下文

在第 07 课时中,我们介绍了领域驱动设计中界定的上下文的概念,如果把领域驱动设计的思想应用在微服务架构上,可以把微服务与界定的上下文进行一一对应。每一个界定的上下文都直接对应一个微服务,然后利用上下文映射的模式来定义微服务之间的交互方式。


这样就把微服务划分的问题,转换成了领域驱动设计中界定的上下文的划分问题。如果你已经对领域驱动设计有较深的了解,那会是一个优势;如果没有的话,第 07 课时的内容可以让你快速入门。


下面以本专栏的示例应用为例来进行具体说明。

示例应用的微服务划分

第 06 课时对示例应用的用户场景进行了介绍,基于这些场景,可以确定应用的领域。在实际的应用开发中,通常需要有领域专家和业务人员参与其中,通过与业务人员的交流,我们可以对领域有更清楚的认识。具体到示例应用来说,由于应用的领域比较贴近生活,也为了简化相关的介绍, 由我们自己来进行领域分析。不过这样有一个弊端,那就是开发人员所做的领域分析,并不一定能反映真实的业务流程。不过,对于示例应用来说,这已经足够好了。


领域驱动设计以领域为核心,领域分为问题空间解决空间


问题空间帮助我们在业务层面上进行思考,是核心领域所依赖的领域中的部分,它包括核心领域,以及所需的其他子领域。核心领域必须从头开始创建,因为这是我们将要开发的软件系统的核心内容;其他子领域则有可能已经存在,或者也需要从头开始创建。问题空间的核心问题是如何识别和划分子领域。


解决空间则由一个或多个界定的上下文组成,以及上下文中的模型。在理想情况下,界定的上下文和子领域之间,存在一一对应的关系。这样可以从业务层次开始进行划分,然后在实现层次也采用同样的划分方式,这样就可以实现问题空间和解决空间的完美集成。在现实的实践中,界定的上下文和子领域之间,不太可能存在一一对应的关系。软件系统在实现中,通常需要与已有的遗留系统和外部系统进行集成,这些系统有自己的界定的上下文。在实际中,比较现实的情况是,多个界定的上下文属于同一个子领域,或是一个界定的上下文对应于多个子领域。


领域驱动设计的思路是,从领域出发,先划分子领域,然后再从子领域中抽象出界定的上下文,以及上下文中的模型,每个界定的上下文都对应一个微服务。

核心领域

核心领域是软件系统存在的价值所在,也是设计的起点。在软件系统开始之前,你应该对软件系统的核心价值有清楚的认识,如果没有的话,那么你首先需要考虑清楚软件系统的卖点在哪里,不同的软件系统其核心领域是不同的。作为打车应用,它的核心领域是如何让乘客快速、舒适和安全的出行,这也是滴滴打车和优步这些打车应用的核心领域。对于作为示例的快乐出行应用来说,这样的核心领域有些过大了,快乐出行应用对核心领域进行了简化,只关注如何让乘客快速的出行。


我们需要给核心领域一个适合的名称。快乐出行的核心领域是如何在需要叫车的乘客和提供出行服务的司机之间,进行快速的匹配。在用户创建行程之后,由系统派发给可用的司机,当司机接收行程之后,系统选中一个司机来派发行程。核心领域的重点是派发行程,因此命名为行程派发

领域中的概念

我们接着罗列出领域中的概念。这是一个头脑风暴的过程,可以在白板上进行,逐一列出所有想到的相关概念,概念都是名词,最早的概念是行程,表示从某个起点到终点的一次旅程。从行程出发,可以引出乘客和司机的概念,乘客是行程的发起者,司机是行程的完成者,每个行程有起点和终点,对应的概念是地址。司机使用私人车辆来完成行程,因此车辆是另外一个概念。


我们根据概念来找到其他的子领域,行程这个概念属于核心领域。司机和乘客应该属于各种独立的子领域,然后分别进行管理,这就产生了乘客管理司机管理两个子领域。地址这个概念,属于地址管理子领域;车辆这个概念,属于司机管理子领域。


在通过领域中的概念进行子领域划分之后,下一步我们就要从领域中的操作中继续发现新的子领域。在用户场景中提到了行程需要进行验证,这个操作有其对应的子领域行程验证。在行程完成之后,乘客需要进行支付,这个操作有其对应的子领域支付管理


下图给出了示例应用中的子领域。


云原生中微服务存在于容器中吗 云原生微服务架构_云原生中微服务存在于容器中吗

![在这里插入图片描述](https://s2.51cto.com/images/blog/202405/15142123_664454634d7db71943.png?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_30,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=#pic_center)

界定的上下文

在确定了核心领域和其他子领域之后,下一步可以从问题空间转移到解决空间。首先把子领域都映射成界定的上下文,界定的上下文与子领域的名称相同;接着对界定的上下文进行建模,建模的主要任务是对相关的概念进行具体化。

行程派发

行程派发模型中的重要实体是行程,也是行程所在的聚合的根。行程有它的起始位置和结束位置,以值对象地址来表示。行程由乘客发起,因此行程实体需要有乘客的引用,当系统选中一个司机接受行程之后,行程实体有对司机的引用。在整个生命周期过程中,行程可能处于不同的状态,有一个属性及其对应的枚举类型来描述行程的状态。


下图给出了模型中的实体和值对象。


云原生中微服务存在于容器中吗 云原生微服务架构_云原生中微服务存在于容器中吗_02

![在这里插入图片描述](https://s2.51cto.com/images/blog/202405/15142123_66445463858b991072.png?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_30,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=#pic_center)

乘客管理

乘客管理模型中的重要实体是乘客,也是乘客所在的聚合的根。乘客实体的属性包括姓名、Email 地址、联系电话等,乘客实体有与之关联的已保存的地址列表,地址是一个实体。


下图给出了模型中的实体。


云原生中微服务存在于容器中吗 云原生微服务架构_云原生_03

![在这里插入图片描述](https://s2.51cto.com/images/blog/202405/15142123_66445463c11a413985.png?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_30,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=)

司机管理

司机管理模型中的重要实体是司机,也是司机所在的聚合的根。司机实体的属性包括姓名、Email 地址、联系电话等,除了司机实体之外,聚合中还包含了车辆实体,车辆实体的属性包括生产厂商、型号、出厂日期和牌照号等。


下图给出了模型中的实体。


云原生中微服务存在于容器中吗 云原生微服务架构_微服务_04

![在这里插入图片描述](https://s2.51cto.com/images/blog/202405/15142124_664454640686777995.png?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_30,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=#pic_center)

地址管理

地址管理模型中的重要实体是地址,地址都是分级的,从省、直辖市、自治区到乡村和街道。除了分级地址之外,还有一个重要的信息,那就是地理位置坐标,包括经度和纬度。

行程验证

行程验证模型中并不包含具体的实体,而是验证行程的服务和相关的算法实现。

支付管理

支付管理模型中的重要实体是支付记录,包含了对行程的引用和支付状态等信息。

界定的上下文之间的交互

在我们界定的上下文的模型中,行程派发模型的行程实体需要引用乘客管理模型中的聚合“乘客”的根实体,以及司机管理模型中的聚合“司机”的根实体。在第 07 课时中,我们提到过,外部对象只能引用聚合的根实体,在引用时,应该引用的是聚合的根实体的标识符,而不是实体本身。乘客实体和司机实体的标识符都是字符串类型,因此行程实体中包含两个字符串类型的属性来分别引用乘客实体和司机实体。


当不同的界定的上下文中的模型中出现相同的概念时,则需要进行映射,我们可以使用第 07 课时中提到的上下文映射的模式来进行映射。


在地址管理和行程派发上下文中,都有地址的概念。地址管理中的地址实体是一个复杂的结构,包括不同分级的地理名称,这是为了实现多级式的地址选择和地址查询。在行程派发上下文中,地址则只包含一个完整的名称,以及地理位置坐标。为了在两个上下文中进行映射,我们可以在行程派发上下文上增加一个防腐蚀层来进行模型的转换。

已有单体应用的迁移

本专栏的示例应用是从头开始创建的新应用,因此在划分微服务时并没有可参考的已有实现。当把已有的单体应用迁移为微服务架构时,划分微服务会更加的有迹可循,可以从单体应用的已有实现中,了解到系统各个部分的实际交互,有助于更好的根据它们的职责来进行划分。这样划分出来的微服务更贴近实际的运行情况。


ThoughtWorks 的 Sam Newman 在其《Building Microservices》一书中分享了产品 SnapCI 的微服务划分的经验,由于有开源项目 GoCD 的相关经验,SnapCI 团队很快就划分了 SnapCI 的微服务。但是,GoCD 和 SnapCI 的用户场景存在一些不同,在一段时间过后,SnapCI 团队发现当前的微服务划分带来了很多问题,他们经常需要做一些跨多个微服务的改动,产生了很高的开销。


SnapCI 团队的做法是把这些微服务重新合并成一个单体系统,让他们有更多的时间来了解系统的实际运行状况。一年之后,SnapCI 团队把这个单体系统重新划分成微服务,经过这一次的划分,微服务的边界变得更加稳定。SnapCI 的这个例子说明了在划分微服务时,对领域的了解是至关重要的。

总结

微服务划分在微服务架构应用开发中至关重要。通过应用领域驱动设计的思想,把微服务的划分问题转换成领域驱动设计中子领域的划分问题,再通过界定的上下文来对领域中的概念进行建模。通过界定的上下文之间的映射模式,可以进行模型的转换。


第09讲:快速部署开发环境与框架

本课时将介绍“快速部署开发环境与框架”相关的内容。


在前面的课时中,我们对云原生微服务架构相关的背景知识进行了介绍,接下来的课时将进入到实际的微服务开发中。本课时作为微服务开发相关的第一个课时,将着重介绍如何准备本地开发环境,以及对示例应用中用到的框架、第三方库和工具进行介绍。

开发必备

开发必备指的是开发环境所必须的。

Java

示例应用的微服务是基于 Java 8 开发的。虽然 Java 14 已经发布,示例应用仍然采用较旧的 Java 8 版本,这是因为该版本的使用仍然很广泛,并且在 Java 8 之后添加的新功能对示例应用的作用不大。如果没有安装 JDK 8,建议你去 AdoptOpenJDK 网站下载 OpenJDK 8 的安装程序。在 MacOS 和 Linux 上,可以用 SDKMAN! 来安装 JDK 8 和管理不同版本的 JDK。


下面是 java -version 的输出结果:

openjdk version "1.8.0_242"OpenJDK Runtime Environment (AdoptOpenJDK)(build 1.8.0_242-b08)OpenJDK 64-Bit Server VM (AdoptOpenJDK)(build 25.242-b08, mixed mode)


Maven

示例应用使用的构建工具是 Apache Maven,你可以手动安装 Maven 3.6,或使用 IDE 中自带的 Maven 来构建项目。MacOS 和 Linux 上推荐使用 HomeBrew 来安装,Windows 上推荐使用 Chocolatey 来安装。

集成开发环境

一个好的集成开发环境可以极大的提高开发人员的生产力。在 IDE 方面,主要是 IntelliJ IDEA 和 Eclipse 两个选择;在 IDE 的选择上,两者并没有太大的区别,我自己使用的 IntelliJ  IDEA 社区版 2020。

Docker

本地开发环境需要使用 Docker 来运行应用所需的支撑服务,包括数据库和消息中间件等。通过 Docker,解决了不同软件服务的安装问题,使得开发环境的配置变得非常简单。从另外一个方面来说,应用的生产运行环境是 Kubernetes,其也是使用容器化部署的,这样就保证了开发环境和生产环境的一致性。为了简化本地开发流程,在本地环境上使用 Docker Compose 来进行容器编排。


根据开发环境的操作系统的不同,安装 Docker 的方式也不相同。一共有 3 种不同的 Docker 产品可以用来安装 Docker,分别是 Docker Desktop、Docker Toolbox 和 Docker Engine,下表给出了这 3 个产品的适用平台。对 MacOS 和 Windows 来说,如果版本支持,则应该优先安装 Docker Desktop,然后再考虑 Docker Toolbox。


云原生中微服务存在于容器中吗 云原生微服务架构_架构_05

![在这里插入图片描述](https://s2.51cto.com/images/blog/202405/15142124_6644546448c669455.png?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_30,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=#pic_center)


Docker Desktop 产品由很多组件组成,包括 Docker Engine、Docker 命令行客户端、Docker Compose、Notary、Kubernetes 和 Credential Helper。Docker Desktop 的优势在于直接使用操作系统提供的虚拟化支持,可以提供更好的集成,除此之外,Docker Desktop 还提供了图形化的管理界面。大部分时候,我们都是通过 docker 命令行来操作 Docker。如果 docker -v 命令可以显示正确的版本信息,就说明 Docker Desktop 安装成功。


下图给出了 Docker Desktop 的版本信息。


云原生中微服务存在于容器中吗 云原生微服务架构_微服务_06

![在这里插入图片描述](https://s2.51cto.com/images/blog/202405/15142124_66445464a20bb32201.png?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_30,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=#pic_center)


Docker Toolbox 是 Docker Desktop 之前的产品。Docker Toolbox 使用 VirtualBox 进行虚拟化,对系统的要求较低。Docker Toolbox 由 Docker Machine、Docker 命令行客户端、Docker Compose、Kitematic 和 Docker Quickstart 终端组成。安装完成之后,通过 Docker Quickstart 启动一个终端来执行 docker 命令。


下图是 Docker Quickstart 终端的运行效果。


云原生中微服务存在于容器中吗 云原生微服务架构_微服务_07

![在这里插入图片描述](https://s2.51cto.com/images/blog/202405/15142124_66445464e4b8b84045.png?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_30,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=#pic_center)


在 Linux 上,我们只能直接安装 Docker Engine,同时还需要手动安装 Docker Compose。


Docker Desktop 和 Docker Toolbox 在使用上有一个显著的区别。Docker Desktop 上运行的容器可以使用当前开发环境主机上的网络,容器暴露的端口,可以使用 localhost 来访问;Docker Toolbox 上运行的容器,实际上运行在 VirtualBox 的一个虚拟机之上,需要通过虚拟机的 IP 地址来访问。我们可以在 Docker Quickstart 启动的终端上通过 docker-machine ip 命令来获取到该 IP 地址,如 192.168.99.100。容器暴露的端口,需要使用这个 IP 地址来访问,这个 IP 地址不是固定不变的。推荐的做法是在 hosts 文件中添加名为 dockervm 的主机名,并指向这个 IP 地址。在访问容器中的服务时,总是使用 dockervm 这个主机名。当虚拟机的 IP 地址改变时,只需要更新 hosts 文件即可。

Kubernetes

在部署应用时,我们需要一个可用的 Kubernetes 集群,一般有 3 种方式创建 Kubernetes 集群。


第一种方式是使用云平台来创建。很多云平台都提供了对 Kubernetes 的支持,由云平台来负责 Kubernetes 集群的创建和管理,只需要通过 Web 界面或命令行工具就可以快速创建 Kubernetes 集群。使用云平台的好处是省时省力,但是开销较大。


第二种方式是在虚拟机或物理裸机上安装 Kubernetes 集群。虚拟机可以是云平台提供的,也可以是自己创建和管理的,使用自己维护的物理裸机集群也是可以的。有非常多的开源 Kubernetes 安装工具可供使用,如 RKEKubesprayKubicorn 等。这种方式的好处是开销比较小,不足之处是需要前期的安装和后期的维护。


第三种方式是在本地开发环境上安装 Kubernetes。Docker Desktop 已经自带 Kubernetes,只需要启用即可,除此之外,还可以安装 Minikube。这种方式的好处是开销最低并且高度可控,不足之处是会占用本地开发环境的大量资源。


在上述三种方式中,云平台的方式适合于生产环境的部署。对于测试和交付准备(Staging)环境来说,则可以选择云平台,也可以从开销的角度选择自己搭建环境。本地开发环境上的 Kubernetes 在很多时候也是必须的。


在本地开发环境中,Docker Desktop 的 Kubernetes 需要手动启用。对于 Minikube,可以参考官方文档来进行安装。两者的区别在于,Docker Desktop 自带的 Kubernetes 版本一般会落后几个小版本。如下图所示,勾选“Enable Kubernetes”选项,可以启动 Kubernetes 集群。Docker Desktop 自带的 Kubernetes 版本是 1.15.5,而目前最新的 Kubernetes 版本是 1.18。


云原生中微服务存在于容器中吗 云原生微服务架构_云原生中微服务存在于容器中吗_08

![在这里插入图片描述](https://s2.51cto.com/images/blog/202405/15142125_664454654cd7948772.png?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_30,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=#pic_center)

框架与第三方库

示例应用会用到一些框架和第三方库,下面对它们进行简单的介绍。

Spring 框架和 Spring Boot

Java 应用开发很难离开 Spring 框架。Spring Boot 也是目前 Java 开发微服务的流行选择之一,关于 Spring 和 Spring Boot 的介绍,不在本专栏的范围之内。示例应用的微服务会用到 Spring 框架中的一些子项目,包括 Spring Data JPA、Spring Data Redis 和 Spring Security 等。

Eventuate Tram

Eventuate Tram 是示例应用使用的事务性消息框架,事务性消息模式在保持数据的一致性上有重要作用。Eventuate Tram 提供了对事务性消息模式的支持,还包括对异步消息传递的支持。Eventuate Tram 与 PostgreSQL、Kafka 进行集成。

Axon 服务器与框架

示例应用也使用了事件源和 CQRS 技术,事件源实现使用的是 Axon 服务器和 Axon 框架。Axon 服务器提供了事件的存储;Axon 框架则连接 Axon 服务器,并提供了 CQRS 支持。

支撑服务与工具

示例应用的支撑服务是运行时所必须的,相关的工具则是开发中可能会用到的。

Apache Kafka 和 ZooKeeper

示例应用在不同的微服务之间使用异步消息来保证数据的最终一致性,因此需要使用消息中间件。Apache Kafka 作为示例应用中使用的消息中间件,ZooKeeper 是运行 Kafka 必须的。

PostgreSQL

示例应用的某些微服务使用关系型数据库来存储数据。在众多的关系型数据库中,PostgreSQL 被选择作为示例应用中某些微服务的数据库。

数据库管理工具

在开发中,我们可能需要查看关系型数据库中的数据。有很多 PostgreSQL 的客户端可供使用,包括 DBeaverpgAdmin 4OmniDB 等,还可以使用 IDE 的插件,比如 IntelliJ IDEA 上的 Database Navigator 插件。

Postman

在开发和测试中,我们经常需要发送 HTTP 请求来测试 REST 服务,与测试 REST 服务相关的工具很多,常用的有 PostmanInsomniaAdvanced REST Client 等。我推荐使用 Postman,是因为它可以直接导入 OpenAPI 规范文件,并生成相应的 REST 请求模板。由于我们的微服务采用 API 优先的设计方式,每个微服务的 API 都有相应的 OpenAPI 规范文件。在开发时,我们只需要把 OpenAPI 文件导入 Postman,就可以开始测试了,省去了手动创建请求的工作。

总结

在讲解实战之前,我们首先需要准备本地的开发环境。本课时首先介绍了如何安装和配置 Java、Maven、集成开发环境、Docker 和 Kubernetes;接着对示例应用中用到的框架和第三方库进行了简要介绍;最后介绍了示例应用所使用的支撑服务,以及开发中需要用到的工具。


第10讲:使用 OpenAPI 和 Swagger 实现 API 优先设计

从本课时开始,我们将进入到云原生微服务架构应用的实战开发环节,在介绍微服务的具体实现之前,首要的工作是设计和确定每个微服务的开放 API。开放 API 在近几年得到了广泛的流行,很多在线服务和政府机构都对外提供了开放 API,其已经成为在线服务的标配功能。开发者可以利用开放 API 开发出各种不同的应用。


微服务应用中的开放 API 与在线服务的开放 API,虽然存在一定的关联,但作用是不同的。在微服务架构的应用中,微服务之间只能通过进程间的通讯方式来交互,一般使用 REST 或 gRPC。这样的交互方式需要以形式化的方式固定下来,就形成了开放 API,一个微服务的开放 API 对外部使用者屏蔽了服务内部的实现细节,同时也是外部使用者与之交互的唯一方式(当然,这里指的是微服务之间仅通过 API 来进行集成,如果使用异步事件来进行集成的话,这些事件也是交互方式)。由此可以看出,微服务 API 的重要性。从受众的角度来说,微服务API的使用者主要是其他微服务,也就是说,主要是应用内部的使用者,这一点与在线服务的 API 主要面向外部用户是不同的。除了其他微服务之外,应用的 Web 界面和移动客户端也需要使用微服务的 API,不过,它们一般通过 API 网关来使用微服务的 API。


由于微服务 API 的重要性,我们需要在很早的时候就得进行 API 的设计,也就是 API 优先的策略。

API 优先的策略

如果你有过开发在线服务 API 的经验,会发现通常是先有实现,再有公开 API,这是因为在设计之前,并没有考虑到公开 API 的需求,而是之后才添加的公开 API。这种做法带来的结果就是,开放的 API 只是反映了当前的实际实现,而不是 API 应该有的样子。API 优先(API First)的设计方式,是把 API 的设计放在具体的实现之前,API 优先强调应该更多地从 API 使用者的角度来考虑 API 的设计。


在写下第一行实现的代码之前,API 的提供者和使用者应该对 API 进行充分的讨论,综合两方面的意见,最终确定 API 的全部细节,并以形式化的格式固定下来,成为 API 规范。在这之后,API 的提供者确保实际的实现满足 API 规范的要求,使用者则根据 API 规范来编写客户端实现。API 规范是提供者和使用者之间的契约,API 优先的策略已经应用在很多在线服务的开发中了。API 设计并实现出来之后,在线服务自身的 Web 界面和移动端应用,和其他第三方应用一样,都使用相同的 API 实现。


API 优先的策略,在微服务架构的应用实现中,有着更加重要的作用。这里有必要区分两类 API:一类是提供给其他微服务使用的 API,另一类是提供给 Web 界面和移动客户端使用的 API。在第 07 课时中介绍领域驱动设计的时候,我提到过界定的上下文的映射模式中的开放主机服务和公开语言,微服务与界定的上下文是一一对应的。如果把开放主机服务和公共语言结合起来,就得到了微服务的 API,公共语言就是 API 的规范。


从这里我们可以知道,第一类微服务 API 的目的是进行上下文映射,与第二类 API 的作用存在显著的不同。举例来说,乘客管理微服务提供了管理乘客的功能,包括乘客注册、信息更新和查询等。对于乘客 App 来说,这些功能都需要 API 的支持,其他微服务如果需要获取乘客信息,也必须调用乘客管理微服务的 API。这是为了把乘客这个概念在不同的微服务之间进行映射。

API 实现方式

在 API 实现中,首要的一个问题是选择 API 的实现方式。理论上来说,微服务的内部 API 对互操作性的要求不高,可以使用私有格式。不过,为了可以使用服务网格,推荐使用通用的标准格式,下表给出了常见的 API 格式。除了使用较少的 SOAP 之外,一般在 REST 和 gRPC 之间选择。两者的区别在于,REST 使用文本格式,gRPC 使用二进制格式;两者在流行程度、实现难度和性能上存在不同。简单来说,REST 相对更加流行,实现难度较低,但是性能不如 gRPC。


云原生中微服务存在于容器中吗 云原生微服务架构_云原生中微服务存在于容器中吗_09

![在这里插入图片描述](https://s2.51cto.com/images/blog/202405/15142125_664454659252c11110.png?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_30,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=#pic_center)


本专栏的示例应用的 API 使用 REST 实现,不过会有一个课时专门来介绍 gRPC。下面介绍与 REST API 相关的 OpenAPI 规范。

OpenAPI 规范

为了在 API 提供者和使用者之间更好的沟通,我们需要一种描述 API 的标准格式。对于 REST API 来说,这个标准格式由 OpenAPI 规范来定义。


OpenAPI 规范(OpenAPI Specification,OAS)是由 Linux 基金会旗下的 OpenAPI 倡议(OpenAPI Initiative,OAI)管理的开放 API 的规范。OAI 的目标是创建、演化和推广一种供应商无关的 API 描述格式。OpenAPI 规范基于 Swagger 规范,由 SmartBear 公司捐赠而来。


OpenAPI 文档描述或定义 API,OpenAPI 文档必须满足 OpenAPI 规范。OpenAPI 规范定义了 OpenAPI 文档的内容格式,也就是其中所能包含的对象及其属性。OpenAPI 文档是一个 JSON 对象,可以用 JSON 或 YAML 文件格式来表示。下面对 OpenAPI 文档的格式进行介绍,本课时的代码示例都使用 YAML 格式。


OpenAPI 规范中定义了几种基本类型,分别是 integer、number、string 和 boolean。对于每种基本类型,可以通过 format 字段来指定数据类型的具体格式,比如 string 类型的格式可以是 date、date-time 或 password。


下表中给出了 OpenAPI 文档的根对象中可以出现的字段及其说明,目前 OpenAPI 规范的最新版本是 3.0.3。


云原生中微服务存在于容器中吗 云原生微服务架构_Swagger_10

![在这里插入图片描述](https://s2.51cto.com/images/blog/202405/15142125_66445465db81b86518.png?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_30,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=#pic_center)

Info 对象

Info 对象包含了 API 的元数据,可以帮助使用者更好的了解 API 的相关信息。下表给出了 Info 对象中可以包含的字段及其说明。


云原生中微服务存在于容器中吗 云原生微服务架构_微服务_11

![在这里插入图片描述](https://s2.51cto.com/images/blog/202405/15142126_6644546612a6298742.png?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_30,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=#pic_center)


下面的代码是 Info 对象的使用示例。

title: 测试服务description: 该服务用来进行简单的测试termsOfService: http://myapp.com/terms/contact:  name: 管理员  url: http://www.myapp.com/support  email: support@myapp.comlicense:  name: Apache 2.0  url: https://www.apache.org/licenses/LICENSE-2.0.htmlversion: 2.1.0


Server 对象

Server 对象表示 API 的服务器,下表给出了 Server 对象中可以包含的字段及其说明。


云原生中微服务存在于容器中吗 云原生微服务架构_架构_12

![在这里插入图片描述](https://s2.51cto.com/images/blog/202405/15142126_664454664968054814.png?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_30,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=#pic_center)      


下面代码是 Server 对象的使用示例,其中服务器的 URL 中包含了 port 和 basePath 两个参数,port 是枚举类型,可选值是 80 和 8080。

url: http://test.myapp.com:{port}/{basePath}description: 测试服务器variables:  port:    enum:      - '80'      - '8080'    default: '80'  basePath:    default: v2


Paths 对象

Paths 对象中的字段是动态的。每个字段表示一个路径,以“/”开头,路径可以是包含变量的字符串模板。字段的值是 PathItem 对象,在该对象中可以使用 summary、description、servers 和 parameters 这样的通用字段,还可以使用 HTTP 方法名称,包括 get、put、post、delete、options、head、patch 和 trace,这些方法名称的字段,定义了对应的路径所支持的 HTTP 方法。

Operation 对象

在 Paths 对象中,HTTP 方法对应的字段的值的类型是 Operation 对象,表示 HTTP 操作。下表给出了 Operation 对象中可以包含的字段及其说明,在这些字段中,比较常用的是 parameters、requestBody 和 responses。


云原生中微服务存在于容器中吗 云原生微服务架构_Swagger_13

![在这里插入图片描述](https://s2.51cto.com/images/blog/202405/15142126_66445466858d76877.png?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_30,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=#pic_center)

Parameter 对象

Parameter 对象表示操作的参数。下表给出了 Parameter 对象中可以包含的字段及其说明。


云原生中微服务存在于容器中吗 云原生微服务架构_Swagger_14

![在这里插入图片描述](https://s2.51cto.com/images/blog/202405/15142126_66445466c0aee22595.png?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_30,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=#pic_center)


下面的代码是 Parameter 对象的使用示例,参数 id 出现在路径中,类型是 string。

name: idin: pathdescription: 乘客IDrequired: trueschema:  type: string


RequestBody 对象

RequestBody 对象表示 HTTP 请求的内容,下表给出了 RequestBody 对象中可以包含的字段及其说明。


云原生中微服务存在于容器中吗 云原生微服务架构_微服务_15

![在这里插入图片描述](https://s2.51cto.com/images/blog/202405/15142127_66445467129742761.png?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_30,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=#pic_center)

Responses 对象

Responses 对象表示 HTTP 请求的响应,该对象中的字段是动态的。字段的名称是 HTTP 响应的状态码,对应的值的类型是 Response 或 Reference 对象。下表给出了 Response 对象中可以包含的字段及其说明。


云原生中微服务存在于容器中吗 云原生微服务架构_Swagger_16

![在这里插入图片描述](https://s2.51cto.com/images/blog/202405/15142127_6644546757d3923033.png?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_30,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=#pic_center)

Reference 对象

在对不同类型的对象描述中,字段的类型可以是 Reference 对象,该对象表示对其他组件的引用,其中只包含一个 $ref 字段来声明引用。引用可以是同一文档中的组件,也可以来自外部文件。在文档内部,可以在 Components 对象中定义不同类型的可复用组件,并由 Reference 对象来引用;文档内部的引用是以 # 开头的对象路径,比如 #/components/schemas/CreateTripRequest。

Schema 对象

Schema 对象用来描述数据类型的定义,数据类型可以是简单类型、数组或对象类型,通过字段 type 可以指定类型,format 字段表示类型的格式。如果是数组类型,即 type 的值是 array,则需要通过字段 items 来表示数组中元素的类型;如果是对象类型,即 type 的值是 object,则需要通过字段 properties 来表示对象中属性的类型。

完整文档示例

下面是一个完整的 OpenAPI 文档示例。在 paths 对象中,定义了 3 个操作,操作的请求内容和响应格式的类型定义,都在 Components 对象的 schemas 字段中定义。操作的 requestBody 和 responses 字段都使用 Reference 对象来引用。

openapi: '3.0.3'info:  title: 行程服务  version: '1.0'servers:  - url: http://localhost:8501/api/v1tags:  - name: trip    description: 行程相关paths:  /:    post:      tags:        - trip      summary: 创建行程      operationId: createTrip      requestBody:
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateTripRequest"
        required: true      
      responses:
        '201':
          description: 创建成功
  /{tripId}:
    get:
      tags:
        - trip
      summary: 获取行程
      operationId: getTrip
      parameters:
        - name: tripId
          in: path
          description: 行程ID
          required: true
          schema:
            type: string
      responses:
        '200':
          description: 获取成功  
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TripVO"
        '404':
          description: 找不到行程
  /{tripId}/accept:
    post:
      tags:
        - trip
      summary: 接受行程
      operationId: acceptTrip
      parameters:
        - name: tripId
          in: path
          description: 行程ID
          required: true
          schema:
            type: string
      requestBody:
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/AcceptTripRequest"
        required: true
      responses:
        '200':
          description: 接受成功
components:
  schemas:
    CreateTripRequest:
      type: object
      properties:
        passengerId:
          type: string   
        startPos:
          $ref: "#/components/schemas/PositionVO"
        endPos:
          $ref: "#/components/schemas/PositionVO"
      required:
        - passengerId
        - startPos
        - endPos
    AcceptTripRequest:
        type: object
        properties:
          driverId:
            type: string
          posLng:
            type: number
            format: double
          posLat:
            type: number
            format: double
        required:
          - driverId
          - posLng
          - posLat
    TripVO:
      type: object
      properties:
        id: 
          type: string
        passengerId:
          type: string
        driverId:
          type: string
        startPos:
          $ref: "#/components/schemas/PositionVO"
        endPos:
          $ref: "#/components/schemas/PositionVO"
        state:
          type: string                     
    PositionVO:
      type: object
      properties:
        lng:
          type: number
          format: double
        lat:
          type: number
          format: double 
        addressId:
          type: string 
      required:
        - lng
        - lat


OpenAPI 工具

我们可以用一些工具来辅助 OpenAPI 规范相关的开发。作为 OpenAPI 规范的前身,Swagger 提供了很多与 OpenAPI 相关的工具。

Swagger 编辑器

Swagger 编辑器是一个 Web 版的 Swagger 和 OpenAPI 文档编辑器。在编辑器的左侧是编辑器,右侧是 API 文档的预览。Swagger 编辑器提供了很多实用功能,包括语法高亮、快速添加不同类型的对象、生成服务器代码和生成客户端代码等。


使用 Swagger 编辑器时,可以直接使用在线版本,也可以在本地运行,在本地运行最简单的方式是使用 Docker 镜像 swaggerapi/swagger-editor。


下面的代码启动了 Swagger 编辑器的 Docker 容器,容器启动之后,通过 localhost:8000 访问即可。

docker run -d -p 8000:8080 swaggerapi/swagger-editor



下图是 Swagger 编辑器的界面。


云原生中微服务存在于容器中吗 云原生微服务架构_云原生中微服务存在于容器中吗_17

![在这里插入图片描述](https://s2.51cto.com/images/blog/202405/15142127_664454678dd4278870.png?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_30,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=#pic_center)

Swagger 界面

Swagger 界面提供了一种直观的方式来查看 API 文档,并进行交互。通过该界面,可以直接发送 HTTP 请求到 API 服务器,并查看响应结果。


同样,我们可以用 Docker 来启动 Swagger 界面,如下面的命令所示。容器启动之后,通过 localhost:8010 来访问即可。

docker run -d -p 8010:8080 swaggerapi/swagger-ui

对于本地的 OpenAPI 文档,可以配置 Docker 镜像来使用该文档。假设当前目录中有 OpenAPI 文档 openapi.yml,则可以使用下面的命令来启动 Docker 镜像来展示该文档。

docker run -p 8010:8080 -e SWAGGER_JSON=/api/openapi.yml -v $PWD:/api swaggerapi/swagger-ui



下图是 Swagger 界面的截图。


云原生中微服务存在于容器中吗 云原生微服务架构_云原生_18

![在这里插入图片描述](https://s2.51cto.com/images/blog/202405/15142127_66445467d101369748.png?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_30,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=#pic_center)

代码生成

通过 OpenAPI 文档,可以利用 Swagger 提供的代码生成工具来自动生成服务器存根代码和客户端。代码生成时可以使用不同的编程语言和框架。


下面给出了代码生成工具所支持的编程语言和框架。

aspnetcore, csharp, csharp-dotnet2, go-server, dynamic-html, html, html2, java, jaxrs-cxf-client, jaxrs-cxf, inflector, jaxrs-cxf-cdi, jaxrs-spec, jaxrs-jersey, jaxrs-di, jaxrs-resteasy-eap, jaxrs-resteasy, micronaut, spring, nodejs-server, openapi, openapi-yaml, kotlin-client, kotlin-server, php, python, python-flask, r, scala, scala-akka-http-server, swift3, swift4, swift5, typescript-angular, javascript



代码生成工具是一个 Java 程序,下载之后可以直接运行。在下载 JAR 文件 swagger-codegen-cli-3.0.19.jar 之后,可以使用下面的命令来生成 Java 客户端代码,其中 -i 参数指定输入的 OpenAPI 文档,-l 指定生成的语言,-o 指定输出目录。

java -jar swagger-codegen-cli-3.0.19.jar generate -i openapi.yml -l java -o /tmp



除了生成客户端代码之外,还可以生成服务器存根代码。下面代码是生成 NodeJS 服务器存根代码:

java -jar swagger-codegen-cli-3.0.19.jar generate -i openapi.yml -l nodejs-server -o /tmp


总结

API 优先的策略保证了微服务的 API 在设计时,充分考虑到 API 使用者的需求,使得 API 成为提供者和使用者之间的良好契约。本课时首先介绍了 API 优先的设计策略,然后介绍了 API 的不同实现方式,接着介绍了 REST API 的 OpenAPI 规范,最后介绍了 OpenAPI 的相关工具。