自最初提出微服务理念及其变种以来,已经过去十多年了。如今,这种架构风格已经成为常态,而不再是例外。相关的工具和框架已经足够成熟,架构原则也在本地部署和云环境中得到了充分验证。微服务相较于之前的架构模式,其独特性已逐渐清晰,因此本章将重点讨论那些尚未在其他地方深入探讨的关键内容。
本章首先将从计算范式的概览开始,解释当前微服务趋势在其中所处的位置。你将了解到,微服务本质上是分布式计算的一种形式,但其真正优势在于通过分布式的业务事务处理能力,为微服务组件带来自主性。这种“去中心化”赋予了微服务之间的对等自治能力,从而产生一个关键特性:最终一致性。最终一致性系统具有更高的容错性,并能增强端到端业务事务的可靠性——即便它们依赖于相对不稳定的基于 HTTP 的传输协议。接下来,本章还将讨论“微服务”与“并不那么微的服务”之间的边界问题,这一部分主要体现在服务粒度的讨论中。最后,本章还将介绍六边形架构的概念,并配以实例说明(在第2章中还将提供更多的六边形架构示例)。
本章将涵盖以下内容:
- 不同的计算架构范式
- 最终一致性的软件系统
- 服务粒度的划分
- 六边形架构的隐喻
- 第一个 Java 微服务的编码实现
计算架构范式
理解微服务架构中服务的分布式特性非常重要,因此本节将解释这一架构的内涵,以及它与其他更熟悉的架构风格之间的区别。
集中式计算
与如今普及的微型计算机或个人计算机相比,大型机(Mainframe)拥有更强的处理能力。早期的大型机计算机通常配有一个大型机柜(称为“主机架”),其中包含中央处理器和主存储器。大型机的特点是支持多个用户通过交互式终端进行操作,采用分时技术,同时还能执行批处理任务。它们广泛用于如航空预订、银行业务等商业事务处理。事务通常包括一系列操作,如磁盘 I/O、操作系统调用以及各子系统之间的数据传输,如图 1-1 所示。
如图所示,大型机是一种以分时模式运行的集中式计算机,可以同时支持数百个用户执行交互任务和批处理任务。用户通过键盘/打字机终端以及带有集成键盘的专用文本终端 CRT 显示器访问大型机。由于这些单色监视器通常采用绿色“P1”荧光屏,因此俗称为“绿屏”。
在这种架构下,事务处理或批处理作为一种服务由中心主机系统提供,因此它被归类为集中式计算范式。在这种范式中,所有关于业务事务成功与否的决策,都是由中央计算机从头到尾全权处理的。
分布式计算
随后出现的是分布式计算范式,其中计算系统的不同组件分布在不同的联网计算机上,这些组件通过相互传递消息来实现通信与协同。在这种架构中,通常会有某一个节点或计算机充当协调者(coordinator) ,负责统筹事务中的各个子计算步骤,并决定该业务事务的最终成功或失败。
分布式计算有多种变体,主要包括:
- 客户端–服务器架构(Client–server) :在客户端-服务器架构中,客户端向服务器请求并接收数据,然后将其显示给用户。客户端通常承担大部分业务逻辑的校验和处理,因此被称为“厚客户端”(Thick client)。该架构常面临客户端程序分发维护复杂的问题。
- 三层架构(Three-tier) :三层架构将客户端的“智能”迁移到中间层,使客户端可以是无状态且轻量级的(Thin client)。这简化了部署,只需部署在中间件服务器上,几乎不需要客户端程序分发。大多数 Web 应用采用的就是三层架构。
- 多层架构(n-tier) :也称为多层架构模式,是一种软件工程设计模式,它将应用程序分成逻辑层和物理层。每一层负责特定的功能,物理层可以部署在不同服务器、进程或网络中,层与层之间存在物理或逻辑边界。
- 点对点架构(Peer-to-peer, P2P) :在这种架构中,不存在专门的服务提供者或中心管理者,所有节点(称为“对等体”)均分担系统职责。每个节点既可以是客户端,也可以是服务器。典型示例包括 BitTorrent。比特币网络也采用点对点通信机制,但其架构更接近后面将介绍的去中心化架构。
- 微服务架构(Microservices) :微服务是一种以功能单元为基础进行分布的架构。由于业务通常被拆分为多个功能组件,因此完成一个完整的业务事务往往需要多个微服务协作完成。如果再配合进一步的架构设计能力,微服务还可以划入另一类计算范式,即去中心化架构,我们将在本章后面讨论这一点。
图 1-2 展示了一个典型的分布式架构图示,其中客户端、业务逻辑以及数据库分别部署在通过网络连接的不同进程或主机上。
图 1-2 分布式计算
在图 1-2 中,展示了一个中间层服务器的例子。这个服务器由一组业务模块组成:
- 航班可用性模块(Availability) :该模块用于帮助用户规划航班行程。旅客可以输入出发日期和目的地,通过所谓的互联网预订引擎(Internet Booking Engine,简称 IBE),获取来自不同航空公司的所有可用航班列表。旅客可以浏览这些航班、比较价格,并最终决定预订哪一班。
- 预订模块(Booking) :该模块协助旅客完成航班预订,并为其所选航班创建旅客姓名记录(Passenger Name Record,PNR)。
- 库存模块(Inventory) :当一个航班被预订座位后,该模块会减少相应的座位库存。
在某些场景中,中间层服务器还会连接到其他第三方系统,比如全球分销系统(Global Distribution System,GDS),如 Amadeus 或 Sabre。此时,这种架构在最纯粹意义上体现了分布式计算范式。图 1-2 中的“分布式网络结构符号(distributed mesh notation)”即对此进行了说明。
去中心化计算(Decentralized Computing)
在去中心化计算架构中,应用服务由分布式网络中的各个计算设备或节点执行,系统中没有中央控制点。这种软件开发与部署模式为开发者带来了极大的灵活性和成本节省,因为他们无需构建一个集中式控制中心。比特币(Bitcoin)、以太坊(Ethereum)和去中心化交易平台 Uniswap 就是采用去中心化计算的典型协议。
图 1-3 展示了一个微服务架构,它显然属于分布式架构。不仅如此,完整业务流程中的不同功能组件也可能分布在不同微服务之间,因此在某种意义上,这种架构也体现了去中心化特征。
图 1-3 去中心化计算
我不会详细讨论图 1-3 为什么表现为微服务架构的内部细节,因为我假设读者已经熟悉这些基础知识。下面是该架构的一些特征:
- 分布式(Distributed) :业务应用按功能划分并分布部署(例如:航班查询、预订、库存管理是三个功能模块)。
- 独立性(Independent) :每个微服务可以独立开发和部署(前提是保持微服务之间通信所需的 API 兼容性)。
- 可用性(Availability) :即使某个微服务宕机,用户仍然可以通过运行中的其他微服务继续使用应用的部分功能(比如 Booking 模块挂了,用户仍然可以浏览和查询航班)。
因此,在具备适当设计原语的前提下,微服务架构也可以作为一种去中心化应用来运作(尽管“去中心化”这个术语更多用于描述涉及区块链的应用)。
最终一致性软件系统(Eventually Consistent Software Systems)
理解微服务架构中服务的分布式特性是非常重要的。本节将解释这种特性,以及它与其他熟悉的架构模型之间的差异。
原子一致性(Atomic Consistency)
图 1-4 展示的是图 1-2 中的分布式架构,不过这次用户正在进行一次航班预订操作。
图 1-4 ACID 事务
参考图 1-4,典型的航班预订业务事务流程如下:
1.浏览器将用户的事务请求发送到业务层服务器。
2.由于这是一次航班预订事务,请求会被 Booking 模块拦截。在执行预订之前,Booking 模块会在启动一个 ACID² 事务后,向 Inventory 模块发送更新库存的请求,以减少座位库存。
3.在之前启动的 ACID 事务上下文中,Inventory 模块更新其库存数据库表。
4.如果库存更新成功,Booking 模块将在相同事务上下文中,在其数据库表中创建一条预订记录。
如果 Booking 表和 Inventory 表位于同一个数据库中,并且 Booking 和 Inventory 是同一个中间层服务器中的两个模块,那么这就是一次本地事务。这意味着这两个表的更新要么一起成功,要么一起回滚,符合 原子性(ACID) 的事务特性。
最终一致性(Eventual Consistency)
本节将以同样的航班预订场景为例,但采用的是微服务架构,其处理过程是分布式的。请参考图 1-5。
图 1-5 微服务间事务
同样的航班预订业务事务,在微服务架构下的流程如下:
1.Booking 应用将用户事务请求发送至业务层服务器。
2.由于该事务是一次航班预订,它会被 booking 微服务 拦截。在执行预订操作之前,booking 微服务会向 inventory 微服务 发送一个更新库存的请求,以减少座位数量。然而,这里无法开启一个全局事务并在其中发送库存更新请求。因为更新库存通常是通过同步的 REST 调用完成的,而在微服务之间并不鼓励使用分布式事务。在没有分布式事务上下文的情况下,booking 微服务只能接收到更新库存请求的成功或失败响应。
3.Inventory 模块会更新其库存数据库表,并返回成功或失败的结果。
4.如果库存更新成功,booking 微服务会在其数据库表中创建一条预订记录,并将成功信息反馈给终端用户。
这里存在一个隐患:
如果库存数据库更新成功了,但 booking 微服务未能成功在 booking 数据库中创建预订记录,那么这两个表之间的数据一致性就出现了问题。这时就需要 人工干预 来纠正数据,确保两个微服务最终达到一致性。
最终一致性与去中心化系统
上文已经开始探讨如何实现微服务间的最终一致性,并讨论了一个特殊情况:库存数据库更新成功,但 booking 微服务失败,导致系统不一致。现在我们考虑另一个反过来的情况——如果 inventory 微服务未运行,而 booking 微服务也无法完成预订会怎样?
这种情况发生在 booking 微服务尝试同步调用 inventory 微服务的 REST 接口时,因对方服务不可用而无法完成请求,导致 booking 创建流程也随之失败。
图 1-6 展示了在图 1-5 架构上的改进方案:引入一个基于事件的通道(event-based channel)来解耦调用关系。
图 1-6 去中心化微服务架构
在该架构中,微服务之间并不是紧密耦合的,而是通过 JMS 或 AMQP 风格的消息协议 进行松耦合通信。这样,航班预订业务事务的执行顺序可能如下:
1.浏览器将用户的事务请求发送至业务层服务器。
2.由于该事务是一次航班预订,它会被 booking 微服务 拦截。在执行预订操作前,booking 模块会向 消息通道 发送一条 “flight booked(航班已预订)” 的消息。
3.booking 微服务会在其数据库表中创建一条预订记录,并向终端用户返回响应。
4.“flight booked” 消息将会被任何感兴趣的微服务所接收——此处即为 inventory 微服务。
5.inventory 微服务更新库存数据库表,并可选择将成功或失败的消息再次发送至消息通道。
通过将 步骤 2 和步骤 3 放入同一个事务范围内,可以进一步提升此业务事务的可靠性。
无论是否使用事务,设计上一个显著的变化是:该业务事务是由多个微服务共同协作完成的,其最终的成功与否依赖于这些微服务分别完成各自任务的结果。
没有任何一个微服务 单独 控制整个业务流程的全局状态。相反,每个微服务都完成自己的那一部分职责,使得该架构成为真正意义上的去中心化架构。
注:本节讨论假设库存充足。在库存有限的更复杂场景下,可能还需要额外设计机制,这超出了本节范围。
服务粒度(Service Granularity)
要真正理解服务架构的可能性与局限性,首先要正确认识不同层级的服务粒度。本节将介绍这些不同的服务粒度层次。
单体架构(Monolith)
图 1-7 展示了一个典型的单体架构。在这种架构中,功能模块只是在逻辑上进行划分,但在物理上它们都被打包进了一个单一的 .ear(Enterprise Archive,企业归档)文件中。
图 1-7 单体架构(A monolith architecture)
在许多情况下,单体架构会与位于不同节点上的数据库进行通信,且所有功能模块都紧密耦合在这一个数据库中。
宏服务(Macroservice)
图 1-8 所示的宏服务架构,是在单体架构基础上的一种打包层面的改进。
图 1-8 宏服务架构(A macroservice architecture)
在宏服务架构中,不同的功能模块被分别打包为独立的 .jar(Java 归档文件)。不过其部署方式仍与单体架构类似:所有这些 .jar 文件会被统一打包进一个 .ear 或 .war(Web 归档文件)中,并部署在同一个节点上。
小型服务(Mini Service)
小型服务架构是一种真正尝试突破单体架构或宏服务架构限制的改进方案。图 1-9 展示了一个小型服务架构。
图 1-9 小型服务架构(A mini service architecture)
如图 1-9 所示,小型服务架构也会像宏服务架构一样,将各个模块分别打包成独立的归档文件。此外,其中的一些归档还可以进一步被打包为可独立部署的归档。
然而,在实际应用中,由于架构上的限制(例如依赖库、通信方式等),这些部署归档往往还是被部署在同一个节点上。
微服务(Microservices)
微服务架构则试图在开发、部署和发布等方面提供完全的灵活性,如图 1-10 所示。
图 1-10 微服务架构(A microservice architecture)
微服务架构的一个显著优势是它鼓励多语言(polyglot)技术栈的使用。这意味着每个微服务都可以根据自身的功能需求,自由选择实现技术和架构方式,同时依然能够通过标准协议与其他微服务进行暴露和交互。
六边形架构比喻(The Hexagonal Architecture Metaphor)
在学习松耦合系统(尤其是微服务背景下)时,讨论“六边形架构”(Hexagonal Architecture)是非常有帮助的。以下部分将对此进行介绍。
分层架构(Layered Architecture)
分层架构是最常见的软件架构组织方式之一,主要目的是分离系统中的关注点,尤其是通信和职责的分离。图 1-11 展示了一个典型的分层架构示意图。
图 1-11 分层架构(Layered architecture)
在图 1-11 所示的分层架构中,最上层和最底层只是应用程序的入口和出口。
端口与适配器架构(Ports and Adapter Architecture)
“端口与适配器”架构由 Alistair Cockburn 于 2005 年在其博客中提出,其核心思想如下:
“允许一个应用程序可以同等地被用户、程序、自动化测试或批处理脚本驱动,同时也可以在与其最终运行环境(例如设备和数据库)隔离的情况下进行开发与测试。”
进一步解释来看,这种架构将应用程序视为系统的中心实体,所有输入都通过“端口”传入,所有输出也都通过“端口”传出,而这些端口将应用程序与各种外部工具、技术和传输机制解耦。
换句话说,应用程序不应知道是谁发送输入,或者谁在接收输出。
这种设计的目标是保护应用程序免受技术演进和业务变化的影响,因为这些变化常常导致应用在开发完成后不久就变得过时。
为了更好地理解“端口与适配器”架构,接下来我们将把典型的南北朝向的分层架构,旋转为东西方向,如图 1-12 所示。
图 1-12 变更方向后的分层架构
图 1-12 展示了典型的应用程序流程,其起点是用户界面的代码,经过应用核心,再到基础设施层的代码,随后又返回应用核心,最终将响应返回给用户界面。
端口与适配器架构如何解决这一问题:
为了解决如何保护和复用内层业务逻辑(business layer)的问题,端口与适配器架构通过引入一个抽象层(即端口与适配器)来实现解耦。
- 端口(Port) :
是进入或离开应用程序的与消费者无关的抽象接口。在 Java 中,它可以是一个interface;在更通用的层面,它可以是一个 HTTP REST 接口,或者 JMS/AMQP 消息通道。 - 适配器(Adapter) :
是一个具体实现某个接口的类,它向调用方暴露接口定义的功能,但其背后可能使用了调用方并不了解的具体类或技术。例如,一个适配器可以将“保存订单”的接口实现成写入 PostgreSQL 数据库,也可以是调用一个第三方 API。
在这种架构中,系统中的代码被划分为三个基本模块:
- 接口层(Interface) :
提供运行用户界面的能力,无论是 GUI、命令行、REST 还是测试脚本。 - 业务层(Business) :
应用核心,包含领域模型和业务逻辑,负责实际业务处理。 - 基础设施层(Infrastructure) :
连接数据库、消息队列、第三方 API 等系统外部资源的代码。
在图 1-12 的结构中:
- 左侧的适配器代表用户界面,称为主适配器(Primary 或 Driving Adapters) ,因为它们是应用执行的发起者。
- 右侧的适配器连接外部系统资源,称为次适配器(Secondary 或 Driven Adapters) ,它们只是对主适配器操作的响应者。
因此:
- **左侧(用户入口)**的端口及其实现(如 UseCase)属于应用程序内部;
- **右侧(资源出口)**的端口属于应用内部,但它的实现(适配器)属于外部资源的对接代码,包裹了数据库、缓存、远程 API 等依赖。
六边形架构(Hexagonal Architecture)
如图 1-12 所示,应用有两个主要方向:
- 一侧是用户输入入口(如 UI 或 REST API)
- 一侧是基础设施出口(如数据库或消息队列)
由于每一侧都可能有多个端口,因此这种架构在图示上通常被画为一个多边形结构,表示“多端口”特性。虽然可以使用任意多边形,但社区默认使用六边形,这也是“六边形架构”这个名字的由来。
微服务元模型(Microservice Metamodel)
现在,我们可以将六边形架构的思想推广到微服务架构中。因为前面提到的“端口与适配器”的概念在微服务上下文中尤为适用。
图 1-13 就展示了一个六边形架构风格下的微服务结构。
图 1-13 微服务元模型(The microservices metamodel)
你现在可以将图 1-13 中展示的微服务元模型,与基于 Java Spring 的微服务结构进行对比,典型组件包括如下内容:
- REST 控制器(REST Controller) :
一个基于 HTTP 的 REST 控制器类,使用@RestController注解。它作为微服务的主适配器,负责接收外部请求并调用业务逻辑。 - POJO 服务(POJO Service) :
核心业务逻辑所在的类。它应当被隔离在接口内部,避免与输入/输出方式的耦合,以便实现业务逻辑的复用和独立性。 - ORM 仓储(ORM Repository) :
负责数据库持久化的仓储类,通常是JpaRepository或CrudRepository的实现。它属于次适配器,封装了对基础设施的访问。 - 聚合实体(Aggregate Entity) :
基于领域模型构建的核心业务实体,可被多个服务复用。 - 数据存储(Data Store) :
实际的持久化基础设施,如关系型数据库、NoSQL 或对象存储等。 - 消息监听器(Message Listener) :
基于 JMS 或 AMQP 的监听器控制类,用于消费外部消息,是事件驱动架构中的入口点之一。 - 客户端程序(Client Program) :
发起对其他服务或基础设施访问的程序,它通过“端口”访问外部世界,可能是调用 REST API、发送消息等。 - 流程(Process) :
在涉及复杂业务建模的场景中,可以引入显式的“流程”概念。这个流程可以跨多个服务调用,编排业务逻辑。流程建模是可选的,但在需要编排多个微服务行为时十分有用。
若引入流程建模,流程与服务之间的抽象元模型关系将在图 1-14中展示。
图 1-14 流程与服务的关系(Process vs. Service Relationship)
- 服务(Service) :对外暴露服务接口。
- 服务可以是其他服务的聚合:例如一个复杂服务可以组合多个下层服务进行协作处理。
- 流程(Process) :负责聚合并编排多个服务,甚至其他流程。它代表的是一组业务操作的整体流程逻辑。
- 流程也会暴露服务接口:外部调用流程时,像调用一个普通服务一样,只不过内部包含的是更复杂的编排。
后续的内容将以图 1-13 所展示的微服务元模型为基础,来演示各种场景。
第一个 Java 微服务
现在你已经了解了足够多的理论,是时候动手构建你的第一个 Java 微服务了。
这个示例使用 Spring Boot 来构建 Java 微服务。你可以将项目代码导入任何你熟悉的 IDE(如 IntelliJ IDEA、Eclipse 等),也可以直接在 Windows 或 macOS 的终端中使用命令行进行构建和运行,无需 IDE。后续章节也会采用类似的方式进行说明,因为 IDE 的选择与使用方法不在本书的讨论范围内,默认你已经了解这些基本技能,或者可以查阅 Java 或 JEE 的入门书籍获取帮助。
Spring Boot 简介
Spring Boot 提供了对 Spring 框架及众多第三方 Java 库的“约定优于配置”的封装,使得构建可运行、可部署的生产级 Spring 应用变得非常简单。Spring Boot 遵循了大量 Java 开发者日常使用的实践和规范,因此大部分 Spring Boot 项目几乎不需要显式配置即可运行。
需要注意的是,本书不会深入讲解 Spring Boot 本身。如果你已经具备一定 Java 或 Spring 基础,可以直接访问 Spring Boot 官网 开始上手。
设计你的第一个六边形微服务
本节将以简化版的六边形架构视图(基于图 1-13)作为起点,开始构建你的第一个微服务。简化架构视图如图 1-15 所示。
图 1-15 产品 Web 微服务(Product Web Microservice)
请结合“端口与适配器”(Ports and Adapters)的语言来理解图 1-15。这里使用了一个 HTTP 端口,它本质上就是一个规范,说明用户的浏览器如何与应用核心进行交互。这个端口(接口)属于业务逻辑内部的一部分,具体实现形式是一个 REST 控制器(REST Controller)。
在这个架构中,Product 实体(Product Entity)构成了应用的核心逻辑部分(application core)。
详见图 1-16。
图 1-16 产品 Web 微服务的核心实体
在图 1-15 中,为了简化第一个示例,我特意从图 1-13 中剥离了 service(服务)和 process(流程)组件。接下来你将使用数据库适配器连接一个内存数据库。为了保持示例简单,我没有实现正式的“适配器”类,而是直接依赖了一个内存数据库类。下一章中将介绍如何以正确方式使用适配器。
在这个例子中,ProductRestController 是一个主适配器(primary or driver adapter) ,它围绕着一个 HTTP 端口工作。你可以用它来告诉应用核心应该执行什么操作。它负责将来自交付机制(本例中是浏览器请求)中的内容,转换为应用核心中的方法调用。
代码结构说明
本书的源码托管在 GitHub 上,你可以通过图书产品页(www.apress.com/97988688055… ch01/ch01-01 文件夹内,其结构如 代码清单 1-1 所示,遵循标准 Maven 项目结构,因此 pom.xml 位于项目根目录。
├── 02-ProductWeb
│ ├── make.bat
│ ├── make.sh
│ ├── pom.xml
│ ├── run.bat
│ ├── run.sh
│ └── src
│ └── main
│ ├── java
│ │ └── com
│ │ └── acme
│ │ └── ecom
│ │ └── product
│ │ ├── EcomProdMicroAppl.java
│ │ ├── InitComponent.java
│ │ ├── controller
│ │ │ ├── ProdRestContr.java
│ │ │ └── ProdRestConConfig.java
│ │ ├── db
│ │ │ └── InMemoryDB.java
│ │ └── model
│ │ ├── Product.java
│ │ └── ProductCategory.java
│ └── resources
│ ├── application.properties
│ ├── log4j2-spring.xml
│ └── static
│ ├── css
│ │ ├── app.css
│ │ └── bootstrap.css
│ ├── js
│ │ ├── app.js
│ │ ├── controller
│ │ │ └── product_controller.js
│ │ └── service
│ │ └── product_service.js
│ └── product.html
├── README.txt
├── make.sh
├── makeandrun.bat
├── makeandrun.sh
└── pom.xml
代码清单 1-1 Spring Boot 项目源码结构说明
ch01/ch01-01/02-ProductWeb/src/main/java目录下是微服务的 Java 源代码。ch01-01/02-ProductWeb/src/main/resources包含微服务的配置和 Web 页面源码,供终端用户通过浏览器访问使用。- 顶层目录下还有一些脚本文件,可用于构建和运行服务。
理解代码
为了这个初学者示例,领域实体的设计非常简单。实体代表一个可以展示在电商网站上的产品。多个产品属于一个产品分类,因此网站可以先展示产品分类,用户再浏览具体分类下的所有产品。
以下是实体类定义(代码清单 1-2):
public class Product {
@Id
private String productId;
private String name;
private String code;
private String title;
private Double price;
}
public class ProductCategory {
@Id
private String id;
private String name;
private String title;
private String description;
private String imgUrl;
}
代码清单 1-2 Product / ProductCategory 领域模型
由于这两个实体很直观,这里不再展开说明。接下来,我们来看一个使用 Spring @RestController 注解构建的 REST 接口(代码清单 1-3):
@RestController
public class ProductRestController {
@Autowired
private InMemoryDB inMemoryDB;
@RequestMapping(value = "/productsweb",
method = RequestMethod.GET,
produces = {MediaType.APPLICATION_JSON_VALUE})
public ResponseEntity<List<Product>> getAllProducts() {
List<Product> products = inMemoryDB.getAllProducts();
if (products.isEmpty()) {
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
}
return new ResponseEntity<>(products, HttpStatus.OK);
}
}
代码清单 1-3 基于 REST 的 HTTP 接口:获取全部产品
该方法 getAllProducts 将以 JSON 格式返回所有产品的列表。它通过 Spring 注入的 InMemoryDB 类来获取产品数据。
REST 控制器剩余部分实现(代码清单 1-4)
public class ProductRestController {
@RequestMapping(value = "/productsweb/{productId}",
method = RequestMethod.GET,
produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<Product> getProduct(@PathVariable("productId") String productId) {
Product product = inMemoryDB.getProduct(productId);
if (product == null) {
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
}
return new ResponseEntity<>(product, HttpStatus.OK);
}
@RequestMapping(value = "/productsweb",
method = RequestMethod.POST,
produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<Product> addProduct(@RequestBody Product product) {
if (inMemoryDB.getProduct(product.getProductId()) != null) {
return new ResponseEntity<>(HttpStatus.CONFLICT);
}
inMemoryDB.addProduct(product);
return new ResponseEntity<>(product, HttpStatus.OK);
}
@RequestMapping(value = "/productsweb/{productId}",
method = RequestMethod.DELETE,
produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<Product> deleteProduct(@PathVariable("productId") String productId) {
if (inMemoryDB.getProduct(productId) == null) {
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
}
inMemoryDB.deleteProduct(productId);
return new ResponseEntity<>(HttpStatus.NO_CONTENT);
}
@RequestMapping(value = "/productsweb/{productId}",
method = RequestMethod.PUT,
produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<Product> updateProduct(@PathVariable("productId") String productId,
@RequestBody Product product) {
Product currentProduct = inMemoryDB.getProduct(productId);
if (currentProduct == null) {
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
}
currentProduct.setName(product.getName());
currentProduct.setCode(product.getCode());
currentProduct.setTitle(product.getTitle());
currentProduct.setPrice(product.getPrice());
Product newProduct = inMemoryDB.updateProduct(currentProduct);
return new ResponseEntity<>(newProduct, HttpStatus.OK);
}
}
代码清单 1-4 完整的 CRUD 操作实现
getProduct:通过产品 ID 获取单个产品信息。addProduct:添加新产品。deleteProduct:删除产品。updateProduct:更新产品信息。
构建和运行代码(代码清单 1-5)
确保你本地已安装 Java 和 Maven。进入 ch01/ch01-01 目录,执行以下命令:
mvn -Dmaven.test.skip=true clean package
java -jar -Dserver.port=8080 ./02-ProductWeb/target/Ecom-Product-Web-Microservice-0.0.1-SNAPSHOT.jar
或者使用提供的脚本(代码清单 1-6):
sh makeandrun.sh
执行后,你会看到类似以下输出,说明服务成功构建并启动:
...
Started EcomProd...
INFO ProdRest.getAllProducts – Ending
使用浏览器测试微服务
服务启动后,在浏览器中访问:
http://localhost:8080/product.html
如图 1-17 所示,即可通过页面界面交互测试微服务功能。若你需要图示部分翻译,也可以继续发送。
图 1-17 通过浏览器访问微服务
在浏览器中输入该 URL 后,将会渲染项目中 resources 文件夹内打包好的前端 UI 应用。通过该浏览器界面,用户可以点击 “Add New Product(添加新产品)” 按钮并输入产品详情,来添加一个新产品(如图 1-18 所示)。
图 1-18 添加新产品
在用户界面中为所有字段输入有意义的值,然后点击 Submit(提交) 按钮,该操作会请求微服务在内存数据库中创建一个新的产品。新添加的产品也会显示在刷新后的页面中,如图 1-19 所示。
图 1-19 显示新添加的产品
现在你可以尝试通过点击每个产品条目右侧的编辑图标来编辑(更新)新添加的产品。参见图 1-20。
图 1-20 更新产品
如图 1-20 所示,在不更改产品 ID 的前提下,对某些字段进行有效修改——例如价格。点击“提交”按钮使更改生效。参见图 1-21。
图 1-21 更新后的产品列表
如图 1-21 所示,产品所做的修改将在刷新后的页面中显示出来。
最后但同样重要的是,你可能还想测试“删除”功能。你可以点击每个产品最右侧的“删除”图标,并确认你的操作。参见图 1-22。
图 1-22 确认删除
你可以点击每个产品最右侧的“删除”图标,然后确认你的操作,如图 1-22 所示。
使用 cURL 测试微服务
你也可以使用 cURL 或 Postman 来测试微服务中实现的所有方法。有关详细说明,请参阅附录 A。
总结
在本章中,我特意没有对“微服务”下定义,因为业内已经存在许多不同的定义和观点。相反,我尝试从一些不太常见的视角来看待微服务,帮助你理解它们如何融入更广泛的计算范式。
本章还探讨了微服务的一些显著特征如何在执行业务事务时提供更强的能力。我还通过示例介绍了什么是六边形架构,并将在接下来的两章中继续深入探讨这一话题,辅以更多实例,帮助你清晰理解:如果采纳六边形架构的原则,你的微服务将具备哪些设计结构与质量特性。