13.【.NET 8 实战--孢子记账--从单体到微服务--转向微服务】--微服务基础工具与技术--Refit

在微服务架构中,不同服务之间经常需要相互调用以完成复杂业务流程,而 Refit 能让这种"跨服务调用"变得简洁又可靠。开发者只需将对外暴露的 REST 接口抽象成 C# 接口,并通过共享库或内部 NuGet 包在各服务中引用,这种契约优先的做法能够确保接口签名和数据模型在编译期就被校验,避免了运行时因 URL 拼写或参数不一致引发的问题。借助 .NET Core 的依赖注入与 HttpClientFactory,将 Refit 客户端注册到 DI 容器后,所有跨服务调用都使用由框架管理的 HttpClient 实例,不仅解决了连接复用和 DNS 刷新问题,还能集中配置超时、重试和熔断策略(如 Polly)。由于 Refit 的所有方法均返回 Task 或 Task,它与 async/await 模型无缝契合,在高并发场景下既能保持响应性,又能充分利用线程资源。并且基于接口的设计也极大地提升了可测试性:测试时只需 Mock 接口即可模拟各种返回结果和异常场景,无需启动真实服务,从而加快测试反馈和覆盖率。通过共享契约、类型安全、依赖注入和异步支持,Refit 将繁琐的 HTTP 调用封装为直观的接口方法,让微服务之间的调用像本地方法调用一样简单、可靠且易于维护。

一、 安装与配置

安装与配置 Refit 非常简单,主要包括以下几步:

  1. 安装 NuGet 包

    • 核心库:Refit
    • ASP.NET Core HttpClientFactory 集成:Refit.HttpClientFactory
    • 可选Newtonsoft.Json 支持:Refit.Newtonsoft.Json
  2. 注册到依赖注入容器

    Program.cs中,通过 AddRefitClient<T>() 将接口客户端注册到 DI 容器,并配置基础地址及其他策略:

    csharp 复制代码
    builder.Services
      .AddRefitClient<IMyApi>() // 注入接口
      .ConfigureHttpClient(c =>  // 配置 HttpClient
        c.BaseAddress = new Uri("https://api.abc.com"))
      .AddPolicyHandler(Policy.TimeoutAsync<HttpResponseMessage>(TimeSpan.FromSeconds(10))); // Polly 重试/熔断
  3. 自定义序列化

    默认使用 System.Text.Json,性能优异。如果需要 Newtonsoft.Json 特性(如复杂契约、忽略循环引用),安装 Refit.Newtonsoft.Json 并在注册时传入 RefitSettings

    csharp 复制代码
    var settings = new RefitSettings
    {
      ContentSerializer = new NewtonsoftJsonContentSerializer(
        new JsonSerializerSettings
        {
          ContractResolver = new CamelCasePropertyNamesContractResolver(),
          NullValueHandling = NullValueHandling.Ignore
        })
    };
    builder.Services
      .AddRefitClient<IBlogApi>(settings)
      .ConfigureHttpClient(c => c.BaseAddress = new Uri("https://api.abc.com"));
  4. 使用客户端

    在需要调用的服务或控制器中直接构造依赖:

    csharp 复制代码
    public class MyService
    {
      private readonly IMyApi _api;
      public MyService(IMyApi api) => _api = api;
      public async Task DoWorkAsync() {
        var result = await _api.GetDataAsync();
        // more code
      }
    }

完成上述步骤后,Refit 会在运行时自动生成 HTTP 客户端实现,处理 URL 拼接、序列化/反序列化及异常封装。通过与 HttpClientFactory、Polly、DI 容器的无缝集成,既能获得高性能和可扩展性,又能保持代码的简洁与可维护。

二、定义接口

2.1 定义接口
  1. 创建接口定义

    在 Refit 中,接口定义是使用接口和特性来描述的。首先,我们需要创建一个接口,并在其中定义我们要调用的 HTTP 方法。以下是一个简单的示例:

    csharp 复制代码
    using Refit;
    using System.Threading.Tasks;
    
    public interface IMyApi
    {
        [Get("/users/{id}")]
        Task<User> GetUserAsync(int id);
    
        [Post("/users")]
        Task<User> CreateUserAsync([Body] User user);
    }

    在这个示例中,我们定义了一个名为 IMyApi 的接口,其中包含两个方法:GetUserAsyncCreateUserAsync。每个方法都使用了不同的 HTTP 请求类型特性([Get][Post])。

  2. 特性注解详解

    Refit 提供了一组特性用于描述 HTTP 请求的不同注解,常用注解如下:

    • [Get]:用于发送 HTTP GET 请求。可以包含 URL 路径参数和查询参数。

      csharp 复制代码
      [Get("/users/{id}")]
      Task<User> GetUserAsync(int id);
    • [Post]:用于发送 HTTP POST 请求。通常用于创建资源。

      csharp 复制代码
      [Post("/users")]
      Task<User> CreateUserAsync([Body] User user);
    • [Put]:用于发送 HTTP PUT 请求。通常用于更新资源。

      csharp 复制代码
      [Put("/users/{id}")]
      Task<User> UpdateUserAsync(int id, [Body] User user);
    • [Delete]:用于发送 HTTP DELETE 请求。用于删除资源。

      csharp 复制代码
      [Delete("/users/{id}")]
      Task DeleteUserAsync(int id);
    • [Body]:用于指定请求正文中的参数。通常与 POST 和 PUT 请求一起使用。

      csharp 复制代码
      [Post("/users")]
      Task<User> CreateUserAsync([Body] User user);
    • [Query]:用于指定查询参数。可以将方法参数映射到查询字符串中。

      csharp 复制代码
      [Get("/search")]
      Task<List<User>> SearchUsersAsync([Query] string name);
    • [Header]:用于指定请求头。可以在方法级别或参数级别使用。

      csharp 复制代码
      [Get("/users")]
      [Header("Authorization", "Bearer")]
      Task<List<User>> GetUsersAsync();
    • [AliasAs]:用于指定参数的别名。可以用于更改 URL 路径参数或查询参数的名称。

      csharp 复制代码
      [Get("/users/{userId}")]
      Task<User> GetUserAsync([AliasAs("userId")] int id);

通过这些特性,我们可以灵活地定义接口,并描述我们希望 Refit 如何生成和发送 HTTP 请求。

三、使用 Refit 进行 HTTP 调用

3.1 同步调用

虽然 Refit 更适合异步调用,但在某些情况下,我们可能需要进行同步调用。以下是一个使用 Refit 进行同步调用的示例:

csharp 复制代码
using Refit;
using System;

public interface IMyApi
{
    [Get("/users/{id}")]
    Task<User> GetUserAsync(int id);
}

public class ApiService
{
    private readonly IMyApi _api;

    public ApiService()
    {
        _api = RestService.For<IMyApi>("https://api.abc.com");
    }

    public User GetUser(int id)
    {
        // 使用 Task.Result 进行同步调用
        return _api.GetUserAsync(id).Result;
    }
}

在这个示例中,我们定义了一个 IMyApi 接口,并创建了一个 ApiService 类。在 GetUser 方法中,我们使用 Task.Result 来进行同步调用。这种方式虽然简单,但可能会导致线程阻塞,因此应谨慎使用。

3.2 异步调用

Refit 最常用的方式是进行异步调用。以下是一个使用 Refit 进行异步调用的示例:

csharp 复制代码
using Refit;
using System.Threading.Tasks;

public interface IMyApi
{
    [Get("/users/{id}")]
    Task<User> GetUserAsync(int id);

    [Post("/users")]
    Task<User> CreateUserAsync([Body] User user);
}

public class ApiService
{
    private readonly IMyApi _api;

    public ApiService()
    {
        _api = RestService.For<IMyApi>("https://api.abc.com");
    }

    public async Task<User> GetUserAsync(int id)
    {
        return await _api.GetUserAsync(id);
    }

    public async Task<User> CreateUserAsync(User user)
    {
        return await _api.CreateUserAsync(user);
    }
}

在这个示例中,我们同样定义了一个 IMyApi 接口,并创建了一个 ApiService 类。在 GetUserAsyncCreateUserAsync 方法中,我们使用 await 关键字来进行异步调用。这种方式不会阻塞线程,更适合现代应用程序的开发。

通过以上示例,我们可以看到使用 Refit 进行 HTTP 调用的基本方法。根据具体需求,我们可以选择同步调用或异步调用,但在大多数情况下,异步调用是更好的选择。

四、错误处理

4.1 异常处理机制

在使用 Refit 进行 HTTP 调用时,可能会遇到各种异常情况。Refit 提供了一种简单的方式来处理这些异常。以下是一个基本的异常处理示例:

csharp 复制代码
using Refit;
using System;
using System.Net.Http;
using System.Threading.Tasks;

public class ApiService
{
    private readonly IMyApi _api;

    public ApiService()
    {
        _api = RestService.For<IMyApi>("https://api.abc.com");
    }

    public async Task<User> GetUserAsync(int id)
    {
        try
        {
            return await _api.GetUserAsync(id);
        }
        catch (ApiException ex)
        {
            // 处理 API 异常
            Console.WriteLine($"API Error: {ex.StatusCode}");
            // 可以根据具体的 StatusCode 进行进一步处理
            throw;
        }
        catch (HttpRequestException ex)
        {
            // 处理 HTTP 请求异常
            Console.WriteLine($"Request Error: {ex.Message}");
            throw;
        }
        catch (Exception ex)
        {
            // 处理其他类型的异常
            Console.WriteLine($"Unexpected Error: {ex.Message}");
            throw;
        }
    }
}

在这个示例中,我们在 GetUserAsync 方法中使用了 try-catch 结构来捕获并处理不同类型的异常。ApiException 用于处理 Refit 特定的 API 异常,而 HttpRequestException 用于处理一般的 HTTP 请求异常。其他类型的异常则通过通用的 Exception 来处理。

4.2 常见问题与解决方案
  1. 404 Not Found 错误
    问题 : 当请求的资源不存在时,会返回 404 错误。
    解决方案 : 确保请求的 URL 和参数正确。同时,可以在捕获 ApiException 时检查 StatusCode 并进行相应处理。

    csharp 复制代码
    catch (ApiException ex) when (ex.StatusCode == System.Net.HttpStatusCode.NotFound)
    {
        Console.WriteLine("Resource not found.");
    }
  2. 401 Unauthorized 错误
    问题 : 当缺少或使用了无效的身份验证令牌时,会返回 401 错误。
    解决方案 : 确保请求中包含正确的身份验证令牌。可以在接口定义中使用 [Header] 特性添加身份验证头。

    csharp 复制代码
    [Get("/protected-resource")]
    [Header("Authorization", "Bearer")]
    Task<ProtectedResource> GetProtectedResourceAsync();
  3. 超时错误
    问题 : 当请求超时时,会抛出 TaskCanceledException
    解决方案 : 可以设置 HttpClient 的超时时间,并在捕获 TaskCanceledException 时进行相应处理。

    csharp 复制代码
    var httpClient = new HttpClient
    {
        Timeout = TimeSpan.FromSeconds(30)
    };
    var api = RestService.For<IMyApi>(httpClient);
    
    try
    {
        var result = await api.GetUserAsync(id);
    }
    catch (TaskCanceledException ex)
    {
        Console.WriteLine("Request timed out.");
    }
  4. 网络连接错误
    问题 : 当网络连接失败时,会抛出 HttpRequestException
    解决方案 : 可以在捕获 HttpRequestException 时进行重试或其他处理。

    csharp 复制代码
    catch (HttpRequestException ex)
    {
        Console.WriteLine("Network error: " + ex.Message);
        // 可以在这里实现重试逻辑
    }

通过上述示例和解决方案,我们可以有效地处理在使用 Refit 进行 HTTP 调用时遇到的各种错误和异常情况。

五、Refit 的高级功能

5.1 自定义处理程序

Refit 允许我们自定义 HTTP 处理程序,以便在发送请求之前或接收响应之后执行额外的逻辑。通过创建自定义的 HttpMessageHandler,我们可以实现请求重试、日志记录、身份验证等功能。

  1. 创建自定义处理程序
    以下是创建自定义处理程序的示例:

    csharp 复制代码
    using System.Net.Http;
    using System.Threading;
    using System.Threading.Tasks;
    
    public class CustomHttpMessageHandler : DelegatingHandler
    {
        protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
        {
            // 在发送请求之前执行逻辑
            Console.WriteLine("Sending request to " + request.RequestUri);
    
            // 发送请求并获取响应
            var response = await base.SendAsync(request, cancellationToken);
    
            // 在接收响应之后执行逻辑
            Console.WriteLine("Received response with status code " + response.StatusCode);
    
            return response;
        }
    }
  2. 使用自定义处理程序
    在创建 Refit 客户端时,可以将自定义处理程序传递给 HttpClient

    csharp 复制代码
    using Refit;
    using System.Net.Http;
    
    public class ApiService
    {
        private readonly IMyApi _api;
    
        public ApiService()
        {
            var handler = new CustomHttpMessageHandler
            {
                InnerHandler = new HttpClientHandler()
            };
    
            var httpClient = new HttpClient(handler)
            {
                BaseAddress = new Uri("https://api.abc.com")
            };
    
            _api = RestService.For<IMyApi>(httpClient);
        }
    }

通过这种方式,我们可以在整个请求生命周期中插入自定义逻辑。

5.2 拦截器与中间件

拦截器与中间件提供了一种更高级的方式来处理 HTTP 请求和响应。我们可以使用拦截器来修改请求或响应,或者添加额外的处理逻辑。

  1. 创建拦截器

    以下是创建请求和响应拦截器的示例:

    csharp 复制代码
    using System.Net.Http;
    using System.Threading;
    using System.Threading.Tasks;
    
    public class LoggingHandler : DelegatingHandler
    {
        protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
        {
            // 日志记录请求信息
            Console.WriteLine("Request:");
            Console.WriteLine(request.ToString());
            if (request.Content != null)
            {
                Console.WriteLine(await request.Content.ReadAsStringAsync());
            }
    
            var response = await base.SendAsync(request, cancellationToken);
    
            // 日志记录响应信息
            Console.WriteLine("Response:");
            Console.WriteLine(response.ToString());
            if (response.Content != null)
            {
                Console.WriteLine(await response.Content.ReadAsStringAsync());
            }
    
            return response;
        }
    }
  2. 使用拦截器

    同样,我们可以在创建 Refit 客户端时使用拦截器:

    csharp 复制代码
    using Refit;
    using System.Net.Http;
    
    public class ApiService
    {
        private readonly IMyApi _api;
    
        public ApiService()
        {
            var handler = new LoggingHandler
            {
                InnerHandler = new HttpClientHandler()
            };
    
            var httpClient = new HttpClient(handler)
            {
                BaseAddress = new Uri("https://api.abc.com")
            };
    
            _api = RestService.For<IMyApi>(httpClient);
        }
    }
  3. 中间件的使用

    Refit 本身并不直接支持中间件,但我们可以通过自定义处理程序和拦截器来实现类似中间件的功能。通过将多个处理程序链接在一起,我们可以创建一个请求处理管道,每个处理程序都可以在请求和响应的不同阶段插入逻辑。

    例如,我们可以将日志记录、身份验证和重试逻辑分别实现为不同的处理程序,并将它们组合在一起:

    csharp 复制代码
    var loggingHandler = new LoggingHandler
    {
        InnerHandler = new AuthenticationHandler
        {
            InnerHandler = new RetryHandler
            {
                InnerHandler = new HttpClientHandler()
            }
        }
    };
    
    var httpClient = new HttpClient(loggingHandler)
    {
        BaseAddress = new Uri("https://api.abc.com")
    };
    
    _api = RestService.For<IMyApi>(httpClient);

    通过这种方式,我们可以实现灵活且可扩展的请求处理管道,满足各种复杂的需求。

六、实战案例

6.1 实战案例
  1. 案例背景

    在本节中,我们将通过一个简单的示例项目来演示如何使用 Refit 进行 HTTP 调用。这个项目将模拟一个用户管理系统,我们将实现以下功能:

    • 获取用户信息
    • 创建新用户

    我们将首先定义接口,然后配置依赖注入,最后调用接口并处理响应。

  2. 实现步骤

    • 创建接口定义

      首先,我们需要创建一个接口来定义我们的 HTTP 调用。这些接口将使用 Refit 特性来描述每个请求。

      csharp 复制代码
      using Refit;
      using System.Threading.Tasks;
      
      public interface IMyApi
      {
          [Get("/users/{id}")]
          Task<User> GetUserAsync(int id);
      
          [Post("/users")]
          Task<User> CreateUserAsync([Body] User user);
      }

      在这个接口定义中,我们定义了两个方法:GetUserAsyncCreateUserAsyncGetUserAsync 方法使用 [Get] 特性来标识这是一个 GET 请求,并且 URL 中包含一个路径参数 {id}CreateUserAsync 方法使用 [Post] 特性来标识这是一个 POST 请求,并且请求正文中包含一个 User 对象。

    • 配置依赖注入

      接下来,我们需要在项目中配置依赖注入,以便在需要的地方可以使用我们的 API 接口。

      Program.cs 文件中,我们需要添加以下代码:

      csharp 复制代码
      using Microsoft.Extensions.DependencyInjection;
      using Refit;
      
      public class Program
      {
         // more code
           builder.Services
      	     .AddRefitClient<IMyApi>() // 注入接口
      	     .ConfigureHttpClient(c =>  // 配置 HttpClient
      	       c.BaseAddress = new Uri("https://api.abc.com"));
      	 // more code
      }

      在这个配置中,我们使用 AddRefitClient 方法将 IMyApi 接口添加到依赖注入容器中,并设置基础地址为 https://api.abc.com

    • 调用接口并处理响应

      最后,我们可以在需要的地方调用接口并处理响应。在一个控制器或服务中,我们可以注入 IMyApi 接口并使用它来进行 HTTP 调用。

      csharp 复制代码
      using Microsoft.AspNetCore.Mvc;
      using System.Threading.Tasks;
      
      [ApiController]
      [Route("api/[controller]")]
      public class UsersController : ControllerBase
      {
          private readonly IMyApi _api;
      
          public UsersController(IMyApi api)
          {
              _api = api;
          }
      
          [HttpGet("{id}")]
          public async Task<ActionResult<User>> GetUser(int id)
          {
              try
              {
                  var user = await _api.GetUserAsync(id);
                  return Ok(user);
              }
              catch (ApiException ex)
              {
                  return StatusCode((int)ex.StatusCode, ex.Content);
              }
          }
      
          [HttpPost]
          public async Task<ActionResult<User>> CreateUser(User user)
          {
              try
              {
                  var createdUser = await _api.CreateUserAsync(user);
                  return CreatedAtAction(nameof(GetUser), new { id = createdUser.Id }, createdUser);
              }
              catch (ApiException ex)
              {
                  return StatusCode((int)ex.StatusCode, ex.Content);
              }
          }
      }

      在这个控制器中,我们定义了两个方法:GetUserCreateUser。我们使用依赖注入的 IMyApi 接口来调用 API,并处理可能的异常。在 GetUser 方法中,我们使用 await 关键字来异步调用 GetUserAsync 方法,并返回用户信息。在 CreateUser 方法中,我们使用 await 关键字来异步调用 CreateUserAsync 方法,并返回创建的用户信息。

通过以上步骤,我们完成了一个简单的 Refit 实战案例。这个案例演示了如何定义接口、配置依赖注入,以及调用接口并处理响应。在实际项目中,我们可以根据需要扩展和修改这个示例。

七、总结

在微服务架构中,不同服务之间经常需要相互调用以完成复杂业务流程,而 Refit 能让这种"跨服务调用"变得简洁又可靠。通过将对外暴露的 REST 接口抽象成 C# 接口,并通过共享库或内部 NuGet 包在各服务中引用,开发者可以确保接口签名和数据模型在编译期被校验,避免了运行时因 URL 拼写或参数不一致引发的问题。

借助 .NET Core 的依赖注入与 HttpClientFactory,将 Refit 客户端注册到 DI 容器后,所有跨服务调用都使用由框架管理的 HttpClient 实例,不仅解决了连接复用和 DNS 刷新问题,还能集中配置超时、重试和熔断策略(如 Polly)。由于 Refit 的所有方法均返回 Task 或 Task,它与 async/await 模型无缝契合,在高并发场景下既能保持响应性,又能充分利用线程资源。

基于接口的设计也极大地提升了可测试性,测试时只需 Mock 接口即可模拟各种返回结果和异常场景,无需启动真实服务,从而加快测试反馈和覆盖率。通过共享契约、类型安全、依赖注入和异步支持,Refit 将繁琐的 HTTP 调用封装为直观的接口方法,让微服务之间的调用像本地方法调用一样简单、可靠且易于维护。

在本文中,我们详细介绍了 Refit 的安装与配置、接口定义、HTTP 调用、错误处理、高级功能以及实战案例。通过这些内容,读者可以掌握使用 Refit 进行跨服务调用的基本方法和技巧,并在实际项目中灵活应用。

相关推荐
眠りたいです23 分钟前
基于脚手架微服务的视频点播系统-数据管理与网络通信部分的预备工作
c++·qt·ui·微服务·云原生·架构·媒体
虫小宝2 小时前
返利软件的分布式缓存架构:Redis集群在高并发场景下的优化策略
分布式·缓存·架构
一水鉴天2 小时前
整体设计 之 绪 思维导图引擎 之 引 认知系统 之 引 认知系统 之 序 认知元架构 之6 拼句 之1 (豆包助手 之8)
架构·认知科学
纪元A梦3 小时前
Redis最佳实践——安全与稳定性保障之高可用架构详解
redis·安全·架构
Dontla3 小时前
流行的前端架构与后端架构介绍(Architecture)
前端·架构
熊文豪4 小时前
KingbaseES读写分离集群架构解析
数据库·架构·kingbasees·金仓数据库·电科金仓
往事随风去4 小时前
别再纠结了!IM场景下WebSocket和MQTT的正确选择姿势,一文讲透!
后端·websocket·架构
爱读源码的大都督4 小时前
为什么Spring 6中要把synchronized替换为ReentrantLock?
java·后端·架构
一水鉴天5 小时前
整体设计 之 绪 思维导图引擎 之 引 认知系统 之 引 认知系统 之 序 认知元架构 之 元宇宙:三种“即是”逻辑与数据安全措施的适配(豆包助手 之10)
架构·认知科学
虫小宝5 小时前
淘宝客app的API网关设计:认证授权与流量控制策略
java·分布式·架构