Refit是一个用于.NET平台的REST库,它可以将REST API转换为实时类型安全的接口。通过Refit,我们可以轻松实现微服务之间的跨服务调用,使服务间通信变得更加简单和类型安全。本文将介绍如何在我们的项目中使用Refit来实现微服务间的通信。
一、什么是Refit
Refit是一个强大的REST客户端库,它能够将普通的HTTP API接口转换为类型安全的.NET接口。通过声明式的接口定义,Refit可以自动处理HTTP请求的序列化和反序列化,使得微服务之间的通信变得更加简单和可靠。它的工作原理是在运行时动态生成实现类,将接口方法转换为对应的HTTP请求。
在使用Refit时,我们只需要定义一个包含所需API调用的接口,并使用特性(Attributes)来描述HTTP请求的细节,如URL、请求方法、请求头等。Refit会自动处理底层的HTTP通信细节,包括请求的构建、发送以及响应的处理,让开发者可以专注于业务逻辑的实现,而不必关心底层的通信实现细节。
相比传统的HttpClient方式,Refit提供了更优雅的API调用方式,并且具有强类型检查的优势,可以在编译时发现潜在的问题。此外,Refit还支持多种高级特性,如请求重试、超时控制、自定义序列化等,使其成为.NET微服务架构中不可或缺的工具之一。
1.1 Refit的主要特性
Refit作为一个强大的REST客户端库,提供了丰富的特性支持。它通过简单的C#接口声明方式来定义API调用,支持包括GET、POST、PUT、DELETE在内的各种HTTP方法,并可以通过特性灵活配置请求参数和请求头。在类型安全方面,Refit提供了编译时类型检查机制,使用强类型的请求和响应模型,有效避免了运行时可能出现的类型错误。对于数据处理,Refit支持JSON、XML等多种格式的自动序列化和反序列化,可以自定义序列化器,并能自动处理复杂对象的转换。在配置选项方面,Refit支持请求重试策略、超时时间设置、自定义消息处理器,以及集成认证和授权功能。性能方面,Refit通过高效的HTTP请求处理、请求缓存支持和连接池管理来确保最佳性能。
1.2 使用场景
Refit在多个场景中展现出其强大的实用价值。在微服务架构中,它能够有效处理服务发现和注册、负载均衡以及断路器模式等微服务间的通信需求。对于外部API集成,Refit可以优雅地封装第三方服务调用、RESTful API,并作为高效的WebAPI客户端。在分布式系统中,Refit支持服务编排、数据聚合,并能够处理跨服务事务,使得分布式系统的开发和维护变得更加简单和可靠。
二、SP.FinanceService服务使用Refit
我们将以SP.FinanceService
服务为例,介绍如何使用Refit实现跨服务调用,其中包括了Refit接口定义、调用Refit接口、Refit接口与目标微服务的绑定。
2.1 Refit 接口
在这个服务中,修改和新增记账记录的时候需要调用SP.ConfigService
服务的/api/configs/by-type
接口获取用户配置的默认币种。先来看一下这个接口,代码如下:
csharp
///<summary>
/// 根据配置类型获取配置
///</summary>
[HttpGet("by-type/{type}")]
public ActionResult<ConfigResponse> QueryByType([FromRoute] ConfigTypeEnum type)
{
// more code ...
}
这个接口接受一个路由参数type
,这个参数用来区分获取的配置类型,并返回ConfigResponse
类型的参数。type
参数是一个枚举类型ConfigTypeEnum
,用于标识不同的配置类型,比如默认币种、默认账本等。接口通过[FromRoute]
特性将参数绑定到路由中,这意味着参数值将从URL路径中获取,而不是查询字符串或请求体。返回的ConfigResponse
对象包含了配置的具体信息。
我们已经了解了这个接口,下一步我们就要使用Refit在SP.FinanceService
服务中调用它。首先,新建RefitClient
文件夹,并在里面新建Refit客户端接口IConfigServiceApi
,我们不需要实现它,这是因为Refit已经为我们提供了默认的实现,这个默认实现已经满足了大部分的情况。代码如下:
csharp
using Refit;
using SP.Common.Model.Enumeration;
using SP.FinanceService.Models.Response;
namespace SP.FinanceService.RefitClient;
/// <summary>
/// 配置服务接口
/// </summary>
public interface IConfigServiceApi
{
/// <summary>
/// 根据类型获取配置
/// </summary>
/// <param name="type"></param>
/// <returns></returns>
[Get("/api/configs/by-type/{type}")]
Task<ApiResponse<ConfigResponse>> QueryByType(ConfigTypeEnum type);
}
接口上的[Get("/api/configs/by-type/{type}")]
特性是Refit提供的HTTP方法特性之一,它声明了这个接口方法将通过HTTP GET请求来调用远程服务。特性中的路径"/api/configs/by-type/{type}"定义了实际的API端点,其中{type}是一个路由参数占位符,会在运行时被实际的参数值替换。当我们调用这个接口方法时,Refit会首先通过服务发现机制获取目标服务的可用实例地址。然后,它会将获取到的服务基地址与接口路径组合,构建完整的请求URL。Refit会自动处理参数的序列化,将方法参数正确地映射到HTTP请求中,最后发送HTTP请求到目标服务。
Tip:在使用Refit定义接口时,一定要将接口定义为异步的,这是因为Refit调用数据跨服务调用,异步操作允许系统同时处理多个请求,而不是串行处理。其次,也会避免同步调用可能在高并发情况下出现的死锁问题。再者,在等待网络响应时,线程可以执行其他任务,提高CPU和内存的利用效率。
2.2 调用Refit接口
在编写完成Refit接口,我们就要开始调用它了,调用的方式和调用本地方法一样,先来看一下代码:
csharp
// more code ..
/// <summary>
/// 记账服务实现类
/// </summary>
public class AccountingServerImpl : IAccountingServer
{
// more code ...
///<summary>
/// 用户配置接口
///</summary>
private readonly IConfigServiceApi _configService;
/// <summary>
/// 记账服务构造函数
/// </summary>
/// <param name="dbContext"></param>
/// <param name="autoMapper"></param>
/// <param name="configService"></param>
/// <param name="accountBookServer"></param>
/// <param name="rabbitMqMessage"></param>
/// <param name="currencyServer"></param>
public AccountingServerImpl(FinanceServiceDbContext dbContext, IMapper autoMapper,
IConfigServiceApi configService, IAccountBookServer accountBookServer, RabbitMqMessage rabbitMqMessage,
ICurrencyService currencyServer)
{
_dbContext = dbContext;
_autoMapper = autoMapper;
_accountBookServer = accountBookServer;
_rabbitMqMessage = rabbitMqMessage;
_currencyServer = currencyServer;
_configService = configService;
}
// more code ...
/// <summary>
/// 从用户配置中获取用户设置的目标币种
/// </summary>
/// <returns>返回目标币种ID</returns>
private long GetUserTargetCurrencyId()
{
ApiResponse<ConfigResponse> apiResponse = _configService.QueryByType(ConfigTypeEnum.Currency).Result;
// 检查响应是否成功,并且内容不为空
if (apiResponse.IsSuccessStatusCode && apiResponse.Content != null)
{
return long.Parse(apiResponse.Content.Value ?? string.Empty);
}
throw new RefitException($"获取汇率失败: {apiResponse.StatusCode}");
}
}
在上面的代码中,我们实现了对SP.ConfigService
服务的调用,就像调用本地方法一样简单直观。具体来说,我们在AccountingServerImpl
类的构造函数中注入了IConfigServiceApi
接口,这个接口是我们之前定义的Refit客户端接口。在GetUserTargetCurrencyId
方法中,我们通过_configService.QueryByType(ConfigTypeEnum.Currency)
调用远程服务的API,获取用户配置的默认币种。这个调用过程中,Refit会自动处理HTTP请求的构建和发送,并将返回的结果封装在ApiResponse<ConfigResponse>
对象中。通过检查响应的状态码和内容,我们可以确保调用成功并获取到所需的配置值。如果调用失败或返回内容为空,则抛出RefitException
异常。
我们看到QueryByType
方法封装了ConfigResponse
,这个类型定义在了当前服务中。我们为什么要在当前服务中重复定义ConfigResponse
,而不是将配置服务的ConfigResponse
提取为公共类型呢?这是因为服务的调用方不一定需要全部的属性,而且这样做可以降低服务之间的耦合度。在微服务架构中,每个服务应该是独立的,能够独立部署和演进。如果我们将响应类型放在公共库中,那么当配置服务的响应结构发生变化时,所有依赖这个公共类型的服务都需要更新和重新部署。通过在每个服务中定义自己需要的响应类型,我们可以更好地控制服务之间的依赖关系,实现服务的松耦合。另外,这种方式也允许不同的服务根据自己的需求定义不同的响应结构,提供了更大的灵活性。当然这也意味着我们需要在服务之间进行适当的数据映射,但这种额外的工作是值得的,因为它带来了更好的服务独立性和可维护性。
2.3 Refit接口与目标微服务绑定
最后一步,我们需要将编写的Refit接口与目标微服务进行绑定,这是实现跨服务调用的关键环节。通过绑定告诉Refit应该在Nacos中查找哪个具体的服务实例。这个绑定过程通常在服务启动时的依赖注入配置中完成。绑定完成后,当我们通过Refit接口发起调用时,框架会自动从Nacos获取目标服务的可用实例列表,然后根据配置的负载均衡策略选择合适的实例进行调用。在Program
中新增代码如下:
csharp
// more code ...
// 注册 Refit 客户端(基于通用服务发现 + Nacos,无需硬编码 BaseUrl)
var nacosSection = builder.Configuration.GetSection("nacos");
var groupName = nacosSection.GetValue<string>("GroupName") ?? "DEFAULT_GROUP";
var clusterName = nacosSection.GetValue<string>("ClusterName") ?? "DEFAULT";
builder.Services.AddNacosRefitClient<IConfigServiceApi>(
serviceName: "SPConfigService",
groupName: groupName,
clusterName: clusterName,
scheme: "http");
// more code ...
在上面的代码中,我们通过AddNacosRefitClient
扩展方法将Refit客户端接口与Nacos服务发现进行了集成。这个方法需要几个关键参数:serviceName
指定了要调用的目标服务在Nacos中注册的名称,在这里是"SPConfigService";groupName
和clusterName
分别从配置文件中获取服务组名和集群名,如果配置文件中没有指定这些值,则使用默认值"DEFAULT_GROUP"和"DEFAULT";scheme
参数指定了服务调用使用的协议,这里使用"http"。通过这种配置,当服务启动时,框架会自动在Nacos中查找名为"SPConfigService"的服务实例,并在运行时根据服务发现的结果动态构建请求URL。这种方式避免了硬编码服务地址,使得服务调用更加灵活和可维护。当目标服务的实例发生变化时,Nacos会自动更新可用实例列表,确保服务调用始终能够找到正确的目标实例。
Tip:在这里,我们将服务间的调用配置为了http,这是因为服务间的内部调用基本上是安全的。在微服务架构中,服务通常部署在同一个内部网络或VPC(Virtual Private Cloud)中,网络环境是受控和隔离的。使用http而不是https可以减少TLS/SSL加密解密带来的性能开销,同时简化了证书管理的复杂性。但是,如果服务需要跨越不同的网络环境或需要更高的安全要求,我们也可以将scheme配置为https,并配置相应的证书来确保通信安全。在生产环境中,具体使用http还是https应该根据实际的安全需求和部署环境来决定。
三、总结
本文详细介绍了在.NET微服务架构中使用Refit实现跨服务调用的方法。通过Refit这个强大的REST客户端库,我们可以将HTTP API转换为类型安全的.NET接口,大大简化了服务间通信的实现。文章以SP.FinanceService
服务为例,展示了如何定义Refit接口、实现跨服务调用以及将接口与目标微服务进行绑定。通过与Nacos服务发现的集成,实现了服务地址的动态发现和负载均衡,避免了硬编码服务地址的问题。这种实现方式不仅提供了类型安全的服务调用,还保持了服务间的松耦合,使得微服务架构更加灵活和可维护。