项目

本文档有多个版本。请选择最适合您的选项。

UI
Database

微服务教程 第 06 部分:集成服务:HTTP API 调用

在上一部分中,我们实现了订购微服务的功能。然而,在列出订单时,我们需要显示产品名称而非产品 ID。为此,我们必须调用目录服务来获取每个订单项的产品名称。

在本节中,我们将通过 HTTP API 调用来集成订购服务与目录服务。

集成服务的必要性

在微服务架构中,每个服务负责其自身的数据和业务逻辑。然而,服务之间经常需要相互通信以完成其职责。这种通信可以是同步的,也可以是异步的,具体取决于需求。

web-orders-page

在我们的案例中,订购服务需要显示产品名称而非产品 ID。为此,我们需要调用目录服务,根据产品 ID 获取产品详细信息。这是微服务之间典型的同步通信模式。作为该问题的解决方案,我们将使用一个集成服务,它将负责与目录服务的通信。ABP 中的集成服务概念专为模块化应用中的模块间通信以及分布式系统中的微服务间通信(请求/响应风格)而设计。

创建产品集成服务

首先,我们需要创建一个服务来处理与目录服务的通信。该服务将负责根据产品 ID 获取产品详细信息。

定义 IProductIntegrationService 接口

在 IDE 中打开 CloudCrm.CatalogService .NET 解决方案。找到 CloudCrm.CatalogService.Contracts 项目,创建一个名为 IntegrationServices 的新文件夹。在该文件夹内,添加一个名为 IProductIntegrationService 的新接口,代码如下:

using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using CloudCrm.CatalogService.Products;
using Volo.Abp;
using Volo.Abp.Application.Services;

namespace CloudCrm.CatalogService.IntegrationServices;

[IntegrationService]
public interface IProductIntegrationService : IApplicationService
{
    Task<List<ProductDto>> GetProductsByIdsAsync(List<Guid> ids);
}

IProductIntegrationService 与典型的应用服务非常相似。唯一的区别是它标记了 [IntegrationService] 属性。该属性用于标识服务为集成服务,允许 ABP 处理服务间的通信。ABP 对它们的行为会有所不同(例如,如果你已配置自动 API 控制器功能,ABP 默认不会将集成服务暴露为 HTTP API)。

IProductIntegrationService 包含一个名为 GetProductsByIdsAsync 的方法。该方法接收一个产品 ID 列表,并返回一个 ProductDto 对象列表。这正是我们在订购服务中所需要的。

设计提示

你可能会想,我们是否可以使用现有的应用服务(如 IProductAppService)而非创建特定的集成服务。从技术上讲,你可以使用,ABP 没有限制。然而,从良好设计和最佳实践的角度,我们不建议这样做。因为应用服务是专门为表示层消费而设计的。它们将具有不同的授权和验证逻辑,需要不同的 DTO 输入和输出属性,具有不同的性能、优化和缓存需求等等。最重要的是,所有这些都会随着时间基于 UI 需求而变化,而这些变化可能会在以后破坏你的集成。最好实现专门为该目的设计和优化的特定集成 API。

我们复用了为 IProductAppService 创建的 ProductDto 对象,从维护的角度来看可能是合理的。但如果你认为未来集成服务的结果与应用服务的结果会有所不同,那么从一开始就将它们分开可能是好的,这样你以后就不需要引入破坏性变更。

实现 ProductIntegrationService

现在,让我们实现 IProductIntegrationService 接口。在 CloudCrm.CatalogService 项目中创建一个名为 IntegrationServices 的新文件夹。在该文件夹内,添加一个名为 ProductIntegrationService 的新类,代码如下:

using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using CloudCrm.CatalogService.Localization;
using CloudCrm.CatalogService.Products;
using Volo.Abp;
using Volo.Abp.Application.Services;
using Volo.Abp.Domain.Repositories;

namespace CloudCrm.CatalogService.IntegrationServices;

[IntegrationService]
public class ProductIntegrationService : ApplicationService, IProductIntegrationService
{
    private readonly IRepository<Product, Guid> _productRepository;

    public ProductIntegrationService(IRepository<Product, Guid> productRepository)
    {
        LocalizationResource = typeof(CatalogServiceResource);
        _productRepository = productRepository;
    }

    public async Task<List<ProductDto>> GetProductsByIdsAsync(List<Guid> ids)
    {
        var products = await _productRepository.GetListAsync(
            product => ids.Contains(product.Id)
        );

        return ObjectMapper.Map<List<Product>, List<ProductDto>>(products);
    }
}

ProductIntegrationService 是一个典型的应用服务类,实现了 IProductIntegrationService 接口。它有一个构造函数,接收一个 IRepository<Product, Guid> 对象。该存储库用于从数据库获取产品详细信息。

这里,我们直接使用了 List<T> 类,但你可以将输入和输出包装到 DTOs 中。这样,就可以在不改变集成服务方法签名(且不会为客户端应用引入破坏性变更)的情况下,向这些 DTO 添加新属性。

消费产品集成服务

现在我们已经创建了 IProductIntegrationService 接口和 ProductIntegrationService 类,我们可以从订购服务消费此服务。

添加对 CloudCrm.OrderingService 包的引用

首先,我们需要在订购服务中添加对 CloudCrm.OrderingService 包的引用。打开 ABP Studio,如果应用程序正在运行则停止。然后,打开解决方案资源管理器,右键单击 CloudCrm.OrderingService 包。选择添加 -> 包引用命令:

add-package-reference-ordering-service

添加包引用窗口中,从此解决方案选项卡选择 CloudCrm.CatalogService.Contracts 包。单击确定按钮添加引用:

add-catalog-service-contracts-reference

ABP Studio 添加包引用并安排模块依赖。

与其直接添加此类包引用,最好先导入模块(右键单击 CloudCrm.OrderingService,选择_导入模块_命令并导入 CloudCrm.CatalogService 模块),然后安装包引用。这样,将更容易查看和跟踪模块间的依赖关系。

使用产品集成服务

现在,我们可以在 OrderAppService 类中使用 IProductIntegrationService 接口来获取产品详细信息。

打开 OrderAppService 类(位于 CloudCrm.OrderingService .NET 解决方案的 CloudCrm.OrderingService 项目的 Services 文件夹下的 OrderAppService.cs 文件),并按如下代码块更改其内容:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using CloudCrm.CatalogService.IntegrationServices;
using CloudCrm.OrderingService.Entities;
using CloudCrm.OrderingService.Enums;
using CloudCrm.OrderingService.Localization;
using Volo.Abp.Application.Services;
using Volo.Abp.Domain.Repositories;

namespace CloudCrm.OrderingService.Services;

public class OrderAppService : ApplicationService, IOrderAppService
{
    private readonly IRepository<Order, Guid>  _orderRepository;
    private readonly IProductIntegrationService _productIntegrationService;

    public OrderAppService(
        IRepository<Order, Guid> orderRepository,
        IProductIntegrationService productIntegrationService)
    {
        LocalizationResource = typeof(OrderingServiceResource);

        _orderRepository = orderRepository;
        _productIntegrationService = productIntegrationService;
    }

    public async Task<List<OrderDto>> GetListAsync()
    {
        var orders = await _orderRepository.GetListAsync();

        // 准备我们需要的产品列表
        var productIds = orders.Select(o => o.ProductId).Distinct().ToList();
        var products = (await _productIntegrationService
                .GetProductsByIdsAsync(productIds))
            .ToDictionary(p => p.Id, p => p.Name);

        var orderDtos = ObjectMapper.Map<List<Order>, List<OrderDto>>(orders);

        orderDtos.ForEach(orderDto =>
        {
            orderDto.ProductName = products[orderDto.ProductId];
        });

        return orderDtos;
    }

    public async Task CreateAsync(OrderCreationDto input)
    {
        var order = new Order
        {
            CustomerName = input.CustomerName,
            ProductId = input.ProductId,
            State = OrderState.Placed
        };

        await _orderRepository.InsertAsync(order);
    }
}

现在打开 OrderDto 类(位于 CloudCrm.OrderingService .NET 解决方案的 CloudCrm.OrderingService.Contracts 项目的 Services 文件夹下的 OrderDto.cs 文件),并添加一个名为 ProductName 的新属性:

using System;
using CloudCrm.OrderingService.Enums;

namespace CloudCrm.OrderingService.Services;

public class OrderDto
{
    public Guid Id { get; set; }
    public string CustomerName { get; set; }
    public Guid ProductId { get; set; }
    public string ProductName { get; set; } // 新属性
    public OrderState State { get; set; }
}

最后,打开 OrderingServiceApplicationMappers 类(位于 CloudCrm.OrderingService .NET 解决方案的 CloudCrm.OrderingService 项目的 ObjectMapping 文件夹下的 OrderingServiceApplicationMappers.cs 文件),并在映射配置中忽略 ProductName 属性:

using Riok.Mapperly.Abstractions;
using Volo.Abp.Mapperly;

namespace CloudCrm.OrderingService.ObjectMapping;

[Mapper]
public partial class OrderingServiceApplicationMappers : MapperBase<Order, OrderDto>
{
    [MapperIgnoreTarget(nameof(OrderDto.ProductName))]
    public override partial OrderDto Map(Order source);

    [MapperIgnoreTarget(nameof(OrderDto.ProductName))]
    public override partial void Map(Order source, OrderDto destination);
}

让我们解释一下所做的更改:

  • 我们在 OrderDto 类中添加了一个名为 ProductName 的新属性。该属性将保存产品名称。
  • 我们修改了 OrderAppService 类的 GetListAsync 方法,以使用 IProductIntegrationService 接口获取产品详细信息。我们首先从订单中获取产品 ID,然后调用 IProductIntegrationService 接口的 GetProductsByIdsAsync 方法来获取产品详细信息。最后,我们将产品名称映射到 OrderDto 对象。

为集成服务生成代理类

我们已在 CloudCrm.CatalogService 解决方案中创建了 IProductIntegrationService 接口和 ProductIntegrationService 类。现在,我们需要在 CloudCrm.OrderingService 包中为集成服务生成代理类。首先,在 ABP Studio 解决方案运行器启动 CloudCrm.CatalogService 应用程序。然后,打开解决方案资源管理器,右键单击 CloudCrm.OrderingService 包。选择 ABP CLI -> 生成代理 -> C# 命令:

generate-proxy-catalog-service

这将打开生成 C# 代理窗口。从应用程序下拉列表中选择 CloudCrm.CatalogService 应用程序。然后,从模块下拉列表中选择 catalog 模块,并从服务类型下拉列表中选择 integration 服务。勾选无合约复选框,并单击生成按钮:

generate-catalog-service-proxy

IProductIntegrationService 接口的代理类已生成。

最后,打开 CloudCrmOrderingServiceModule 类(位于 CloudCrm.OrderingService .NET 解决方案的 CloudCrm.OrderingService 项目下的 CloudCrmOrderingServiceModule.cs 文件),并在 ConfigureServices 方法中添加以下代码:

public override void ConfigureServices(ServiceConfigurationContext context)
{
    // 其他配置...
    context.Services.AddStaticHttpClientProxies(
        typeof(CloudCrmCatalogServiceContractsModule).Assembly,
        "CatalogService");
}

更新 UI 以显示产品名称

打开 order.component.html 文件(位于 angular\projects\ordering-service\src\lib\order 下的 order.component.html 文件),并更新表格内容以显示产品名称而非产品 ID:

<div class="card">
    <div class="card-body">
        <table class="table table-bordered">
            <thead>
                <tr>
                    <th>Order ID</th>
                    <th>Product Name</th>
                    <th>Customer Name</th>
                </tr>
                <tr *ngFor="let item of items">
                    <td>{{item.id}}</td>
                    <td>{{item.productName}}</td>
                    <td>{{item.customerName}}</td>
                </tr>
            </thead>
        </table>
    </div>
</div>

完成了!现在,你可以在 ABP Studio 中启动所有应用程序并在浏览器中查看结果:

web-orders-page-with-product-name

现在,订购服务显示产品名称而非产品 ID。我们已经成功地通过 HTTP API 调用将订购服务与目录服务集成在一起。

设计提示

建议你尽量减少此类通信,避免服务之间相互耦合。这可能会使你的解决方案变得复杂,并可能降低系统性能。当你需要这样做时,请考虑性能并尝试进行一些优化。例如,如果订购服务频繁需要产品数据,你可以使用某种缓存层,这样就不必频繁向目录服务发出请求。

更新 Kubernetes 配置

ABP 微服务启动模板提供了预配置的 Helm 图表,用于将你的解决方案部署到 Kubernetes。在开发解决方案时,如果你希望保持这些图表是最新的并且正常工作,也应该关注这些图表的配置。

在上面的为集成服务生成代理类部分中,我们向订购微服务的 appsettings.json 文件添加了一个新配置。我们应该配置相应的 Helm 图表配置以保持同步。

在文本编辑器中打开 etc\helm\cloudcrm\charts\ordering\templates\ordering.yaml 文件,并在 env 部分下添加以下行,就像其他现有值一样(注意缩进,因为这在 YAML 文件中至关重要):

- name: "RemoteServices__CatalogService__BaseUrl"
  value: "http://{{ .Release.Name }}-catalog"

通过这个简单的配置,现在订购模块可以在你的 Kubernetes 集群内发现目录微服务的 URL。


在本文档中