Java-EE-领域驱动设计实践指南-二-

58 阅读1小时+

Java EE 领域驱动设计实践指南(二)

原文:Practical Domain-Driven Design in Enterprise Java

协议:CC BY-NC-SA 4.0

四、货物跟踪器:Eclipse MicroProfile

快速回顾一下

我们将货物跟踪确定为主要问题空间/核心领域,并将货物跟踪应用作为解决这一问题空间的解决方案。

我们确定了货物跟踪应用的各种子域/有界上下文。

我们详细描述了每个有界上下文的域模型,包括集合、实体、值对象和域规则的标识。

我们确定了有界环境中所需的支持领域服务。

我们在有限的上下文中确定了各种操作(命令、查询、事件和故事)。

我们使用 Jakarta EE 实现了一个单一版本的货物跟踪器。

本章详细介绍了使用新的 Eclipse MicroProfile 平台的货物跟踪应用的第二个 DDD 实现。货物跟踪器应用将使用基于微服务的架构进行设计。和以前一样,我们将把 DDD 工件映射到 Eclipse MicroProfile 平台中可用的相应实现。

首先,这里是 Eclipse MicroProfile 平台的概述。

Eclipse 微文件

随着微服务架构风格开始在企业中迅速流行,Java EE 平台迫切需要发展以满足这些需求。受发布流程的限制,再加上 Java EE 平台更侧重于传统的整体应用,一组现有的 Java EE 供应商决定构建一个更优化的平台,适合具有加速发布周期的微服务架构。

这个新平台命名为 MicroProfile,于 2016 年首次发布,并加入了 Eclipse 基金会,因此得名“Eclipse MicroProfile”。MicroProfile 平台的目标是利用 Java EE 平台强大的基础规范,并通过一组特定于云/微服务的规范对其进行增强,从而成为一个由 Jakarta EE 支持的完整微服务平台。

该平台在很短的时间内就获得了广泛的认可和广泛的社区参与,并有近九家供应商实现了各种规范。考虑到微服务领域目前和未来的挑战,这些规范已经非常成熟。

微服务架构风格

微服务架构风格已经迅速成为构建下一代企业应用的基础。微服务架构风格促进了整个软件开发和交付生命周期的独立性,显著加快了企业应用的交付速度。图 4-1 简要总结了微服务的优势。

img/473795_1_En_4_Fig1_HTML.jpg

图 4-1

基于微服务的架构的优势

微服务架构确实有其自身的复杂性。微服务架构固有的分布式特性导致了以下领域的实现复杂性

*** 事务管理

  • 数据管理

  • 应用模式(例如,报告、审计)

  • 部署架构

  • 分布式测试

随着这种架构风格变得越来越流行,这些领域正在以各种方式使用开源框架和专有供应商软件来解决。

Eclipse MicroProfile:功能

Eclipse MicroProfile 平台为计划迁移到微服务架构风格的应用提供了一个非常强大的基础平台。加上 DDD 为我们提供了设计和开发微服务的明确流程和模式,这一组合为构建基于微服务的应用提供了一个强大的平台。

在我们深入研究微档案平台的技术组件之前,让我们阐明微服务平台的需求,如图 4-2 所示。

img/473795_1_En_4_Fig2_HTML.jpg

图 4-2

完整微服务平台的要求

这些要求被划分到各个领域,以满足微服务平台的独特要求。

Eclipse MicroProfile 平台提供的一组规范如图 4-3 所示。规格分为两类:

img/473795_1_En_4_Fig3_HTML.jpg

图 4-3

Eclipse 微文件规范

  • 核心集 ,有助于迎合云原生/微服务架构风格的特定需求。这些规范提供了配置、健康检查、通信模式、监控、安全性和弹性方面的解决方案。

  • 支持套件 有助于满足应用的传统需求,无论是微服务还是单片应用。这包括构建业务逻辑、API 设计、数据管理和数据处理的能力。

与主 Java EE/Jakarta EE 平台不同,MicroProfile 项目中没有概要文件。只有一套规范,任何供应商都需要实现它才能与 MicroProfile 兼容。

下面是当前实现微概要规范的一组供应商。

|

实现名称

|

实施版本

| | --- | --- | | Helidon (Oracle) | 微概要文件 2.2 | | SmallRye(社区) | 微概要文件 2.2 | | 红帽 | 微型文件 3.0 | | 开放自由(IBM) | 微型文件 3.0 | | WebSphere Liberty (IBM) | 微型文件 3.0 | | Payara 服务器(Payara 服务) | 微概要文件 2.2 | | Payara Micro (Payara 服务) | 微概要文件 2.2 | | 汤米(阿帕拉契省) | 微档案 2.0 | | 库穆卢兹 | 微概要文件 2.2 |

让我们深入了解一下规格。我们将首先浏览核心规范,然后是支持集。

Eclipse MicroProfile:核心规范

核心微配置文件规范有助于实现云原生/微服务应用要求的一系列技术问题。这组规范经过精心设计,旨在让希望采用微服务风格的开发人员能够轻松实现这些特性。

从微服务需求映射的角度来看,图 4-4 中所示的阴影框是用核心规范实现的。

img/473795_1_En_4_Fig4_HTML.jpg

图 4-4

映射到微服务需求的 Eclipse MicroProfile 核心规范

让我们浏览一下 MicroProfile 的核心规范集。

Eclipse 微配置文件配置

配置规范定义了一种易于使用的机制,用于实现微服务所需的应用配置。每个微服务都需要某种配置(例如,其他服务 URL 或数据库连接、业务配置、功能标志等资源位置)。根据微服务被部署到的环境(例如,开发、测试、生产),配置信息也可以不同。微服务工件不应该被改变以适应不同的配置模式。

微配置文件配置定义了一种聚合和注入微服务所需的配置信息的标准方式,而不需要重新打包工件。它提供了一种注入缺省配置的机制,该机制通过外部手段(环境变量、Java 命令行变量、容器变量)覆盖缺省配置。

除了注入配置信息,MicroProfile Config 还定义了实现配置源的标准方式,即存储配置信息的存储库。配置源可以是 GIT 存储库或数据库。

该规范的当前版本是 1.3 版。

Eclipse MicroProfile 健康检查

微概要健康检查规范定义了确定微服务的状态和可见性的标准运行时机制。它旨在通过机器对机器机制在集装箱化环境中使用。

该规范的当前版本是 2.0 版。

Eclipse 微文件 JWT 认证

微配置文件 JSON Web Token (JWT)身份验证规范定义了一个标准的安全机制,用于使用 JSON Web Token (JWT)为微服务端点实现身份验证和授权(RBAC[基于角色的访问控制])。

该规范的当前版本是 1.1 版。

Eclipse 微概要度量

MicroProfile Metrics 规范为微服务定义了一种标准机制,以发出可被监控工具识别的度量。

该规范的当前版本是 2.0 版。

eclipse open API 微配置文件

微文件 OpenAPI 规范定义了为微服务生成符合 OpenAPI 的合同/文档的标准机制。

该规范的当前版本是 1.1 版。

Eclipse 微文件 OpenTracing

微文件 OpenTracing 规范定义了在微服务应用中实现分布式跟踪的标准机制。

该规范的当前版本是 1.3 版。

Eclipse 微概要类型安全休息客户端

MicroProfile Rest 客户端规范定义了一种在微服务之间实现 RESTful 调用的标准机制。

该规范的当前版本是 1.3 版。

这完善了 Eclipse MicroProfile 提供的核心规范集。可以看出,这些规范是经过深思熟虑的,提供了一套全面完整的功能来帮助构建基于标准的微服务应用。

Eclipse MicroProfile:支持规范

虽然核心规范集帮助我们实现横切微服务关注点,但是支持规范帮助我们构建微服务的业务逻辑方面。这包括领域模型、API、数据处理和数据管理。

从微服务需求映射的角度来看,如图 4-5 所示的橙色方框是根据支持规范实现的。

img/473795_1_En_4_Fig5_HTML.jpg

图 4-5

Eclipse 微文件支持规范

接下来,我们将快速了解一下支持规范。

Java 的上下文和依赖注入(2.0)

如前一章所述,Java EE 规范中引入了 CDI 来构建一个组件,并通过注入来管理它的依赖关系。CDI 现在已经成为该平台几乎所有其他部分的基础技术,而 EJB 正慢慢地被挤出人们的视线。

该规范的最新版本是 2.0 版。

常见注释

该规范提供了一组注释或标记,帮助运行时容器执行常见任务(例如,资源注入、生命周期管理)。

该规范的最新版本是 1.3 版。

用于 RESTful Web 服务的 Java API(JAX-RS)

该规范为开发人员实现 RESTful web 服务提供了一个标准的 Java API。另一个流行的规范,该规范的最新版本发布了一个主要版本,支持反应式客户端和服务器端事件。

该规范的最新版本是 2.1 版。

JSON 绑定的 Java API

Java EE 8 中引入的一个新规范,它详细描述了一个 API,该 API 提供了一个绑定层来将 Java 对象转换为 JSON 消息,反之亦然。

该规范的第一个版本是 1.0 版。

用于 JSON 处理的 Java API

这个规范提供了一个 API,可以用来访问和操作 JSON 对象。Java EE 8 中规范的最新版本是一个主要版本,有各种增强,比如 JSON 指针、JSON 补丁、JSON 合并补丁和 JSON 收集器。

该规范的当前版本是 1.1 版。

可以看出,该平台没有使用基于编排的 Sagas 为分布式事务管理提供开箱即用的支持。我们需要使用 事件编排 来实现分布式事务。在未来的版本中,MicroProfile 平台正在计划实现 saga 编排模式,作为 MicroProfile LRA(长期运行操作)规范的一部分。

Eclipse 微概要规范概要

这就完成了我们对 Eclipse MicroProfile 平台规范的高级概述。这些规范非常全面,提供了构建想要采用微服务架构风格的企业应用所需的几乎所有功能。该平台还提供了扩展点,以防它不能满足企业的任何特定需求。

就像 Jakarta EE 平台一样,最重要的一点是,这些是由多个供应商支持的标准规范。这为企业选择微服务平台提供了灵活性。

货物跟踪器实现:Eclipse MicroProfile

重申一下,本章的目标是利用领域驱动设计和 Eclipse MicroProfile 平台 将货物跟踪系统 实现为微服务应用。

作为实施的一部分,我们将使用 DDD 作为基础 来帮助我们 设计和开发 我们的微服务。当我们在实现中导航时,我们将使用 Eclipse MicroProfile 平台中可用的相应工具来映射和实现 DDD 工件。

任何基于 DDD 的架构的两个基础部分是域模型和域模型服务。图 4-6 说明了这一点。前一章演示了使用 DDD 来帮助我们实现货物跟踪作为一个整体。本章将演示如何使用 DDD 来帮助我们实现既定的目标,将货物跟踪系统作为一个微服务应用来实施。

img/473795_1_En_4_Fig6_HTML.jpg

图 4-6

DDD 工件实施摘要

由于一些 DDD 工件的实现的共性,可能会有一些与前一章的重复。建议您阅读本章,因为您将使用 Eclipse MicroProfile 从头实现一个微服务项目。

实施选择:Helidon MP

第一步是选择我们将用来实现 DDD 工件的微概要文件实现。如前所述,我们确实有多种实施方案可供选择。对于我们的实现,我们选择使用 Oracle 的 Helidon MP 项目( https://helidon.io )。

Helidon MP 项目支持 Eclipse MicroProfile 规范;它被设计得易于使用,并且运行在一个快速反应的 web 服务器上,开销非常小。除了支持核心/支持规范集,它还提供了一组扩展,包括对 gRPC、Jedis (Redis 库)、HikariCP(连接池库)和 JTA/JPA 的支持。

图 4-7 概述了 Helidon MP 项目提供的能力。

img/473795_1_En_4_Fig7_HTML.jpg

图 4-7

Helidon MP 对微文件规范和附加扩展的支持

Helidon MP 的当前版本是 1.2,它支持 Eclipse MicroProfile 2.2。

货物跟踪器实现:有界上下文

我们的实现首先从将货物跟踪器分割成一组微服务开始。为此,我们将货物跟踪域划分为一组 业务能力/子域 。图 4-8 展示了货物跟踪领域的业务能力。

  • –该业务能力/子域负责与新货物的预订、货物路线的分配以及货物的任何更新(例如,目的地的变更/取消)相关的所有操作。

*** 处理–该业务能力/子域负责与货物航程中各港口的货物处理相关的所有操作。这包括登记货物上的处理活动或检查货物(例如,检查它是否在正确的路线上)。

*   ***跟踪***–该业务能力/子域为最终客户提供了一个界面,以准确跟踪货物的进度。

*   ***路由***——该业务能力/子域负责所有与调度和路由维护相关的操作。** 

虽然 DDD 的子域名在所谓的"****,"问题空间中运行,但我们也需要为它们提出解决方案。我们在 DDD 通过使用 有界上下文 的概念来实现这一点。简单地说,有界上下文在",**"解空间中操作,也就是说,它们代表我们的问题空间的实际解工件。

****对于我们的实现, 有界上下文被建模为包含单个或一组微服务 。这是有意义的,原因显而易见,因为有界上下文提供的 独立性 满足了基于微服务架构所需的基本方面。管理有界上下文状态的所有操作,无论是改变状态的命令、检索状态的查询,还是发布状态的事件,都是微服务的一部分。

从部署的角度来看,如图 4-8 所示,每个绑定的上下文都是一个独立的自包含的可部署单元。可部署单元可以以 fat JAR 文件或 Docker 容器映像 的形式打包。由于 Helidon MP 为 Docker 提供一流的支持,我们将利用它作为我们的包装选择。

微服务将需要一个 数据存储 来存储它们的状态。我们选择按照服务模式 **,**采用 数据库,也就是说,我们的每个微服务都将拥有自己独立的数据存储。就像我们的应用层有多种语言的技术选择一样,我们的数据存储也有多种语言的选择。我们可以选择一个普通的关系数据库(如 Oracle、MySQL、PostgreSQL),一个 NoSQL 数据库(如 MongoDB、Cassandra),甚至一个内存中的数据存储(如 Redis)。选择主要取决于微服务想要满足的可伸缩性需求和用例类型。对于我们的实现,我们决定选择 MySQL 作为数据存储。

img/473795_1_En_4_Fig8_HTML.jpg

图 4-8

有界上下文工件

在我们进一步讨论实现之前,这里有一个简短的注释,说明我们将进一步使用的语言。有界语境术语是 DDD 特有的术语,因为这是一本关于 DDD 的书,所以主要使用 DDD 术语是有意义的。这一章也是关于微服务的实现。正如我们已经说过的,这个实现 将一个有界的上下文建模为一个微服务 。因此,我们对术语有界上下文的使用本质上与微服务相同。

有界上下文:打包

将有界的上下文打包包括将我们的 DDD 工件逻辑分组到一个可部署的自给自足的工件中。我们的每个有界上下文都将被构建成一个 Eclipse 微概要应用 。Eclipse MicroProfile 应用是自给自足的,因为它们包含所有的 依赖项、配置和运行时 **,**也就是说,它们不需要任何外部依赖项(比如应用服务器)来运行。

图 4-9 展示了 Eclipse MicroProfile 应用的结构。

img/473795_1_En_4_Fig9_HTML.jpg

图 4-9

Eclipse 微文件应用的剖析

Helidon MP 提供了一个 Maven 原型(helidon-quickstart-mp)来帮助我们搭建 Eclipse MicroProfile 应用。清单 4-1 显示了 Helidon MP Maven 命令,我们将使用该命令为 预订有界上下文 生成微文件应用:

mvn archetype:generate -DinteractiveMode=false -DarchetypeGroupId=io.helidon.archetypes -DarchetypeArtifactId=helidon-quickstart-mp -DarchetypeVersion=1.2 -DgroupId=com.practicalddd.cargotracker -DartifactId=bookingms -Dpackage=com.practicalddd.cargotracker.bookingms

Listing 4-1.Helidon MP quickstart archetype

原型生成的源代码包含一个“”主类。main 类包含了一个 Main 方法,当我们运行应用时,这个方法会调用 Helidon MP web 服务器。图 4-10 展示了快速入门原型生成的代码。

**除了 main 类之外,它还生成一个 示例 REST 资源文件(Greeter) 来帮助测试应用, ***一个 microprofile 配置文件(micro profile-config . properties)***可用于为应用设置配置信息,以及一个用于 CDI 集成的 beans.xml 文件

img/473795_1_En_4_Fig10_HTML.jpg

图 4-10

使用 Helidon 原型生成的项目

我们可以用两种方式运行应用:

  • 作为 JAR 文件

构建项目将会产生一个 JAR 文件( bookingms.jar )。使用命令“Java-JAR booking ms . JAR”将其作为一个简单的 JAR 文件运行,将在已配置的端口(8080)上调出 Helidon MP 的 web 服务器,并使 Greeter REST 资源在http://<>:8080/greet上可用。

我们可以使用 curl 实用程序来测试 Greeter REST 资源

curl -X GET http://<<Machine-Name>>:8080/greet
.

这将显示消息“Hello World”。这表明预订微服务 Helidon MP 应用实例作为 JAR 文件正确启动和运行。

  • 作为码头工人的形象

Helidon MP 提供的另一种方法是将 MicroProfile 应用作为 Docker 映像来构建和运行。这符合 MicroProfile 应用提供构建云原生应用的能力的原则。

构建 Docker 映像是使用以下命令完成的

docker build -t bookingms.

使用以下命令运行 Docker 映像

docker run --rm -p 8080:8080 bookingms:latest.

我们可以再次使用 curl 实用程序来测试 Greeter REST 资源

curl -X GET http://<<Machine-Name>>:8080/greet.

这将显示消息“Hello World”。这表明预订微服务 Helidon MP 应用实例作为 Docker 映像正常启动和运行。

因为我们的首选方法是使用容器,所以我们将在 Cargo Tracker 应用中构建并运行所有的微服务作为 Docker 映像。

有界上下文:包结构

决定了打包方面之后,下一步是决定我们每个有界上下文的包结构,也就是说,将各种 DDD 微概要文件工件逻辑分组到一个可部署的工件中。逻辑分组包括识别一个包结构,我们在这个包结构中放置各种 DD MicroProfile 工件,以实现我们对有界上下文的整体解决方案。

图 4-11 显示了我们任何有界上下文的高级包结构。

img/473795_1_En_4_Fig11_HTML.jpg

图 4-11

有界上下文的包结构

让我们稍微扩展一下包的结构。

接口

这个包将所有入站接口封装到由通信协议分类的有界上下文中。 接口 的主要用途是代表域模型协商协议(如 REST API(s)、WebSocket(s)、FTP(s)、自定义协议)。

例如,预订有界上下文提供了 REST APIs,用于向其发送****,即** 命令 (例如,预订货物命令,将路线分配给货物命令)。同样,Booking Bounded Context 为发送 状态检索请求 提供 REST APIs,即 查询 **,给它(如检索货物订舱明细,列出所有货物)。这被分组到“ 其余的 包中。

**它还有事件处理程序,订阅由其他有界上下文生成的各种事件。所有事件处理程序被分组到“事件处理程序”包中。除了这两个包,接口包还包含了“ 变换 ”包。这用于将传入的 API 资源/事件数据转换为域模型所需的相应命令/查询模型。

**由于我们需要支持 REST、事件和数据转换,包的结构如图 4-12 所示。

img/473795_1_En_4_Fig12_HTML.jpg

图 4-12

接口的封装结构

应用

应用服务充当有界上下文的域模型的外观。它们提供外观服务,将命令/查询发送到底层的域模型。作为命令/查询处理的一部分,它们也是我们向其他有界上下文发出出站调用的地方。

总而言之,应用服务

  • 参与命令和查询调度。

  • 作为命令/查询处理的一部分,在必要时调用基础设施组件。

  • 为基础领域模型提供集中的关注点(例如,日志、安全性、度量)。

  • 对其他有界上下文进行标注。

包装结构如图 4-13 所示。

img/473795_1_En_4_Fig13_HTML.jpg

图 4-13

应用服务的包结构

领域

这个包包含有界上下文的域模型。这是有界上下文的域模型的核心,它包含核心业务逻辑的实现。

我们的有界上下文的核心类如下:

  • 总计

  • 实体

  • 价值对象

  • 命令

  • 事件

包装结构如图 4-14 所示。

img/473795_1_En_4_Fig14_HTML.jpg

图 4-14

我们的领域模型的包结构

基础设施

基础设施包有四个主要目的:

  • 当有界上下文接收到与其状态相关的操作(状态的改变、状态的检索)时,它 需要一个底层知识库来处理操作;在我们的例子中,这个存储库是我们的 MySQL 数据库实例。基础设施包包含有界上下文与底层存储库通信所需的所有必要组件。作为我们实现的一部分,我们打算使用 JPA 或 JDBC 来实现这些组件。

  • 当有界上下文需要传递状态变化事件时,它需要底层事件基础结构来发布状态变化事件。在我们的实现中,我们打算使用一个 消息代理作为底层事件基础设施 (RabbitMQ)。基础设施包包含有界上下文与底层消息代理通信所需的所有必要组件。

  • 当一个有界上下文需要与另一个有界上下文同步通信时,它需要一个底层基础设施来支持通过 REST 的服务到服务的通信。基础设施包包含有界上下文与其他有界上下文通信所需的所有必要组件。

  • 我们在基础设施层中包括的最后一个方面是任何种类的特定于微概要文件的配置。

包装结构如图 4-15 所示。

img/473795_1_En_4_Fig15_HTML.jpg

图 4-15

基础设施组件的包装结构

图 4-16 显示了我们任何有界上下文的整个包结构的完整摘要。

img/473795_1_En_4_Fig16_HTML.jpg

图 4-16

任何有界上下文的包结构

这就完成了我们的货物跟踪微服务应用的有界上下文的实现。我们的每个有界上下文都是作为一个微文件应用实现的,使用 Helidon MP 项目,Docker 图像作为工件。有界的上下文被模块整齐地分组在一个包结构中,具有清晰分离的关注点。

让我们进入货物跟踪应用的实现。

货物跟踪实施

本章的下一节将详细介绍货物跟踪应用的实现,它是一个利用 DDD 和 Eclipse MicroProfile (Helidon MP)的微服务应用。

图 4-17 显示了我们的各种 DDD 工件的逻辑分组的高级概述。正如所见,我们需要实现两组工件:

img/473795_1_En_4_Fig17_HTML.jpg

图 4-17

DDD 工件的逻辑分组

  • 域模型 将包含我们的 核心域/业务逻辑

  • 域模型服务 ,其中包含 对我们核心域模型 的支持服务

就域模型的实际实现而言,这转化为特定有界上下文/微服务的各种 值对象、命令和查询

就域模型服务的实际实现而言,这转化为 、接口 应用服务,以及有界上下文/微服务的域模型所需的基础设施

回到我们的货物跟踪器应用,图 4-18 从各种有界上下文及其支持的操作方面说明了我们的微服务解决方案。如所见,这包含每个有界上下文将处理 的各种 命令,每个有界上下文将服务查询,以及每个有界上下文将订阅/发布事件。每个微服务都是一个独立的可部署工件,有自己的存储。

**img/473795_1_En_4_Fig18_HTML.jpg

图 4-18

货物跟踪微服务解决方案

注意

某些代码实现将只包含摘要/片段,以帮助理解实现概念。本章的源代码包含这些概念的完整实现。

领域模型:实现

我们的领域模型是我们的有界上下文的中心特征,并且如前所述,有一组与之相关的工件。这些工件的实现是在 Eclipse MicroProfile 提供的工具的帮助下完成的。

简单总结一下,我们需要实现的领域模型工件如下:

  • 核心域模型——聚集、实体和值对象

  • 域模型操作–命令、查询和事件

让我们逐一查看这些工件,看看 Eclipse Microprofile 为我们实现这些工件提供了哪些相应的工具。

核心领域模型:实现

任何有界上下文的核心域的实现都包括那些将清楚地表达有界上下文的业务意图的工件的识别。在高层次上,这包括聚合、实体和值对象的识别和实现。

聚合/实体/值对象

聚合是我们领域模型的核心。概括地说,我们在每个有界上下文中有四个聚合,如图 4-19 所示。

img/473795_1_En_4_Fig19_HTML.jpg

图 4-19

我们有限的上下文/微服务中的聚合

聚合的实现包括以下几个方面:

  • 聚合类实现

  • 通过业务属性丰富领域

  • 实现实体/值对象

聚合类实现

因为我们打算使用 MySQL 作为每个有界上下文的数据存储,所以我们打算使用 Java EE 规范中的 JPA (Java Persistence API)。JPA 提供了一种定义和实现与底层 SQL 数据存储交互的实体/服务的标准方法。

JPA 集成:Helidon MP

Helidon MP 通过外部整合机制为美国利用 JPA 提供支持。为了包括这种支持,我们需要添加一些额外的配置/依赖关系:

pom.xml

清单 4-2 显示了需要在 Helidon MP 生成的 pom.xml 依赖文件 中进行的更改。

依赖关系列表包括以下内容:

  • Helidon MP JPA 集成支持()heli don-integrations-CDI-JPA)

  • 使用 HikariCP 作为我们的数据源连接池机制

  • Java 的 MySQL 驱动库(MySQL-connector-Java)

<dependency>
    <groupId>io.helidon.integrations.cdi</groupId>
    <artifactId>helidon-integrations-cdi-datasource-hikaricp</artifactId>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
    <groupId>io.helidon.integrations.cdi</groupId>
    <artifactId>helidon-integrations-cdi-jpa</artifactId>
</dependency>
<dependency>
    <groupId>io.helidon.integrations.cdi</groupId>
    <artifactId>helidon-integrations-cdi-jpa-weld</artifactId>
</dependency>
<dependency>
    <groupId>io.helidon.integrations.cdi</groupId>
    <artifactId>helidon-integrations-cdi-eclipselink</artifactId>
</dependency>
<dependency>
    <groupId>jakarta.persistence</groupId>
    <artifactId>jakarta.persistence-api</artifactId>
</dependency>
<dependency>
    <groupId>io.helidon.integrations.cdi</groupId>
    <artifactId>helidon-integrations-cdi-jta</artifactId>
</dependency>
<dependency>

    <groupId>io.helidon.integrations.cdi</groupId>
    <artifactId>helidon-integrations-cdi-jta-weld</artifactId>
</dependency>
<dependency>
    <groupId>javax.transaction</groupId>
    <artifactId>javax.transaction-api</artifactId>
</dependency>
microprofile-config

Listing 4-2.pom.xml dependencies

我们需要为每个 MySQL 数据库实例配置连接属性。清单 4-3 显示了需要添加的配置属性。必要时,您需要用 MySQL 实例的详细信息替换这些值:

javax.sql.DataSource.<<BoundedContext-Name>>.dataSourceClassName=com.mysql.cj.jdbc.MysqlDataSource
javax.sql.DataSource.<<BoundedContext-Name>>.dataSource.url=jdbc:mysql://<<Machine-Name>>:<<Machine-Port>>/<<MySQL-Database-Instance-Name>>
javax.sql.DataSource.<<BoundedContext-Name>>.dataSource.user=<<MySQL-Database-Instance-UserID>>
javax.sql.DataSource.<<BoundedContext-Name>>.dataSource.password=<<MySQL-Database-Instance-Password>>
persistence.xml

Listing 4-3.Configuration information for the datasource connectivity

最后一步是配置一个 JPA " 持久性单元 "映射到在micro profile-config文件中配置的数据源,如清单 4-4 所示:

<persistence version="2.2"  xmlns:="http://xmlns.jcp.org/xml/ns/persistence  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence  http://xmlns.jcp.org/xml/ns/persistence/persistence_2_2.xsd">
<persistence-unit name="<<BoundedContext-Name>>" transaction-type="JTA">
    <jta-data-source><<BoundedContext-Datasource>></jta-data-source>
</persistence-unit>
</persistence>

Listing 4-4.Configuration information for the persistence unit

我们现在准备在我们的微概要应用中实现 JPA。除非另有说明,否则我们所有有界上下文中的所有聚合都实现了相同的机制。

我们的每个根聚合类都被实现为一个 JPA 实体。JPA 没有提供特定的注释来将特定的类作为根聚合进行注释,所以我们采用常规的 POJO 并使用 JPA 提供的标准注释@Entity。以 Booking Bounded Context 为例,它将 Cargo 作为根聚合,清单 4-5 显示了 JPA 实体所需的最小代码:

package com.practicalddd.cargotracker.bookingms.domain.model.aggregates;
import javax.persistence.∗;
@Entity //JPA Entity Marker
public class Cargo {
}

Listing 4-5.Cargo root aggregate implemented as a JPA Entity

每个 JPA 实体都需要一个标识符。对于我们的聚合标识符实现,我们选择从 MySQL 序列中为我们的货物聚合生成一个 【技术/代理标识符(主键) 。除了技术标识符,我们还选择拥有一个 业务密钥

业务键传达聚合标识符 clear 的业务意图,即新预订货物的预订标识符,并且是向域模型的外部消费者公开的键(稍后将详细介绍)。另一方面,技术关键字是集合标识符的纯内部表示,并且对于在集合及其依赖对象(参见下面的值对象/实体)之间的有界上下文 内维护关系 是有用的。

继续我们在 Booking Bounded 上下文中的 Cargo Aggregate 的例子,我们将技术和业务键添加到类实现中,直到现在。

清单 4-6 演示了这一点。“@ Id”注释标识我们的货物集合上的主键。没有特定的注释来标识业务键,所以我们只是使用 JPA 提供的“@ Embedded ”注释将其实现为常规的 POJO ( BookingId )和Embeddedit:

package com.practicalddd.cargotracker.bookingms.domain.model.aggregates;
import javax.persistence.∗;
@Entity
public class Cargo {
    @Id //Identifier Annotation provided by JPA
    @GeneratedValue(strategy = GenerationType.AUTO) // Rely on a MySQL generated sequence
    private Long id;
    @Embedded //Annotation which enables usage of Business Objects instead of primitive types
    private BookingId bookingId; // Business Identifier
}

Listing 4-6.Cargo root aggregate identifier implementation

清单 4-7 显示了 BookingId 业务键类的实现:

package com.practicalddd.cargotracker.bookingms.domain.model.aggregates;
import javax.persistence.∗;
import java.io.Serializable;
/∗∗
 ∗ Business Key Identifier for the Cargo Aggregate
 ∗/
@Embeddable
public class BookingId implements Serializable {
    @Column(name="booking_id")
    private String bookingId;
    public BookingId(){}
    public BookingId(String bookingId){this.bookingId = bookingId;}
    public String getBookingId(){return this.bookingId;}
}

Listing 4-7.BookingId business key class implementation

我们现在有了一个使用 JPA 的聚合(Cargo)的基本实现。除了处理活动绑定的上下文之外,其他聚合具有相同的实现机制。由于它是一个事件日志,我们决定只为聚合实现一个键,即活动 Id。

图 4-20 总结了我们所有聚合的基本实现。

img/473795_1_En_4_Fig20_HTML.jpg

图 4-20

我们聚合的基本实现

领域丰富性:业务属性

准备好基本的实现后,让我们继续讨论集合的核心部分——域丰富性。 任何有界语境的聚合应该能够清晰地表达有界语境的业务语言 。本质上,用纯技术术语来说,它意味着我们的集合不应该是贫血的,也就是说,只包含 getter/setter 方法。

一个缺乏活力的集合违背了 DDD 的基本原则,因为它本质上意味着 在应用的多个层中表达的商业语言 ,从长远来看,这反过来又会导致一个不可维护的软件。

**那么我们如何实现一个域丰富的聚合呢?简而言之就是 业务属性和业务方式 。在这一节中,我们的重点将放在业务属性方面,而我们将把业务方法部分作为域模型操作实现的一部分。

聚合的业务属性捕获聚合的状态,作为使用业务术语而不是技术术语描述的属性。

让我们看一下我们的货物总量的例子。

将状态转换为业务概念,货物集合具有以下属性:

  • 货物的始发地

  • 货物的预订金额

  • 路线规格 (始发地、目的地、目的地到达截止日期)。

  • 根据路线规格将货物分配到的 路线 。路线由多段 路程 组成,货物可能会通过这些路程到达目的地。

  • 货物的交付进度 对照其指定的路线规格和路线。交货进度提供了关于 路线状态、运输状态、货物的当前航程、货物的最后已知位置、下一个预期活动 和货物上发生的最后活动 的细节。

图 4-21 显示了货物集合及其与相关对象的关系。请注意我们如何能够在 的纯业务术语 中清楚地表示货物总量。

img/473795_1_En_4_Fig21_HTML.jpg

图 4-21

货物集合及其从属关联

JPA 为我们提供了一组注释 (@Embedded,@ Embedded),帮助我们使用业务对象实现聚合类。

清单 4-8 显示了我们的货物集合的例子,其中所有的 依赖项都被建模为业务对象 :

package com.practicalddd.cargotracker.bookingms.domain.model.aggregates;
import javax.persistence.∗;

import com.practicalddd.cargotracker.bookingms.domain.model.entities.∗;
import com.practicalddd.cargotracker.bookingms.domain.model.valueobjects.∗;
@Entity
public class Cargo {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;
    @Embedded
    private BookingId bookingId; // Aggregate Identifier
    @Embedded
    private BookingAmount bookingAmount; //Booking Amount
    @Embedded
    private Location origin; //Origin Location of the Cargo
    @Embedded
    private RouteSpecification routeSpecification; //Route Specification of the Cargo
    @Embedded
    private CargoItinerary itinerary; //Itinerary Assigned to the Cargo
    @Embedded
    private Delivery delivery; // Checks the delivery progress of the cargo against the actual Route Specification and Itinerary
}

Listing 4-8.Business object dependencies for the Cargo root aggregate

聚合的依赖类 建模为实体对象或值对象 。概括地说,有界上下文中的实体对象有自己的身份,但总是存在于根聚合中,也就是说,它们不能独立存在,并且在聚合的整个生命周期中从不改变。另一方面,值对象没有自己的身份,在聚合的任何实例中很容易被替换。

继续我们的示例,在货物集合中,我们有以下内容:

  • 【原点】 的货物作为一个 的实体对象 (位置)。这在货物集合实例中不能改变,因此被建模为实体对象。

  • 预订金额 的货物, 路线规格 的货物, 的货物行程 分配给货物,而 交付的货物 作为价值对象。这些对象在任何货物集合实例中都是可替换的,因此被建模为值对象。

让我们浏览一下场景和基本原理,为什么我们将它们作为值对象而不是实体,因为这是一个 重要的领域建模决策 :

  • 当预订一个新的货物时,我们会有 一个新的路线规格一个空货行程没有交货进度

  • 当货物被分配一个行程单时, 空载货物行程单 被一个 已分配货物行程单 替代。

  • 当货物作为其行程的一部分通过多个港口时, 交付 进度在根集合内被更新和替换。

  • 最后,如果客户选择更改货物的交货地点或交货截止日期,则 路线规格更改 ,分配一个 新的货物行程交货重新计算订舱金额更改

它们 都是可替换的,因此被建模为值对象 。这就是在聚合 中建模实体和值对象的 经验法则。

实现实体对象/值对象

使用 JPA 提供的***" @ embedded "***注释,将实体对象/值对象实现为 JPA 可嵌入对象。然后使用“ @Embedded ”注释将它们嵌入到聚合中。

清单 4-9 展示了嵌入到聚合类中的机制。

让我们看一下 Cargo Aggregate 的实体对象/值对象的实现。

清单 4-9 展示了 位置实体对象 。注意分组在 model.entities 下的包名( ):

package com.practicalddd.cargotracker.bookingms.domain.model.entities;
import javax.persistence.Column;
import javax.persistence.Embeddable;
/∗∗
 ∗ Location Entity class represented by a unique 5-digit UN Location code.
 ∗/
@Embeddable
public class Location {
    @Column(name = "origin_id")
    private String unLocCode;
    public Location(){}
    public Location(String unLocCode){this.unLocCode = unLocCode;}
    public void setUnLocCode(String unLocCode){this.unLocCode = unLocCode;}
    public String getUnLocCode(){return this.unLocCode;}
}

Listing 4-9.Location entity class implementation

清单 4-10 展示了 预订金额/路线规格值对象 的示例。注意分组在 model.valueobjects 下的包名( ):

package com.practicalddd.cargotracker.bookingms.domain.model.valueobjects;
import javax.persistence.Column;
import javax.persistence.Embeddable;
/∗∗
 ∗ Domain model representation of the Booking Amount for a new Cargo.
 ∗ Contains the Booking Amount of the Cargo
 ∗/
@Embeddable
public class BookingAmount {
    @Column(name = "booking_amount", unique = true, updatable= false)
    private Integer bookingAmount;
    public BookingAmount(){}
    public BookingAmount(Integer bookingAmount){this.bookingAmount = bookingAmount;}
    public void setBookingAmount(Integer bookingAmount){this.bookingAmount = bookingAmount;}
    public Integer getBookingAmount(){return this.bookingAmount;}
}

Listing 4-10.Booking Amount Value object implementation

清单 4-11 展示了 路线规范值对象 :

package com.practicalddd.cargotracker.bookingms.domain.model.valueobjects;
import com.practicalddd.cargotracker.bookingms.domain.model.entities.Location;
import javax.persistence.∗;
import javax.validation.constraints.NotNull;
import java.util.Date;
/∗∗
 ∗ Route Specification of the Booked Cargo
 ∗/
@Embeddable
public class RouteSpecification {
    private static final long serialVersionUID = 1L;
    @Embedded
    @AttributeOverride(name = "unLocCode", column = @Column(name = "spec_origin_id"))
    private Location origin;
    @Embedded
    @AttributeOverride(name = "unLocCode", column = @Column(name = "spec_destination_id"))
    private Location destination;
    @Temporal(TemporalType.DATE)
    @Column(name = "spec_arrival_deadline")
    @NotNull
    private Date arrivalDeadline;
    public RouteSpecification() { }
    /∗∗
     ∗ @param origin origin location
     ∗ @param destination destination location
     ∗ @param arrivalDeadline arrival deadline
     ∗/
    public RouteSpecification

(Location origin, Location destination,
                              Date arrivalDeadline) {
        this.origin = origin;
        this.destination = destination;
        this.arrivalDeadline = (Date) arrivalDeadline.clone();
    }
    public Location getOrigin() {
        return origin;
    }
    public Location getDestination() {
        return destination;
    }

    public Date getArrivalDeadline() {
        return new Date(arrivalDeadline.getTime());
    }
}

Listing 4-11.Route Specification Value object implementation

其余的值对象( RouteSpecification、Cargo internary**、** 和 Delivery )使用“@ Embedded”注释以相同的方式实现,并使用“ @Embedded ”注释嵌入到货物聚合中。

注意

完整的实现请参考本章的源代码。

让我们看看其他聚合(处理活动、航行和跟踪)的简化类图。图 4-22 、 4-23 和 4-24 说明了这一点。

img/473795_1_En_4_Fig24_HTML.jpg

图 4-24

跟踪活动及其相关关联

img/473795_1_En_4_Fig23_HTML.jpg

图 4-23

航程及其从属关联

img/473795_1_En_4_Fig22_HTML.jpg

图 4-22

处理活动及其相关关联

这就完成了核心域模型的实现。接下来让我们看看域模型操作的实现。

注意

本书的源代码通过包分离展示了核心领域模型。您可以在github . com/practical DD查看源代码,以便更清楚地了解域模型中的对象类型。

领域模型操作

有界上下文中的域模型操作处理与有界上下文集合的状态相关联的任何种类的操作。这包括更改聚合状态( 命令 )、检索聚合的当前状态( 查询 )或通知聚合状态更改( 事件 )的操作。

命令

命令负责在有界上下文中改变聚集的状态。

在有界环境中实现命令包括以下步骤:

  • 命令的识别/执行

  • 识别/实现处理命令的命令处理程序

命令的识别

命令的识别围绕着识别影响集合状态的任何操作。例如,预订命令有界上下文具有以下操作或命令:

  • 预订货物

  • 运送货物

这两种操作都导致有界环境内货物集合体的状态改变,因此被识别为命令。

命令的实现

一旦被识别,使用常规的 POJOs 在微概要实现中实现被识别的命令。清单 4-12 展示了 BookCargoCommand 类对于 Book Cargo 命令的实现:

package com.practicalddd.cargotracker.bookingms.domain.model.commands;
import java.util.Date;
/∗∗
 ∗ Book Cargo Command class
 ∗/
public class BookCargoCommand {

    private String bookingId;
    private int bookingAmount;
    private String originLocation;
    private String destLocation;
    private Date destArrivalDeadline;

    public BookCargoCommand(){}

    public BookCargoCommand(int bookingAmount,
                            String originLocation, String destLocation, Date destArrivalDeadline){

        this.bookingAmount = bookingAmount;
        this.originLocation = originLocation;
        this.destLocation = destLocation;
        this.destArrivalDeadline = destArrivalDeadline;
    }

    public void setBookingId(String bookingId){this.bookingId = bookingId;}

    public String getBookingId(){return this.bookingId;}

    public void setBookingAmount(int bookingAmount){
        this.bookingAmount = bookingAmount;
    }

    public int getBookingAmount(){
        return this.bookingAmount; 

    }

    public String getOriginLocation() {return originLocation; }

    public void setOriginLocation(String originLocation) {this.originLocation = originLocation; }

    public String getDestLocation() { return destLocation; }

    public void setDestLocation(String destLocation) { this.destLocation = destLocation; }

    public Date getDestArrivalDeadline() { return destArrivalDeadline; }

    public void setDestArrivalDeadline(Date destArrivalDeadline) { this.destArrivalDeadline = destArrivalDeadline; }
}

Listing 4-12.BookCargoCommand class implementation

命令处理程序的标识

每个命令都有一个相应的命令处理程序。命令处理程序 的作用是处理输入的命令并设置集合的状态。命令处理程序是 域模型中唯一设置聚合状态的地方 。这是一个严格的规则,需要遵循它来帮助实现一个丰富的领域模型。

命令处理程序的实现

由于 Eclipse MicroProfile 平台不提供任何现成的功能来实现命令处理程序,我们的实现方法将是 只识别集合上的例程,这些例程可以表示为命令处理程序 。对于我们的第一个命令 【书货】*我们把 构造器标识为我们的命令处理程序;**而对于我们的第二个命令我们创建一个新的例程“【assignor Route()”作为我们的命令处理程序。*****

******清单 4-13 显示了货物集合构造器的代码片段。构造函数接受 BookCargoCommand 作为输入参数,并设置相应的聚集状态:

    /∗∗
     ∗ Constructor Command Handler for a new Cargo booking
     ∗/
    public Cargo(BookCargoCommand bookCargoCommand){
        this.bookingId = new BookingId(bookCargoCommand.getBookingId());
        this.routeSpecification = new RouteSpecification(
                    new Location(bookCargoCommand.getOriginLocation()),
                    new Location(bookCargoCommand.getDestLocation()),
                    bookCargoCommand.getDestArrivalDeadline()
            );
        this.origin = routeSpecification.getOrigin();
        this.itinerary = CargoItinerary.EMPTY_ITINERARY; //Empty Itinerary since the Cargo has not been routed yet
        this.bookingAmount = bookingAmount;
        this.delivery = Delivery.derivedFrom(this.routeSpecification,
                this.itinerary, LastCargoHandledEvent.EMPTY);
    }

Listing 4-13.BookCargoCommand command handler

清单 4-14 显示了assigno route()命令处理程序的代码片段。它接受RouteCargoCommand类作为输入,并设置聚合的状态:

    /∗∗
     ∗ Command Handler for the Route Cargo Command. Sets the state of the Aggregate and registers the
     ∗ Cargo routed event
     ∗ @param routeCargoCommand
     ∗/
    public void assignToRoute(RouteCargoCommand routeCargoCommand) {
        this.itinerary = routeCargoCommand.getCargoItinerary();
        // Handling consistency within the Cargo aggregate synchronously
        this.delivery = delivery.updateOnRouting(this.routeSpecification,
                this.itinerary);

 }

Listing 4-14.RouteCargoCommand command handler

图 4-25 展示了我们的命令处理程序实现的类图。

img/473795_1_En_4_Fig25_HTML.jpg

图 4-25

命令处理程序实现的类图

总之,命令处理程序在管理有限上下文中的聚合状态方面起着非常重要的作用。命令处理程序的实际调用是通过应用服务进行的,我们将在下面的章节中看到。

问题

有界上下文中的查询负责 向外部消费者提供有界上下文的集合 的状态。

为了实现查询,我们利用 JPA 命名查询 **,**即可以定义在聚合上的查询,以各种形式检索状态。清单 4-15 展示了来自 Cargo Aggregate 的代码片段,它定义了需要可用的查询。在这种情况下,我们有三个查询-查找所有货物,通过货物的预订标识符查找货物,以及所有货物的最终预订标识符 :

@NamedQueries({
        @NamedQuery(name = "Cargo.findAll",
                query = "Select c from Cargo c"),
        @NamedQuery(name = "Cargo.findByBookingId",
                query = "Select c from Cargo c where c.bookingId = :bookingId"),
        @NamedQuery(name = "Cargo.findAllBookingIds",
                query = "Select c.bookingId from Cargo c") })
public class Cargo{}

Listing 4-15.Named Queries within the Cargo root aggregate

总之,查询扮演着在有限的上下文中呈现聚合状态的角色。这些查询的实际调用和执行是通过应用服务和存储库类进行的,我们将在接下来的章节中看到。

这就完成了域模型中查询的实现。我们现在将看到如何实现事件。

事件

有界上下文中的事件是将 有界上下文的聚集状态改变为事件 的任何操作。由于命令会改变聚合的状态,因此可以有把握地假设在有界上下文中的任何命令操作都会导致相应的事件。这些事件的订阅者可以是同一域内的其他有界上下文,也可以是属于任何其他外部域的有界上下文。

域事件在微服务架构中扮演着核心角色,以健壮的方式实现它们非常重要。微服务架构的分布式本质要求通过 编排机制使用事件,在基于微服务的应用的各种有界上下文之间保持状态和事务一致性

图 4-26 举例说明了在货物跟踪应用的各种有界上下文之间流动的事件。

img/473795_1_En_4_Fig26_HTML.jpg

图 4-26

微服务架构中的事件流

让我们用一个商业案例来解释这一点。

当货物被分配路线时,这意味着货物现在可以被跟踪,这需要向货物发出跟踪标识符。货物路线的分配是在预订有界环境中处理的,而跟踪标识符的发布是在跟踪有界环境中处理的。在整体式工作方式中,为货物分配路线和发布跟踪标识符的过程同时发生,因为 由于流程、运行时和数据存储 的共享模型,我们可以跨多个有界上下文维护相同的事务上下文。

然而,在微服务架构中,这是不可能实现的,因为它是一个 无共享架构 。当货物被分配一条路线时,Booking Bounded 上下文只负责确保货物集合的状态反映新的路线。跟踪有界上下文需要知道这种状态的变化,以便它能够相应地发布跟踪标识符,从而 完成业务用例 。这就是 域事件和 事件编排发挥重要作用的地方。如果货物绑定上下文可以引发货物集合已经被分配了路线的事件,则跟踪绑定上下文可以订阅该特定事件并发布跟踪标识符来完成该业务用例。 引发事件将事件 传递到各种有界上下文来完成一个业务用例的机制是 事件 编排模式 。简而言之,领域事件/事件编排有助于构建事件驱动的微服务应用。

实现健壮的事件驱动的编排架构有四个阶段:

  • 注册 需要从有界上下文中提出的域事件。

  • 从有界上下文中提出 需要发布的领域事件。

  • 发布 从有界上下文中引发的事件。

  • 订阅 其他有界上下文中已经发布的事件。

考虑到 MicroProfile 平台提供的功能,实现被划分到多个领域:

  • 事件的引发由 应用服务 实现。

  • 事件的发布由出站服务 实现

  • 订阅事件由接口/入站服务 处理

作为我们实现领域事件的一部分,我们将使用 CDI 事件作为逻辑基础设施 用于事件发布/订阅,同时我们将使用 RabbitMQ 为我们提供物理基础设施 来实现事件编排。

由于我们正处于实现领域模型的阶段,所以我们将在本节中涉及的唯一领域是创建事件类,这些事件类跨多个有界上下文参与编排。本章的后续部分将处理其他方面 (应用服务将涵盖这些事件的引发的实现,出站服务将涵盖事件的发布的实现,入站服务将涵盖事件的订阅的实现)

使用“ @interface ”注释将域模型中的事件类创建为自定义注释。当我们实现事件编排架构的其他领域时,我们将在后面的小节中看到这些事件的用法。

清单 4-16 展示了作为定制注释实现的 CargoBookedEvent:

import javax.inject.Qualifier;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.PARAMETER;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
/∗∗
 ∗ Event Class for the Cargo Booked Event. Implemented as a custom annotation
 ∗/
@Qualifier
@Retention(RUNTIME)
@Target({FIELD, PARAMETER})
public @interface CargoBookedEvent {
}

Listing 4-16.CargoBookedEvent stereotype annotation

类似地,清单 4-17 演示了 CargoRoutedEvent 事件类的实现:

import javax.inject.Qualifier;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.PARAMETER;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

/∗∗
 ∗ Event Class for the Cargo Routed Event. Wraps up the Cargo
 ∗/

@Qualifier
@Retention(RUNTIME)
@Target({FIELD, PARAMETER})
public @interface CargoRoutedEvent {
}

Listing 4-17.CargoRoutedEvent stereotype annotation

这就完成了概念的 演示,以实现核心域模型的 。现在让我们转到 实现领域模型服务

领域模型服务

使用域模型服务有两个主要原因。第一种是通过 明确定义的接口 使有界上下文的状态 对外部方 可用。第二是 与外部方 交互,将有界上下文的状态持久化到 数据存储库 (数据库),将有界上下文的状态改变事件发布到外部 消息代理与其他有界上下文 通信。

**对于任何有界的上下文,有三种类型的域模型服务:

  • 入站服务 其中我们实现了定义良好的接口,使外部各方能够与域模型进行交互

  • 出站服务 在这里,我们实现了与外部存储库/其他有界上下文的所有交互

  • 应用服务 ,它充当域模型与入站和出站服务之间的外观层

图 4-27 展示了域模型服务的实现。

img/473795_1_En_4_Fig27_HTML.jpg

图 4-27

域模型服务实现摘要

入站服务

入站服务(或六角形架构模式中表示的入站适配器)充当我们的核心域模型的最外层网关。如上所述,它涉及到定义良好的接口的实现,这些接口使外部消费者能够与核心域模型进行交互。

入站服务 的类型取决于我们需要向 公开的操作 的类型,启用域模型 的外部消费者。

考虑到我们正在为货物跟踪应用实现微服务架构模式,我们提供两种类型的入站服务:

  • 一个基于 REST 的 API 层,供外部消费者调用有界上下文上的操作( 命令/查询 )

  • 基于 CDI 事件 的事件处理层,它消费来自消息代理的事件并处理它们

应用接口

REST API 的职责是代表有界上下文接收来自外部消费者的 HTTP 请求。这个请求可能是命令或查询。REST API 层的职责是将其翻译成由有界上下文的域模型识别的命令/查询模型,并将其委托给应用服务层进行进一步处理。

回头看图 4-5 ,其中详细列出了各种受限上下文 (例如,预订货物、分配货物路线、处理货物、跟踪货物) 的所有操作,所有这些操作都将有相应的 REST APIs 来接受这些请求并处理它们。

Eclipse MicroProfile 中 REST API 的实现是通过利用 Helidon MP 基于 JAX-RS (用于 RESTful Web 服务的 Java API)提供的 REST 功能。顾名思义,这个规范提供了构建 RESTful web 服务的能力。 Helidon MP 为这个规范 提供了一个实现,当我们创建脚手架项目时,这个功能会自动添加进来。

让我们看一个使用 JAX RS 构建的 REST API 的例子。清单 4-18 描述了 CargoBookingController 类 ,它为我们的 Cargo Booking 命令 提供了一个 REST API。

  • REST API 可在网址“/ cargobooking ”获得。

  • 它有一个 POST 方法,接受 BookCargoResource,这是 API 的输入有效负载。

  • 它依赖于 CargoBookingCommandService,后者是一个应用服务,充当一个外观(参见下面的实现)。利用“ @Inject ”注释将该依赖注入到 API 类中。这个注释是 CDI 的一部分。

  • 它使用汇编器实用程序类(BookCargoCommandDTOAssembler)将资源数据(BookCargoResource)转换为命令模型( BookCargoCommand )。

  • 转换后,它将流程委托给 CargoBookingCommandService 进行进一步处理。

  • 它用新预订货物的预订标识符向外部消费者返回一个响应,响应状态为“200 OK ”。

package com.practicalddd.cargotracker.bookingms.interfaces.rest;
import com.practicalddd.cargotracker.bookingms.application.internal.commandservices.CargoBookingCommandService;
import com.practicalddd.cargotracker.bookingms.domain.model.aggregates.BookingId;
import com.practicalddd.cargotracker.bookingms.interfaces.rest.dto.BookCargoResource; 

import com.practicalddd.cargotracker.bookingms.interfaces.rest.transform.BookCargoCommandDTOAssembler;

import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;

@Path("/cargobooking")
@ApplicationScoped
public class CargoBookingController {
    private CargoBookingCommandService cargoBookingCommandService; // Application Service Dependency

    /∗∗
     ∗ Inject the dependencies (CDI)@param cargoBookingCommandService
     ∗/
    @Inject
    public CargoBookingController(CargoBookingCommandService cargoBookingCommandService){
        this.cargoBookingCommandService = cargoBookingCommandService; 

    }

    /∗∗
     ∗ POST method to book a cargo
     ∗ @param bookCargoResource
     ∗/

    @POST
    @Produces(MediaType.APPLICATION_JSON)
    public Response bookCargo(BookCargoResource bookCargoResource){
        BookingId bookingId  = cargoBookingCommandService.bookCargo(
                BookCargoCommandDTOAssembler.toCommandFromDTO(bookCargoResource));

        final Response returnValue = Response.ok()
                .entity(bookingId)
                .build();
        return returnValue;
    }
}

Listing 4-18.CargoBooking controller class implementation

清单 4-19 展示了book cargo resource类的实现:

package com.practicalddd.cargotracker.bookingms.interfaces.rest.dto;

import java.time.LocalDate;

/∗∗
 ∗ Resource class for the Book Cargo Command API
 ∗/
public class BookCargoResource {

    private int bookingAmount;
    private String originLocation;
    private String destLocation;
    private LocalDate destArrivalDeadline;

    public BookCargoResource(){}

    public BookCargoResource(int bookingAmount,
                             String originLocation, String destLocation, LocalDate destArrivalDeadline){

        this.bookingAmount = bookingAmount;
        this.originLocation = originLocation;
        this.destLocation = destLocation;
        this.destArrivalDeadline = destArrivalDeadline;
    }

    public void setBookingAmount(int bookingAmount){
        this.bookingAmount = bookingAmount; 

    }

    public int getBookingAmount(){
        return this.bookingAmount;
    }

    public String getOriginLocation() {return originLocation; }

    public void setOriginLocation(String originLocation) {this.originLocation = originLocation; }

    public String getDestLocation() { return destLocation; }

    public void setDestLocation(String destLocation) { this.destLocation = destLocation; }

    public LocalDate getDestArrivalDeadline() { return destArrivalDeadline; }

    public void setDestArrivalDeadline(LocalDate destArrivalDeadline) { this.destArrivalDeadline = destArrivalDeadline; }

}

Listing 4-19.BookCargo resource class for the controller

清单 4-20 显示了BookCargoCommandDTOAssembler类的实现:

package com.practicalddd.cargotracker.bookingms.interfaces.rest.transform;

import com.practicalddd.cargotracker.bookingms.domain.model.commands.BookCargoCommand;
import com.practicalddd.cargotracker.bookingms.interfaces.rest.dto.BookCargoResource;

/∗∗
 ∗ Assembler class to convert the Book Cargo Resource Data to the Book Cargo Model
 ∗/
public class BookCargoCommandDTOAssembler {

    /∗∗
     ∗ Static method within the Assembler class
     ∗ @param bookCargoResource
     ∗ @return BookCargoCommand Model
     ∗/
    public static BookCargoCommand toCommandFromDTO(BookCargoResource bookCargoResource){

        return new BookCargoCommand(
                       bookCargoResource.getBookingAmount(),
                       bookCargoResource.getOriginLocation(),
                       bookCargoResource.getDestLocation(),
                       java.sql.Date.valueOf(bookCargoResource.getDestArrivalDeadline()));
    }
}

Listing 4-20.DTO Assembler class implementation

清单 4-21 展示了 BookCargoCommand 类的实现:

package com.practicalddd.cargotracker.bookingms.domain.model.commands;

import java.util.Date;

/∗∗
 ∗ Book Cargo Command class
 ∗/
public class BookCargoCommand {

    private int bookingAmount;
    private String originLocation;
    private String destLocation;
    private Date destArrivalDeadline;

    public BookCargoCommand(){}

    public BookCargoCommand(int bookingAmount,
                            String originLocation, String destLocation, Date destArrivalDeadline){

        this.bookingAmount = bookingAmount;
        this.originLocation = originLocation;
        this.destLocation = destLocation;
        this.destArrivalDeadline = destArrivalDeadline;
    }

    public void setBookingAmount(int bookingAmount){
        this.bookingAmount = bookingAmount;
    }

    public int getBookingAmount(){
        return this.bookingAmount;
    }

    public String getOriginLocation() {return originLocation; }

    public void setOriginLocation(String originLocation) {this.originLocation = originLocation; }

    public String getDestLocation() { return destLocation; }

    public void setDestLocation(String destLocation) { this.destLocation = destLocation; }

    public Date getDestArrivalDeadline() { return destArrivalDeadline; }

    public void setDestArrivalDeadline(Date destArrivalDeadline) { this.destArrivalDeadline = destArrivalDeadline; }
}

Listing 4-21.BookCargoCommand class implementation

图 4-28 展示了我们实现的类图。

img/473795_1_En_4_Fig28_HTML.jpg

图 4-28

REST API 实现的类图

我们所有的入站 REST API 实现都遵循相同的方法,如图 4-29 所示。

img/473795_1_En_4_Fig29_HTML.jpg

图 4-29

入站服务实施流程摘要

  1. 对命令/查询的入站请求到达 REST API。API 等级是使用 Helidon MP 提供的 JAX 遥感功能实现的。

  2. REST API 类使用实用汇编器组件将资源数据格式转换为域模型所需的命令/查询数据格式。

  3. 命令/查询数据被发送到应用服务以供进一步处理。

事件处理程序

我们的有界上下文中存在的另一种类型的接口是事件处理程序。在有界上下文中,事件处理程序负责处理有界上下文感兴趣的事件。这些事件由 applicationThese 中的其他有界上下文引发。“ 事件处理程序 ”在驻留在 入站/接口 层内的订阅有界上下文内创建。事件处理程序接收事件以及事件有效负载数据,并将它们作为常规操作进行处理。

清单 4-22 展示了位于跟踪有界上下文中的“”cargoroutdeventhandler。它 观察CargoRoutedEvent并接收“CargoRoutedEventData”作为净荷:

package com.practicalddd.cargotracker.trackingms.interfaces.events;

import com.practicalddd.cargotracker.shareddomain.events.CargoRoutedEvent;
import com.practicalddd.cargotracker.shareddomain.events.CargoRoutedEventData;

import javax.enterprise.context.ApplicationScoped;
import javax.enterprise.event.Observes;

@ApplicationScoped
public class CargoRoutedEventHandler {

    public void receiveEvent(@Observes @CargoRoutedEvent CargoRoutedEventData eventData) {
        //Process the Event
    }
}

Listing 4-22.CargoRouted event handler implementation

我们所有的事件处理器实现都遵循图 4-30 所示的相同方法。

img/473795_1_En_4_Fig30_HTML.jpg

图 4-30

事件处理程序实现的实现过程摘要

  1. 事件处理程序从消息代理接收入站事件。

  2. 事件处理程序使用实用汇编器组件将资源数据格式转换为域模型所需的命令数据格式。

  3. 命令数据被发送到应用服务以供进一步处理。

将入站事件映射到我们的消息代理(RabbitMQ)的相应物理队列的实现包含在 出站服务-消息代理 实现中。

应用服务程序

应用服务在有界上下文中充当入站/出站服务和核心域模型之间的门面或端口。

在一个有界上下文内,应用服务负责 接收来自入站服务 的请求,并委托给相应的服务,即命令委托给 命令服务 ,查询委托给 查询服务 ,与其他有界上下文通信的请求委托给 出站服务 。作为命令/查询/外部有界上下文通信处理的一部分,应用服务可能需要与 存储库、消息代理或其他有界上下文进行通信。出站服务 用于帮助这种通信。

最后,由于 MicroProfile 规范不提供直接从域模型引发域事件的能力,我们依赖于 应用服务来引发域事件。 使用出站服务将域事件发布到消息代理上。

应用服务类使用 CDI 托管 bean 实现 ***。 Helidon MP 为 CDI *提供了一个实现,当我们创建脚手架项目时,这个功能会自动添加进来。

图 4-31 描述了应用服务的职责。

img/473795_1_En_4_Fig31_HTML.jpg

图 4-31

应用服务的责任

应用服务:命令/查询委托

作为该职责的一部分,有界上下文中的应用服务接收处理命令/查询的请求。这些请求通常来自入站服务(API 层)。作为处理的一部分,应用服务首先利用域模型的 命令处理程序/查询处理程序 (参见域模型部分)来设置状态或查询状态。然后,它们利用出站服务来保存状态或执行对聚合状态的查询。

让我们看一个命令委托者应用服务类的例子, 货物预订命令应用服务类 ,它处理所有与预订相关的命令。我们还将更详细地查看其中一个命令, 预订货物命令 **,**这是预订新货物的指令:

  • Application services 类被实现为一个附加了作用域的 CDI Bean(在本例中为 @Application )。

  • 通过@Inject 注释为应用服务类提供了必要的依赖关系。在这种情况下,CargoBookingCommandApplicationService 类依赖于一个 出站服务存储库类(cargo repository),它使用该存储库类来持久存储新创建的货物。它还依赖于“cargobokedevent”,一旦货物被持久化,就需要引发该事件。CargoBookedEvent 是一个原型类,将货物作为其有效负载事件。我们将在接下来的小节中更详细地查看域事件,所以现在让我们继续。

  • 应用服务将处理委托给“ bookCargo ”方法。在处理该方法之前,应用服务确保通过 "@Transactional" 注释打开新的事务。

  • 应用服务类将新预订的货物存储在预订 MySQL 数据库表(cargo)中,并触发 货物预订事件

  • 它向入站接口返回一个响应,其中包含新预订货物的预订标识符。

清单 4-23 演示了 货物订舱命令应用服务类 的实现:

package com.practicalddd.cargotracker.bookingms.application.internal.commandservices;

import com.practicalddd.cargotracker.bookingms.domain.model.aggregates.BookingId;
import com.practicalddd.cargotracker.bookingms.domain.model.aggregates.Cargo;
import com.practicalddd.cargotracker.bookingms.domain.model.commands.BookCargoCommand;
import com.practicalddd.cargotracker.bookingms.domain.model.entities.Location;
import com.practicalddd.cargotracker.bookingms.domain.model.events.CargoBookedEvent;
import com.practicalddd.cargotracker.bookingms.domain.model.valueobjects.BookingAmount;
import com.practicalddd.cargotracker.bookingms.domain.model.valueobjects.RouteSpecification;
import com.practicalddd.cargotracker.bookingms.infrastructure.repositories.jpa.CargoRepository;

import javax.enterprise.context.ApplicationScoped;
import javax.enterprise.event.Event;
import javax.inject.Inject;
import javax.transaction.Transactional;

/∗∗
 ∗ Application Service class

for the Cargo Booking Command
 ∗/
@ApplicationScoped // Scope of the CDI Managed Bean. Application Scope indicates a single instance for the service class.
public class CargoBookingCommandService  // CDI Managed Bean
{

    @Inject
    private CargoRepository cargoRepository; // Outbound Service to connect to the Booking Bounded Context MySQL Database Instance

    @Inject
    @CargoBookedEvent
    private Event<Cargo> cargoBooked; // Event that needs to be raised when the Cargo is Booked

    /∗∗
     ∗ Service Command method to book a new Cargo@return BookingId of the Cargo

     ∗/
    @Transactional // Inititate the Transaction
    public BookingId bookCargo(BookCargoCommand bookCargoCommand){

        BookingId bookingId = cargoRepository.nextBookingId();

        RouteSpecification routeSpecification = new RouteSpecification(
                new Location(bookCargoCommand.getOriginLocation()),
                new Location(bookCargoCommand.getDestLocation()),
                bookCargoCommand.getDestArrivalDeadline()
        );

        BookingAmount bookingAmount = new BookingAmount(bookCargoCommand.getBookingAmount());
        Cargo cargo = new Cargo(
                bookingId,
                bookingAmount,
                routeSpecification);
        cargoRepository.store(cargo); //Store the Cargo

        cargoBooked.fire(cargo); // Fire the Cargo Booked Event

        return bookingId;
    }

    // All other implementations of Commands for the Booking Bounded Context

}

Listing 4-23.Cargo Booking Command Application services implementation class

清单 4-24 展示了 货物预订查询应用服务类 的实现,它服务于与预订相关的所有查询。除了 没有引发任何域事件 之外,实现与货物预订命令应用服务类相同,因为它只是查询有界上下文的状态,而不改变其状态:

package com.practicalddd.cargotracker.bookingms.application.internal.queryservices;

import com.practicalddd.cargotracker.bookingms.domain.model.aggregates.BookingId;
import com.practicalddd.cargotracker.bookingms.domain.model.aggregates.Cargo;
import com.practicalddd.cargotracker.bookingms.infrastructure.repositories.jpa.CargoRepository;

import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;
import javax.transaction.Transactional;
import java.util.List;

/∗∗
 ∗ Application Service

which caters to all queries related to the Booking Bounded Context
 ∗/
@ApplicationScoped
public class CargoBookingQueryService {

    @Inject
    private CargoRepository cargoRepository; // Inject Dependencies

    /∗∗
     ∗ Find all Cargos
     ∗ @return List<Cargo>
     ∗/
    @Transactional
    public List<Cargo> findAll(){
        return cargoRepository.findAll();
    }

    /∗∗
     ∗ List All Booking Identifiers
     ∗ @return List<BookingId>
     ∗/
   public List<BookingId> getAllBookingIds(){
       return cargoRepository.getAllBookingIds();
   }

    /∗∗
     ∗ Find a specific Cargo based on its Booking Id
     ∗ @param bookingId
     ∗ @return Cargo
     ∗/
    public Cargo find(String bookingId){
        return cargoRepository.find(new BookingId(bookingId));
    }
}

Listing 4-24.Cargo Booking Query Application services implementation

图 4-32 展示了实现的类图。

img/473795_1_En_4_Fig32_HTML.jpg

图 4-32

将应用服务实现为命令/查询代理的类图

我们所有的应用服务实现(命令/查询)都遵循相同的方法,如图 4-33 所示。

img/473795_1_En_4_Fig33_HTML.jpg

图 4-33

作为命令/查询委托人的应用服务实施流程摘要

  1. 对命令/查询操作的请求到达有界上下文的应用服务。此请求是从入站服务发送的。应用服务类 被实现为 CDI 托管 bean,它们使用 CDI 注入注释 注入它们所有的 依赖项,它们 有一个作用域 ,最后它们负责 创建一个事务上下文 来开始处理操作。

  2. 应用服务依赖命令/查询处理程序来设置/查询聚合状态。

  3. 作为操作处理的一部分,应用服务需要与外部存储库进行交互。它们依赖出站服务来执行这些交互。

应用服务:引发域事件

应用服务扮演的另一个角色是引发每当有界上下文处理命令时生成的域事件。

在前一章中,我们使用 CDI 2.0 事件模型实现了域事件。基于事件通知/观察者模型,事件总线充当事件生产者和消费者的协调者。在单片实现中,我们使用事件总线的纯内部实现,事件在同一个执行线程中产生和消费。

在微服务的世界里,这是行不通的。由于每个微服务都是单独部署的,因此需要一个 集中式消息代理 来协调跨多个受限上下文/微服务的事件生产者和消费者之间的事件,如图 4-34 所示。我们的实现将使用 RabbitMQ 作为集中式消息代理。

在 Eclipse MicroProfile 的情况下,由于我们使用 CDI 事件,这基本上转化为

  • 激发 CDI 事件并将其作为消息发布给 RabbitMQ 或

  • 观察 CDI 事件并将它们作为来自 RabbitMQ 的消息使用

img/473795_1_En_4_Fig34_HTML.jpg

图 4-34

域事件摘要

我们已经讨论了跟踪有界上下文的业务用例,它需要从预订有界上下文订阅货物路由事件,以便为预订的货物分配跟踪标识符。让我们通过这个业务用例的实现来演示应用服务的事件发布。

清单 4-25 展示了来自CargoBookingCommandService类的代码部分,该类处理为货物分配路线的命令,然后触发“ CargoRouted ”事件:

package com.practicalddd.cargotracker.bookingms.application.internal.commandservices;

import javax.enterprise.event.Event; // CDI Eventing

/∗∗
 ∗ Application Service class for the Cargo Booking Command
 ∗/
@ApplicationScoped
public class CargoBookingCommandService {

    @Inject
    private CargoRepository cargoRepository;

    @Inject
    @CargoRoutedEvent // Custom annotation for the Cargo Routed Event
    private Event<CargoRoutedEventData> cargoRouted; // Event that needs to be raised when the Cargo is Routed

     /∗∗
     ∗ Service Command method to assign a route to a Cargo
     ∗ @param routeCargoCommand
     ∗/
    @Transactional
    public void assignRouteToCargo(RouteCargoCommand routeCargoCommand){

        Cargo cargo = cargoRepository.find(new BookingId(routeCargoCommand.getCargoBookingId()));
        CargoItinerary cargoItinerary = externalCargoRoutingService.fetchRouteForSpecification(new RouteSpecification(
                new Location(routeCargoCommand.getOriginLocation()),
                new Location(routeCargoCommand.getDestinationLocation()),
                routeCargoCommand.getArrivalDeadline()
        )); 

        routeCargoCommand.setCargoItinerary(cargoItinerary);
        cargo.assignToRoute(routeCargoCommand);
        cargoRepository.store(cargo);

        cargoRouted.fire(new CargoRoutedEventData(routeCargoCommand.getCargoBookingId()));
    }
}

Listing 4-25.Firing the CargoRouted event from the Application services class

要激发 CDI 事件及其有效负载,我们需要实现三个步骤:

  • 用需要触发的事件注入应用服务。这是通过为作为域模型操作的一部分实现的事件使用定制注释来完成的( 事件 )。在本例中,我们将 CargoRoutedEvent 自定义注释注入到CargoBookingCommandService中。

  • 我们创建一个事件有效负载数据对象,它将是发布的事件的有效负载。在本例中,它是CargoRoutedEventData对象。

  • 我们使用 CDI 提供的“fire()方法来引发事件 并封装要随事件一起发送的有效载荷。

图 4-35 展示了引发域事件的应用服务实现的类图。

img/473795_1_En_4_Fig35_HTML.jpg

图 4-35

引发域事件的应用服务实现的类图

我们负责引发域事件的应用服务的所有实现都遵循相同的方法。如图 4-36 所示。

img/473795_1_En_4_Fig36_HTML.jpg

图 4-36

负责引发域事件的应用服务的进程概要

域事件的引发实现演示到此结束。引发域事件的实现仍然处于逻辑级别。我们仍然需要将这些事件发布到物理消息代理(在我们的例子中是 RabbitMQ)来完成我们的事件架构。我们使用 绑定类 来实现这一点,这是我们 出站服务代理实现 的一部分。

出站服务

出站服务提供了与有界上下文 外部的 服务进行交互的能力。 外部服务可以是存储有界上下文聚合状态的数据存储库 ,可以是发布聚合状态消息代理,也可以是与另一个有界上下文交互。

图 4-37 说明了出站服务的职责。作为操作(命令、查询、事件)的一部分,它们接收与外部服务通信的请求。它们使用基于外部服务类型的 API(持久性 API、REST APIs、代理 API)与它们进行交互。

img/473795_1_En_4_Fig37_HTML.jpg

图 4-37

出站服务

让我们看看这些出站服务类型的实现。

出站服务:存储库

数据库访问的出站服务被实现为 【储存库】类 。存储库类是围绕特定的聚合构建的,处理该聚合的所有数据库操作,包括以下内容:

  • 新聚集及其关联的持久性

  • 更新聚合及其关联

  • 查询聚合及其关联

让我们看一个仓库类的例子, 货物仓库类 ,它处理与 货物集合 相关的所有数据库操作:

  • Repository 类被实现为一个附加了作用域的 CDI Bean(在本例中为 @Application )。

  • 因为我们将使用 JPA 作为与数据库实例交互的机制,所以我们将为 Repository 类提供由 JPA 管理的实体管理器资源 。实体管理器资源通过提供封装层来实现与数据库的交互。使用“@ persistence context”注释注入实体管理器。持久性上下文注释依赖于 persistence.xml 文件,该文件包含到实际物理数据库的连接信息。我们已经看到了 persistence.xml 文件的实现,它是 Helidon MP 项目设置过程的一部分。

  • 存储库类使用实体管理器提供的方法(【persist()】)来保存/更新货物集合实例。

  • Repository 类使用实体管理器提供的方法创建 JPA 命名查询(【createNamedQueries())并运行它们以返回结果。

清单 4-26 显示了 Cargo Repository 类的实现:

package com.practicalddd.cargotracker.bookingms.infrastructure.repositories.jpa;

import com.practicalddd.cargotracker.bookingms.domain.model.aggregates.BookingId;
import com.practicalddd.cargotracker.bookingms.domain.model.aggregates.Cargo;

import javax.enterprise.context.ApplicationScoped;
import javax.persistence.EntityManager;
import javax.persistence.NoResultException;
import javax.persistence.PersistenceContext;

import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.logging.Level;
import java.util.logging.Logger;

/∗∗
 ∗ Repository class for the Cargo Aggregate. Deals with all repository operations

 ∗ related to the state of the Cargo
 ∗/
@ApplicationScoped
public class CargoRepository {

    private static final long serialVersionUID = 1L;

    private static final Logger logger = Logger.getLogger(
            CargoRepository.class.getName());

    @PersistenceContext(unitName = "bookingms")
    private EntityManager entityManager;

    /∗∗
     ∗ Returns the Cargo Aggregate based on the Booking Identifier of a Cargo
     ∗ @param bookingId
     ∗ @return
     ∗/
    public Cargo find(BookingId bookingId) {
        Cargo cargo;
        try {
            cargo = entityManager.createNamedQuery("Cargo.findByBookingId",
                    Cargo.class)
                    .setParameter("bookingId", bookingId)
                    .getSingleResult();
        } catch (NoResultException e) {
            logger.log(Level.FINE, "Find called on non-existant Booking ID.", e);
            cargo = null;
        }

        return cargo; 

    }

    /∗∗
     ∗ Stores the Cargo Aggregate
     ∗ @param cargo
     ∗/
    public void store(Cargo cargo) {
        entityManager.persist(cargo);
    }

    /∗∗
     ∗ Gets next Booking Identifier
     ∗ @return
     ∗/

    public BookingId nextBookingId() {
        String random = UUID.randomUUID().toString().toUpperCase();

        return new BookingId(random.substring(0, random.indexOf("-")));
    }

    /∗∗
     ∗ Find all Cargo Aggregates

     ∗ @return
     ∗/
    public List<Cargo> findAll() {
        return entityManager.createNamedQuery("Cargo.findAll", Cargo.class)
                .getResultList();
    }

    /∗∗
     ∗ Get all Booking Identifiers
     ∗ @return
     ∗/
    public List<BookingId> getAllBookingIds() {
        List<BookingId> bookingIds = new ArrayList<BookingId>();

        try {
            bookingIds = entityManager.createNamedQuery(
                    "Cargo.getAllTrackingIds", BookingId.class).getResultList();
        } catch (NoResultException e) {
            logger.log(Level.FINE, "Unable to get all tracking IDs", e);
        }

        return bookingIds; 

    }
}

Listing 4-26.Cargo repository class implementation

我们所有的存储库实现都遵循相同的方法,如图 4-38 所示。

img/473795_1_En_4_Fig38_HTML.jpg

图 4-38

存储库实施流程摘要

  1. 储存库接收改变/查询聚集状态的请求。

  2. 存储库使用实体管理器在集合上执行数据库操作(存储、查询)。

  3. 实体管理器执行操作并将结果返回给存储库类。

出站服务:REST API

使用 REST API 作为微服务之间的通信模式是一个很常见的需求。虽然我们已经看到事件编排是实现这一点的一种机制,但有时只需要有界上下文之间的同步调用。

让我们通过一个例子来说明这一点。作为货物预订流程的一部分,我们需要根据路线规范为货物分配一个行程。生成最佳路线所需的数据作为维护船只运动、路线和时间表的路线约束上下文的一部分来维护。这需要预订受限上下文的预订服务向路由受限上下文的路由服务发出一个出站调用,路由服务提供一个 REST API 来根据货物的路线规范检索所有可能的路线。

如图 4-39 所示。

img/473795_1_En_4_Fig39_HTML.jpg

图 4-39

两个有界上下文之间的 HTTP 调用

然而,这确实对领域模型提出了挑战。预订有界上下文的货物集合将路线表示为“ ”对象,而路由有界上下文将路线表示为“ ”对象。因此,两个有界上下文之间的调用将需要在它们的域模型之间进行转换。

这种转换通常在反讹误层中完成,反讹误层充当两个有界上下文之间通信的桥梁。

如图 4-40 所示。

img/473795_1_En_4_Fig40_HTML.jpg

图 4-40

两个有界上下文之间的反腐败层

预订限制上下文依赖于 Helidon MP 提供的微配置文件类型安全 Rest 客户端功能来调用路由服务的 REST API。

让我们通过完整的实现来更好地理解这个概念:

  • 第一步是实现路由服务 REST API。这是通过使用我们在前一章中已经实现的标准 JAX 遥感能力来完成的。清单 4-27 展示了路由服务 REST API 实现:
package com.practicalddd.cargotracker.routingms.interfaces.rest;
import com.practicalddd.cargotracker.TransitPath;
import com.practicalddd.cargotracker.routingms.application.internal.CargoRoutingService;
import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;
import javax.ws.rs.∗;
@Path("/cargoRouting")
@ApplicationScoped
public class CargoRoutingController {
    private CargoRoutingService cargoRoutingService; // Application Service Dependency

    /∗∗
     ∗ Provide the dependencies
     ∗ @param cargoRoutingService
     ∗/
    @Inject
    public CargoRoutingController(CargoRoutingService cargoRoutingService){
        this.cargoRoutingService = cargoRoutingService;
    }

    /∗∗
     ∗
     ∗ @param originUnLocode
     ∗ @param destinationUnLocode
     ∗ @param deadline
     ∗ @return TransitPath - The optimal route for a Route Specification

     ∗/
    @GET
    @Path("/optimalRoute")
    @Produces({"application/json"})
    public TransitPath findOptimalRoute(
             @QueryParam("origin") String originUnLocode,
             @QueryParam("destination") String destinationUnLocode,
             @QueryParam("deadline") String deadline) {
        TransitPath transitPath = cargoRoutingService.findOptimalRoute(originUnLocode,destinationUnLocode,deadline);
        return transitPath;
    }
}

Listing 4-27.Cargo Routing controller implementation

路由服务实现在“/ optimalRoute ”提供了一个 REST API。它接受一组规范——起始位置、目的位置和截止时间。然后,它使用货物路由应用服务类根据这些规范计算最佳路线。路由有界上下文中的域模型根据 运输路径(类似于路线)运输边(类似于路线) 来表示最优路由。

清单 4-28 展示了传输路径域模型类的实现:

import java.util.ArrayList;
import java.util.List;
/∗∗
 ∗ Domain Model representation of the Transit Path
 ∗/
public class TransitPath {
    private List<TransitEdge> transitEdges;
    public TransitPath() {
        this.transitEdges = new ArrayList<>();
    }
    public TransitPath(List<TransitEdge> transitEdges) {
        this.transitEdges = transitEdges;
    }
    public List<TransitEdge> getTransitEdges() {
        return transitEdges;
    }
    public void setTransitEdges(List<TransitEdge> transitEdges) {
        this.transitEdges = transitEdges;
    }
    @Override
    public String toString() {
        return "TransitPath{" + "transitEdges=" + transitEdges + '}';
    }
}

Listing 4-28.Transit path model implementation

清单 4-29 演示了 Transit Edge 域模型类的实现:

package com.practicalddd.cargotracker;

import java.io.Serializable;
import java.util.Date;

/∗∗
 ∗ Represents an edge in a path through a graph, describing the route of a
 ∗ cargo.
 ∗/
public class TransitEdge implements Serializable {

    private String voyageNumber;
    private String fromUnLocode;
    private String toUnLocode;
    private Date fromDate;
    private Date toDate;

    public TransitEdge() {    }

    public TransitEdge(String voyageNumber, String fromUnLocode,
            String toUnLocode, Date fromDate, Date toDate) {
        this.voyageNumber = voyageNumber;
        this.fromUnLocode = fromUnLocode;
        this.toUnLocode = toUnLocode;
        this.fromDate = fromDate;
        this.toDate = toDate;
    }

    public String getVoyageNumber() {
        return voyageNumber;
    }

    public void setVoyageNumber(String voyageNumber) {
        this.voyageNumber = voyageNumber;
    }

    public String getFromUnLocode() {
        return fromUnLocode;
    }

    public void setFromUnLocode(String fromUnLocode) {
        this.fromUnLocode = fromUnLocode;
    }

    public String getToUnLocode() {
        return toUnLocode;
    }

    public void setToUnLocode(String toUnLocode) {
        this.toUnLocode = toUnLocode;
    }

    public Date getFromDate() {
        return fromDate;
    }

    public void setFromDate(Date fromDate) {
        this.fromDate = fromDate;
    }

    public Date getToDate() {
        return toDate;
    }

    public void setToDate(Date toDate) {
        this.toDate = toDate;
    }

    @Override
    public String toString() {
        return "TransitEdge{" + "voyageNumber=" + voyageNumber
                + ", fromUnLocode=" + fromUnLocode + ", toUnLocode="
                + toUnLocode + ", fromDate=" + fromDate
                + ", toDate=" + toDate + '}';
    }
}

Listing 4-29.Transit Edge domain model implementation

图 4-41 展示了实现的类图。

img/473795_1_En_4_Fig41_HTML.jpg

图 4-41

出站服务的类图

下一步是为我们的 Routing Rest 服务实现客户端实现。客户端是CargoBookingCommandService类,负责处理“ 给货物分配路线 ”命令。作为命令处理的一部分,这个服务类将需要调用路由服务 REST API 来获得基于货物路线规范的最佳路线。

CargoBookingCommandService 使用一个出站服务类–ExternalCargoRoutingService–来调用路由服务 REST API。ExternalCargoRoutingService类还将路由服务的 REST API 提供的数据转换成预订绑定上下文的域模型可识别的格式。

清单 4-30 演示了CargoBookingCommandService 中的方法“assignRouteToCargo”。 该服务类被注入了ExternalCargoRoutingService依赖项,该依赖项处理调用路由服务的 REST API 的请求,并返回cargo interary对象,该对象随后被分配给 cargo:

@ApplicationScoped
public class CargoBookingCommandService {

    @Inject
    private ExternalCargoRoutingService externalCargoRoutingService;

    /∗∗
     ∗ Service Command method to assign a route to a Cargo
     ∗ @param routeCargoCommand

     ∗/
    @Transactional
    public void assignRouteToCargo(RouteCargoCommand routeCargoCommand){

        Cargo cargo = cargoRepository.find(new BookingId(routeCargoCommand.getCargoBookingId()));
        CargoItinerary cargoItinerary = externalCargoRoutingService.fetchRouteForSpecification(new RouteSpecification(
                new Location(routeCargoCommand.getOriginLocation()),
                new Location(routeCargoCommand.getDestinationLocation()),
                routeCargoCommand.getArrivalDeadline()
        ));

        cargo.assignToRoute(cargoItinerary);
        cargoRepository.store(cargo);

    }

    // All other implementations

of Commands for the Booking Bounded Context

}

Listing 4-30.Outbound service class dependency

清单 4-31 演示了 ExternalCargoRoutingService 出站服务类。这个类执行两件事:

  • 它使用 MicroProfile 提供的类型安全 Rest 客户端注释( @RestClient )注入一个 Rest 客户端“ExternalCargoRoutingClient”。这个客户端使用 MicroProfile 提供的REST client builderAPI 调用路由服务的 REST API。

  • 它还将路由服务的 Rest API 提供的数据(transi pathtransi edge)转换为预订有界上下文的域模型(cargo internary/Leg)。

package com.practicalddd.cargotracker.bookingms.application.internal.outboundservices.acl;

import com.practicalddd.cargotracker.TransitEdge;
import com.practicalddd.cargotracker.TransitPath;
import com.practicalddd.cargotracker.bookingms.domain.model.valueobjects.CargoItinerary;
import com.practicalddd.cargotracker.bookingms.domain.model.valueobjects.Leg;
import com.practicalddd.cargotracker.bookingms.domain.model.valueobjects.RouteSpecification;
import com.practicalddd.cargotracker.bookingms.infrastructure.services.http.ExternalCargoRoutingClient;
import org.eclipse.microprofile.rest.client.RestClientBuilder;
import org.eclipse.microprofile.rest.client.inject.RestClient;

import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;
import java.util.ArrayList;
import java.util.List; 

/∗∗
 ∗ Anti Corruption Service Class
 ∗/
@ApplicationScoped
public class ExternalCargoRoutingService {

    @Inject
    @RestClient // MicroProfile Type safe Rest Client API
    private ExternalCargoRoutingClient externalCargoRoutingClient;

    /∗∗
     ∗ The Booking Bounded Context makes an external call to the Routing Service of the Routing Bounded Context to
     ∗ fetch the Optimal Itinerary for a Cargo based on the Route Specification
     ∗ @param routeSpecification

     ∗ @return
     ∗/
    public CargoItinerary fetchRouteForSpecification(RouteSpecification routeSpecification){
        ExternalCargoRoutingClient cargoRoutingClient =
                RestClientBuilder
                .newBuilder().build(ExternalCargoRoutingClient.class); // MicroProfile Type safe Rest Client API

        TransitPath transitPath = cargoRoutingClient.findOptimalRoute(
                routeSpecification.getOrigin().getUnLocCode(),
                routeSpecification.getDestination().getUnLocCode(),
                routeSpecification.getArrivalDeadline().toString()
                ); // Invoke the Routing Service’s API using the client

        List<Leg> legs = new ArrayList<Leg>(transitPath.getTransitEdges().size());
        for (TransitEdge edge : transitPath.getTransitEdges()) {
            legs.add(toLeg(edge));
        }

        return new CargoItinerary(legs);

    }

    /∗∗
     ∗ Anti-corruption layer conversion method from the routing service's domain model (TransitEdges)
     ∗ to the domain model recognized by the Booking Bounded Context (Legs)
     ∗ @param edge
     ∗ @return

     ∗/
    private Leg toLeg(TransitEdge edge) {
        return new Leg(
                edge.getVoyageNumber(),
                edge.getFromUnLocode(),
                edge.getToUnLocode(),
                edge.getFromDate(),
                edge.getToDate());
        }
}

Listing 4-31.Outbound service class implementation

清单 4-32 演示了ExternalCargoRoutingClient类型的安全休息客户端实现。它被实现为一个 接口 ,并利用***@ register Rest client注释将其标记为 Rest 客户端。方法签名/方法资源细节应该与其调用的 API 的服务完全相同(在本例中是路由服务的optimalRoute***API):

package com.practicalddd.cargotracker.bookingms.infrastructure.services.http;

import javax.ws.rs.∗;

import com.practicalddd.cargotracker.TransitPath;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;

/∗∗
 ∗ Type safe Rest client for the Routing Service API
 ∗/
@Path("cargoRouting")
@RegisterRestClient //Annotation to register this as a Rest client
public interface ExternalCargoRoutingClient {
    // The method signature / method resource details should be exactly as the calling service
    @GET
    @Path("/optimalRoute")
    @Produces({"application/json"})
    public TransitPath findOptimalRoute(
            @QueryParam("origin") String originUnLocode,
            @QueryParam("destination") String destinationUnLocode,
            @QueryParam("deadline") String deadline);
}

Listing 4-32.ExternalCargoRoutingClient typesafe implementation

图 4-42 展示了实现的类图。

img/473795_1_En_4_Fig42_HTML.jpg

图 4-42

出站服务(HTTP)实现流程类图

我们所有需要与其他有界上下文通信的出站服务实现都遵循相同的方法,如图 4-43 所示。

img/473795_1_En_4_Fig43_HTML.jpg

图 4-43

出站服务(HTTP)实施流程

  1. 应用服务类接收命令/查询/事件。

  2. 作为处理的一部分,如果它需要使用 REST 与另一个有界上下文的 API 进行交互,它会使用出站服务。

  3. 出站服务使用类型安全 Rest 客户端来调用有界上下文的 API。它还执行从该有界上下文的 API 提供的数据格式到当前有界上下文识别的数据模型的转换。

出站服务:消息代理

需要实现的最后一种出站服务是与消息代理的交互。消息代理为发布/订阅域事件提供了必要的物理基础设施。

我们已经看到了几个使用定制注释实现的事件类 (CargoBooked,CargoRouted) 。我们还看到了发布它们 (使用 fire()方法) 以及订阅它们 (使用 observes()方法) 的实现。

让我们看一下如何从 RabbitMQ 服务器的队列/交换中发布和订阅这些事件的实现。

请注意,Eclipse MicroProfile 平台和 Helidon MP 的扩展都没有提供帮助将 CDI 事件发布到 RabbitMQ 上的功能,因此我们需要为此提供自己的实现。该章的源代码提供了一个单独的项目(cargo-tracker-rabbi MQ-adaptor)。该项目提供以下内容:

  • 基础设施功能(RabbitMQ 服务、托管发布者和托管消费者的连接工厂)

  • 向 RabbitMQ 交易所发布 CDI 事件的 AMQP 消息的能力

  • 为 RabbitMQ 队列中的事件使用 AMQP 消息的能力

我们将不会进入这个项目的详细实施。我们将使用这个项目提供的 API 来帮助我们实现 CDI 事件与 RabbitMQ 交换/队列集成的用例。要使用这个项目,我们需要向每个 MicroProfile 项目的 pom.xml 依赖项文件添加以下依赖项:

<dependency>
    <groupId>com.practicalddd.cargotracker</groupId>
    <artifactId>cargo-tracker-rabbimq-adaptor</artifactId>
    <version>1.0.FINAL</version>
</dependency>

实现连通性的第一步是创建一个“***”***绑定器类。Binder 类有以下用途:

  • 将 CDI 事件绑定到交换和路由关键字

  • 将 CDI 事件绑定到队列

清单 4-33 演示了“ RoutedEventBinder ,它负责将“cargo routed”CDI 事件绑定到相应的 RabbitMQ 交换。它扩展了 adaptor 项目提供的“ EventBinder ”类。我们需要覆盖" bindEvents() "方法,在这里我们执行所有的绑定,用于将 CDI 事件映射到交换/队列 。还要注意,我们在 CDI 提供的 构建后生命周期方法 中执行绑定初始化:

package com.practicalddd.cargotracker.bookingms.infrastructure.brokers.rabbitmq;
import javax.annotation.PostConstruct;
import javax.enterprise.context.ApplicationScoped;
import com.practicalddd.cargotracker.rabbitmqadaptor.EventBinder; //Adaptor Class
/∗∗
 ∗ Component which initializes the Cargo Routed Events <-> Rabbit MQ bindings
 ∗/
@ApplicationScoped
public class RoutedEventBinder extends EventBinder {
    /∗∗
     ∗ Method to bind the Cargo Routed Event class to the corresponding exchange in Rabbit MQ with
     ∗ the corresponding Routing Key
     ∗/
    @PostConstruct // CDI Annotation to initialize this in the post construct lifecycle method of this bean
    public void bindEvents(){
        bind(CargoRoutedEvent.class)
                .toExchange("routed.exchange")
                .withPublisherConfirms()
                .withRoutingKey("routed.key");
    }
}

Listing 4-33.RoutedEventBinder implementation class

因此,每次触发 Cargo Routed 事件时,它都会作为一个 AMQP 消息传递给具有指定路由键的相应交换。

同样的机制也适用于事件订阅。我们“ ”将”CDI 事件绑定到对应的 RabbitMQ 队列中,每次观察到一个 CDI 事件,它都会作为 AMQP 消息从对应的队列中传递出来。

本章的源代码有所有有界上下文的事件初始化器的完整实现(见包 com . practical DD . cargo tracker .<<bounded_context_name>> . infra structure . brokers . rabbit MQ)。</bounded_context_name>

图 4-44 展示了实现的类图。

img/473795_1_En_4_Fig44_HTML.jpg

图 4-44

事件绑定器实现

这完成了我们在 DDD 的货物跟踪系统的实现,它是一个微服务应用,使用 Eclipse MicroProfile 平台,由 Helidon MP 提供实现。

实施摘要

我们现在有了货物跟踪应用的完整的 DDD 实现,以及使用 Eclipse MicroProfile 中可用的相应规范实现的各种 DDD 工件。

实施总结如图 4-45 所示。

img/473795_1_En_4_Fig45_HTML.jpg

图 4-45

使用 Eclipse MicroProfile 的 DDD 工件实现概要

摘要

总结我们的章节

  • 我们从建立 Eclipse MicroProfile 平台及其提供的各种功能的细节开始。

  • 我们决定使用 Helidon MP 的 MicroProfile 平台的实现来帮助构建作为微服务应用的货物跟踪器。

  • 我们深入研究了各种 DDD 工件的开发——首先是域模型,然后是使用 Eclipse MicroProfile 和 Helidon MP 上可用的技术的域模型服务。**************************