[.net] 关于Exception的几点思考和在项目中的使用(三)

本文探讨了在.NET WebApi项目中如何有效处理错误和异常,包括使用带有ErrorCode的异常来简化错误返回,以及如何通过ExceptionFilter或中间件统一处理异常。同时,文中还讨论了CheckedException的概念及其在.NET和Java中的应用。

本文链接: https://blog.youkuaiyun.com/brlite/article/details/115603766

系列文章:


Web Api 的错误返回

在使用.net 的 Web Api构建Endpoint对外提供访问时,往往需要统一的错误返回格式。

如果按照前面两篇文章(一)(二)所说,采用带有ErrorCode的异常ErrorCodeException,那么在WebApi中返回错误时,只要返回ErrorCode即可。

代码如下:

[HttpPut]
[Authorize]
public IActionResult Update(UpdatetNickNameRequest request)
{
    try
    {
        long userId = User.GetUserId();

        _identityService.SetNickName(userId, request.NickName);
        
        return Ok();
    }
    catch (IdentityException ex)
    {
        //做相应处理
        //... ...
        return new BadRequestObjectResult(ex.ErrorCode);
    }
    catch (OtherException ex)
    {
        //做相应处理
        //... ...
        return new BadRequestObjectResult(ex.ErrorCode);
    }
}

正如评论里,大家所说的,可以使用ExceptionFilter,或者中间件,来统一的捕捉各类异常。

也可以使用UseExceptionHandler扩展方法,将异常投递到自定义的路径上。

举例来说,如果使用ExceptionFilter来统一处理的话,代码如下:

public class ExceptionFilter : IExceptionFilter
{
    public void OnException(ExceptionContext context)
    {
        ErrorCode errorCode = context.Exception is ErrorCodeException ex ? ex.ErrorCode : ErrorCode.Empty;

        //其他操作,比如日志等,或者根据不同的ErrorCode做出不同的处理

        context.Result = new BadRequestObjectResult(errorCode);

        context.ExceptionHandled = true;
    }
}

ExceptionFilter注册到services中后,可以在services.AddControllers()方法里,全局添加Filter,避免每一个方法都手工添加。

代码如下:

services.AddControllers(options =>
{
    options.Filters.AddService<UserActivityFilter>();  
})                        

总之,重点是在我们返回了ErrorCode。ErrorCode类包含了Id, Name, Message属性,方便客户端在收到错误返回后进行处理。

自定义的客户端在接受到Api的返回后,检查HttpStatusCode,如果不成功(不是2xx),那么也直接抛出带有ErrorCode的异常ErrorCodeException即可。

这样,前后端的处理就变得一致和简便。特别是如果你在前端也采用 .net 技术,比如 blazor、xamarin、wpf、winform等等,那前后端可以使用相同的CodeBase。

Exception的捕捉

前面几篇文章,一直在关注Exception的抛出,现在我们来关注一下Exception的捕捉。

那些基础的catch/finally这里就不再赘述,说几个项目中实际出现的现象和问题。

不要吃掉异常

不要吃掉异常有两个层面,一个是直接忽略,比如catch了所有的Exception,并不妥善处理(假处理,就打印一句log,甚至不处理),不过这种失误要不是新手所犯,要不就是编码过程中草草了事的结果。

这里主要注意第二个层面,即catch时不要隐藏异常的类型,即不要catch(Exception ex),这意味着隐藏了异常的类型,吃掉了针对不同的错误采取不同的措施的机会。

微软的code analysis也直接给出了规则 CA1031:不要捕捉一般异常类型.

Checked Exception

CE是确保你所有的异常都得到捕捉,它往往在编译层面提供对Exception的检查,确保你的程序不会因为未处理的异常而终止。

在编写可靠的程序时,程序员需要知道调用的方法抛出什么异常,我是否需要处理,还是包装,还是直接抛出不理,总之我们需要有途径获取这样的信息。

当然,Checked Exception肯定会带来更多的代码量,且在项目初始建立起来,往往需要修改一处,而动整个链条上的代码,一层层的修改。

所以,有些人喜欢CE,认为它带来了规矩,改善了团队代码质量;当然也有些人认为它带来了繁琐。

仁者见仁智者见智,这里不加入这些争辩。争辩1, 争辩2, 争辩3, 争辩4

.net vs java

不得不拿这两门语言进行对比,每个世界都要互相借鉴。

熟悉java语法的同学肯定会知道java方法定义上有throws这一关键词:

public int div(int i,int j) throws Exception {...}

throws这一关键词保证了从方法定义上就能知道一个方法抛出什么样的异常,直接借助编译器检查或者IDE的智能提醒,就不会漏掉异常。

而c#中并没有相同的实现,可以见Stackoverflow上的讨论.

那么在 .net 世界中当你调用一个方法时,怎么妥善的知道这个方法抛出什么样的异常呢?知道后我们才能决定是否处理这个异常,还是继续抛出。

目前的答案是:注释!(不要笑,很严肃的解决方案)。

/// <exception cref="IdentityService.IdentityService">这样写Exception注释</exception>
public void SetNickName(long userId, string newNickName)
{
    //....
    Ensure.NickNameNotExisted(newNickName);
    //....
}

当你翻看.net的源码时,会看到所有方法的注释中都良好的列出了有哪些异常。

有几个问题:

  • 我们团队没人写注释,怎么办?
  • 调用方法时,并没有智能提示有哪些异常,所以我们经常忽略
  • 想看异常就得F12看定义,太繁琐。

有很多问题只从技术上没法解决,但我们尽量可以借助一些Review工具来检查团队的代码,提出要求。

此外你会喜欢上 ctrl + k, ctrl+i 这个快捷键的,他能帮助你快速查看注释文档,查看有哪些异常。

visual studio 扩展推荐

在这里,我推荐一个visual studio的扩展,是的,它的名字就叫 Checked Exceptions, 这是我必备的一款扩展。
CheckedException

这款插件会借助注释的形式,协助实现Checked Exception的功能,并且可以快速添加相应注释。

这个有个小提示:如果你从项目伊始采用这款插件,折磨小一点,如果半路使用,那么当作检验团队代码强健性的工具也不错。

此外,这个款扩展可能还有些bug,导致即使注释了Exception还不断提示,所以我平时并不一直启用它,而是在做Code Review时,使用它作为一个检查工具。

这样可以比较好的解决上面提到的Checked Exception的缺点,又利用它的优点。

常见模式

不要重复的抛出

这个只是简单提醒下,见如下代码。

void BadSmellMethod()
{
    try
    {
        .....
    }
    catch(Exception ex)
    {
        //.... some thing
        
        // 错误的做法
        throw ex;  
    }
}

void GoodMethod()
{
    try
    {
        .....
    }
    catch
    {
        //... some thing
        // 错误的做法
        throw;  
    }
}

简单来说就是重复抛出,丢失了引发异常原始方法和当前方法调用之间的StackTrace。

在code analysis中也有相应规则。CA2200.

在asp.net core 中

在捕捉异常时,往往一个异常被一路抛出,或者包装再抛出,直到终点。如果到了终点还没有被捕获,那么就会引发程序中止,这是谁都不想看到的。

在asp.net core中,这个终点就是Controller控制器,所以我们需要在Controller的方法里调用需要的Service,处理那些需要特殊处理的异常,然后使用全局错误处理(ExceptionFilter、中间件等)兜住其他的异常。

在xamarin.forms中

如果你是同道中人,使用xamarin.forms,那么你肯定知道MVVM模式。

异常的终点往往就在MVVM模式中的ViewModel中,比如LoginPageViewModel中,同样ViewModel调用各项Service,你需要在这里处理当下场景里需要处理的异常,然后自定义全局的错误处理兜住其他异常。

结语

本文,简要介绍了具体项目中异常的捕捉问题,欢迎大家交流指正。

下篇,我们关注一下 异步编程中的Exception,以及全局错误处理。

谢谢阅读。

<think> 我们分析用户的需求:在C#代码中,使用中间件路由转发(通过Yarp反向代理)来实现WEB调用大屏后台接口,并且要保证在大屏后台服务不启动的情况下,只启动WEB服务也能调用到大屏后台接口并正常运行。 当前代码中,使用Yarp反向代理将请求转发到后台服务(地址为http://localhost:8100)。但是,如果后台服务没有启动,那么当请求到达反向代理时,反向代理会尝试连接后台服务,由于连接失败,会返回给客户端一个错误的响应(如502 Bad Gateway等)。这显然不符合用户要求(即使后台服务不启动,WEB服务也能正常运行,即能正常调用接口)。 因此,我们需要增加一个容错机制:当后台服务不可用时,我们不应该直接让反向代理转发请求,而是应该由WEB服务自身来处理这些请求(例如,返回一个静态的降级数据,或者从缓存中读取数据等)。 然而,Yarp本身并不提供这种熔断或降级的功能。因此,我们需要在反向代理的前面加入一个中间件,该中间件会检测后台服务的可用性。如果后台服务可用,则正常转发;如果不可用,则拦截请求,由WEB服务自己处理。 具体步骤: 1. 我们需要一个服务(例如`IBackendHealthService`)来定期检查后台服务的健康状态(通过一个健康检查的接口)。 2. 在中间件中,根据健康检查的结果,决定是转发请求(通过反向代理)还是由当前服务处理。 但是,注意我们的反向代理配置是直接通过`MapReverseProxy`来映射的。我们可以在反向代理中间件之前加入一个中间件,该中间件判断请求的路径是否为大屏接口(即匹配`/dev/api/blade-visual/**`),并且判断后台服务是否可用。如果不可用,则重写请求路径,将其指向我们本地的一个控制器(该控制器负责返回降级数据或静态数据);如果可用,则交给反向代理处理。 然而,这种方法需要修改请求路径,而且需要本地控制器能够处理所有的大屏接口,这可能会很复杂。 另一种思路:在反向代理的转换器中,我们可以检查后台服务的健康状态,如果不可用,则修改请求的目标地址(改为本地的一个地址,由本地控制器处理)。但是Yarp的转换器主要修改请求头请求路径,并不能改变集群(即不能从集群A切换到集群B)。 重新思考:我们可以配置两个集群,一个指向真实的后台服务,另一个指向本地的一个地址(由本地的一个控制器处理)。然后在中间件中,根据健康状态动态选择集群。 但是,Yarp的路由配置在启动时是固定的,不支持运行时动态修改(虽然Yarp支持热更新配置,但实现起来较复杂)。 因此,我们采用折中方案:在反向代理之前添加一个中间件,该中间件检查请求是否为大屏接口路径,并且检查后台服务是否健康。如果不健康,则修改请求的路径,将其重定向到本地的一个控制器(该控制器模拟大屏后台接口,返回缓存数据或静态数据)。这样,请求就不会被转发到反向代理,而是被本地控制器处理。 具体实现步骤: 1. 创建一个后台服务健康检查器(定期检查后台服务是否可用)。 2. 在中间件中(在`MapReverseProxy`之前): - 如果请求路径匹配大屏接口(即`/dev/api/blade-visual/...`)且后台服务不可用,则重写请求路径,指向本地的一个控制器(例如,将路径改为`/Fallback/ScreenApi/...`)。 - 否则,进入下一个中间件(最终会由反向代理处理)。 3. 编写一个Fallback控制器,用于处理所有重写过来的大屏接口请求,返回预先准备好的数据(或者读取缓存,或者返回一个默认的响应)。 4. 注意:健康检查服务需要定期检查后台服务的健康状态(例如,每10秒检查一次),并将其结果存储在内存中(用一个布尔变量表示)。同时,为了避免频繁请求,健康检查可以使用一个后台定时任务。 代码修改: 步骤1:创建健康检查服务 首先,我们创建一个接口实现类来检查后台服务的健康状态。 ```csharp public interface IBackendHealthChecker { bool IsBackendHealthy { get; } Task CheckHealthAsync(); } public class BackendHealthChecker : IBackendHealthChecker { private readonly HttpClient _httpClient; private bool _isHealthy; private readonly ILogger<BackendHealthChecker> _logger; public BackendHealthChecker(ILogger<BackendHealthChecker> logger) { _httpClient = new HttpClient(); _httpClient.Timeout = TimeSpan.FromSeconds(5); // 5秒超时 _logger = logger; } public bool IsBackendHealthy => _isHealthy; public async Task CheckHealthAsync() { try { // 假设后台服务有一个健康检查端点,比如 /health var response = await _httpClient.GetAsync("http://localhost:8100/health"); _isHealthy = response.IsSuccessStatusCode; _logger.LogInformation($"Backend health check: {_isHealthy}"); } catch (Exception ex) { _isHealthy = false; _logger.LogError(ex, "Backend health check failed"); } } } ``` 然后,在Program.cs中注册为单例,并启动后台任务: ```csharp // 在builder.Services中添加: builder.Services.AddSingleton<IBackendHealthChecker, BackendHealthChecker>(); // 在app启动后,启动后台任务(注意:在app运行之前,健康检查任务应该开始运行) ``` 在Program.cs中,我们可以在app运行之后启动一个后台任务,但是为了避免在后台服务未准备好时就开始健康检查,我们可以在应用启动时启动一个后台任务: 修改Program.cs,在构建完app之后,启动健康检查循环: ```csharp // ... 前面的代码 var app = builder.Build(); // 从容器中获取健康检查器 var healthChecker = app.Services.GetRequiredService<IBackendHealthChecker>(); // 启动后台健康检查任务(注意:这里使用Task.Run,但是要注意优雅关闭) var healthCheckTask = Task.Run(async () => { while (true) { await healthChecker.CheckHealthAsync(); await Task.Delay(TimeSpan.FromSeconds(10)); // 每10秒检查一次 } }); // 注意:在应用关闭时应该取消健康检查任务,这里为了简化,先不做处理(生产环境需要考虑) // ... 后面的中间件配置等 ``` 步骤2:创建降级控制器 创建一个FallbackController,用于处理大屏接口的降级请求。注意,这个控制器需要能够处理所有大屏接口的请求,因此我们可以使用通配路由。 ```csharp [ApiController] [Route("/Fallback/ScreenApi")] public class FallbackScreenApiController : ControllerBase { // 这里我们可以根据请求的路径参数,返回不同的降级数据 // 为了简化,我们只返回一个固定的响应,或者根据请求的最后一部分(如action)来返回不同的数据 [HttpGet("{**catchAll}")] public IActionResult Get(string catchAll) { // 可以根据catchAll路径来返回不同的数据 // 这里简单返回一个降级提示 return Ok(new { message = "Backend service is unavailable. Using fallback data." }); // 实际项目中,可以返回预先准备好的静态数据 } // 同样处理POST等其他请求方法 [HttpPost("{**catchAll}")] public IActionResult Post(string catchAll) { return Ok(new { message = "Backend service is unavailable. Using fallback data for POST." }); } } ``` 步骤3:在中间件中根据健康状态重写请求 在现有的中间件(即处理资源映射的那个中间件)之后,反向代理之前,添加一个新的中间件: ```csharp app.Use(async (context, next) => { // 获取健康检查服务 var healthChecker = context.RequestServices.GetRequiredService<IBackendHealthChecker>(); // 检查请求路径是否匹配大屏接口 var path = context.Request.Path.Value; if (path != null && path.StartsWith("/dev/api/blade-visual/")) { // 如果后台服务不可用 if (!healthChecker.IsBackendHealthy) { // 重写请求路径,指向我们的降级控制器 // 注意:保留原始路径中的子路径查询字符串 var newPath = "/Fallback/ScreenApi" + path.Substring("/dev/api/blade-visual".Length); context.Request.Path = newPath; // 注意:这里我们保留了查询字符串,所以不需要修改 // 日志记录 Console.WriteLine($"Fallback for {path} due to backend unhealthy"); } } await next(); }); ``` 注意:这个中间件应该放在`MapReverseProxy`之前,因为`MapReverseProxy`是一个终结点,如果在中间件中重写了路径,那么反向代理就不会处理这个请求了(因为路径已经改变,不再匹配反向代理的路由规则)。 因此,我们将这个中间件放在`UseEndpoints`之前(因为`MapReverseProxy`是在`UseEndpoints`中配置的)。实际上,中间件应该放在反向代理终结点之前,也就是在请求进入终结点路由之前进行重写。 修改Program.cs中的中间件顺序: ```csharp // ... 其他中间件 // 放在UseRouting之后,UseEndpoints之前 app.UseRouting(); // 自定义中间件:检查大屏接口并重写(如果后台服务不可用) app.Use(async (context, next) => { // ... 如上所述 }); // 然后配置终结点 app.UseEndpoints(endpoints => { endpoints.MapReverseProxy(); // 反向代理端点 // ... 其他终结点 }); ``` 但是注意:我们的健康检查中间件依赖于路由中间件已经执行(因为我们要获取请求路径),所以我们必须放在`UseRouting`之后。另外,我们的重写操作必须在终结点路由之前(即在`UseEndpoints`之前),这样重写后的路径才能被路由到降级控制器。 步骤4:测试 启动WEB服务,不启动大屏后台服务,等待10秒后(健康检查失败),然后请求大屏接口,应该会被重定向到降级控制器,返回降级数据。 注意事项: 1. 健康检查任务需要以安全的方式在后台运行,并且在应用关闭时停止。上面的例子使用了简单的Task.Run,实际可以使用`IHostedService`来实现。 2. 降级控制器需要根据实际接口返回模拟数据,这里只是简单示例。 3. 健康检查的地址(这里是`http://localhost:8100/health`)需要确保后台服务确实存在这个端点。 改进:使用IHostedService来实现后台健康检查 我们可以将健康检查器改为后台服务: ```csharp public class BackendHealthCheckService : IHostedService, IDisposable { private readonly IBackendHealthChecker _healthChecker; private readonly ILogger<BackendHealthCheckService> _logger; private Timer _timer; public BackendHealthCheckService(IBackendHealthChecker healthChecker, ILogger<BackendHealthCheckService> logger) { _healthChecker = healthChecker; _logger = logger; } public Task StartAsync(CancellationToken cancellationToken) { _logger.LogInformation("Backend health check service is starting."); _timer = new Timer(DoWork, null, TimeSpan.Zero, TimeSpan.FromSeconds(10)); return Task.CompletedTask; } private void DoWork(object state) { _ = _healthChecker.CheckHealthAsync(); } public Task StopAsync(CancellationToken cancellationToken) { _logger.LogInformation("Backend health check service is stopping."); _timer?.Change(Timeout.Infinite, 0); return Task.CompletedTask; } public void Dispose() { _timer?.Dispose(); } } ``` 然后注册服务: ```csharp builder.Services.AddSingleton<IBackendHealthChecker, BackendHealthChecker>(); builder.Services.AddHostedService<BackendHealthCheckService>(); ``` 这样,健康检查就会在后台定时运行,并且应用关闭时会停止。 总结:通过添加健康检查服务中间件重写,我们可以在后台服务不可用时,将大屏接口的请求重定向到本地的降级控制器,从而保证服务正常运行。 最后,注意原始代码中反向代理的配置是固定的,我们并未改变反向代理的行为,而是在反向代理之前拦截了请求。 因此,修改后的代码结构如下(在Program.cs中): ```csharp // ... 之前的builder配置 builder.Services.AddReverseProxy() .LoadFromMemory( /* 同上 */); // 注册健康检查相关服务 builder.Services.AddSingleton<IBackendHealthChecker, BackendHealthChecker>(); builder.Services.AddHostedService<BackendHealthCheckService>(); // ... 其他服务注册 var app = builder.Build(); // ... 中间件配置 app.UseHttpsRedirection(); app.UseRouting(); // 自定义中间件:健康检查降级 app.Use(async (context, next) => { var healthChecker = context.RequestServices.GetRequiredService<IBackendHealthChecker>(); var path = context.Request.Path.Value ?? string.Empty; if (path.StartsWith("/dev/api/blade-visual/")) { if (!healthChecker.IsBackendHealthy) { // 保留查询字符串 var queryString = context.Request.QueryString.HasValue ? context.Request.QueryString.Value : ""; // 重写路径 var newPath = $"/Fallback/ScreenApi{path.Substring("/dev/api/blade-visual".Length)}{queryString}"; context.Request.Path = "/Fallback/ScreenApi" + path.Substring("/dev/api/blade-visual".Length); // 注意:查询字符串会自动保留,不需要修改 // 如果需要记录原始请求,可以添加日志 Console.WriteLine($"Redirecting to fallback: {path} -> {context.Request.Path}"); } } await next(); }); app.UseEndpoints(endpoints => { endpoints.MapReverseProxy(); // ... 其他终结点 }); // ... 其他中间件控制器映射 ``` 注意:降级控制器的路由是`/Fallback/ScreenApi`,而我们在重写路径时,将原始路径中`/dev/api/blade-visual`之后的部分保留,并添加在`/Fallback/ScreenApi`后面。这样,降级控制器就可以通过通配路由匹配到。 但是,如果原始请求有查询字符串,它仍然会保留在请求中,所以降级控制器可以使用`Request.Query`来获取查询参数。 这样,我们就实现了在后台服务不可用时,由本地控制器提供降级数据的功能。 </think> ### 问题分析与解决方案 您的代码主要存在以下几个关键问题,导致在大屏后台服务不启动时无法正常运行: 1. **反向代理无降级机制**: - Yarp反向代理直接连接`localhost:8100`,当后台服务未启动时,所有请求将返回 **503 Service Unavailable** 错误[^1] - 缺乏服务状态检测降级处理逻辑 2. **健康检查缺失**: - 没有对后台服务进行可用性检测 - 无法在服务不可用时切换处理方式 3. **错误处理不完善**: - 反向代理错误直接传递给客户端,无友好提示 - 静态文件处理与代理逻辑混合,职责不清晰 --- ### 修复方案(Program.cs完整代码) ```csharp using JNPF.API.Entry; using JNPF.Common.Configuration; using JNPF.Common.Dtos.OtherData; using JNPF.VisualData.Entity; using Microsoft.AspNetCore.StaticFiles; using Microsoft.Extensions.FileProviders; using System.Net; using Yarp.ReverseProxy.Forwarder; var builder = WebApplication.CreateBuilder(args); JnpfInternalAppInitializer.InitJnpfInternalApp(builder); // 添加健康检查服务 builder.Services.AddHealthChecks() .AddCheck<BackendHealthCheck>("backend_service"); // 添加HttpClient用于健康检查 builder.Services.AddHttpClient("BackendHealth") .ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler { ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator }); // 反向代理配置 builder.Services.AddReverseProxy() .LoadFromMemory( routes: new[] { new RouteConfig { RouteId = "ScreenApi_Route", ClusterId = "ScreenApi_Cluster", Match = new RouteMatch { Path = "/dev/api/blade-visual/{**catch-all}" } } }, clusters: new[] { new ClusterConfig { ClusterId = "ScreenApi_Cluster", Destinations = new Dictionary<string, DestinationConfig> { { "ScreenApi_Server1", new DestinationConfig { Address = "http://localhost:8100" } } } } }); // 添加降级控制器 builder.Services.AddControllers() .AddApplicationPart(typeof(FallbackController).Assembly); var app = builder.Build(); // 中间件配置 app.UseHttpsRedirection(); app.UseRouting(); // 健康检查端点 app.MapHealthChecks("/health"); // 自定义代理中间件(带降级处理) app.Use(async (context, next) => { // 检查是否为大屏接口请求 if (context.Request.Path.StartsWithSegments("/dev/api/blade-visual")) { // 获取健康检查结果 var healthReport = await context.RequestServices .GetRequiredService<HealthCheckService>() .CheckHealthAsync(); // 服务不可用时启用降级 if (healthReport.Status == HealthStatus.Unhealthy) { // 重写到本地降级控制器 context.Request.Path = "/fallback" + context.Request.Path; Console.WriteLine($"启用降级处理: {context.Request.Path}"); } } await next(); }); // 配置端点 app.UseEndpoints(endpoints => { endpoints.MapReverseProxy(proxyPipeline => { // 代理错误处理 proxyPipeline.Use(async (context, next) => { try { await next(); } catch (Exception ex) when (ex is HttpRequestException || ex is OperationCanceledException) { context.Response.StatusCode = StatusCodes.Status503ServiceUnavailable; await context.Response.WriteAsync("{\"code\":503,\"message\":\"后台服务不可用,请稍后重试\"}"); } }); }); endpoints.MapRazorPages(); endpoints.MapControllerRoute( name: "default", pattern: "{controller=Home}/{action=Index}/{id?}"); endpoints.MapControllers(); }); // 静态文件配置(保持不变) var systemPath = KeyVariable.SystemPath; var visualImgPath = Path.Combine(systemPath, "BiVisualPath"); // ... [原有静态文件配置不变] app.Run(); // 健康检查实现 public class BackendHealthCheck : IHealthCheck { private readonly IHttpClientFactory _clientFactory; public BackendHealthCheck(IHttpClientFactory clientFactory) => _clientFactory = clientFactory; public async Task<HealthCheckResult> CheckHealthAsync( HealthCheckContext context, CancellationToken cancellationToken = default) { try { var client = _clientFactory.CreateClient("BackendHealth"); var response = await client.GetAsync("http://localhost:8100/health", cancellationToken); return response.IsSuccessStatusCode ? HealthCheckResult.Healthy() : HealthCheckResult.Unhealthy(); } catch { return HealthCheckResult.Unhealthy(); } } } // 降级控制器 [ApiController] public class FallbackController : ControllerBase { [Route("/fallback/dev/api/blade-visual/{**any}")] public IActionResult HandleFallback() { // 返回预置的静态数据或友好提示 return Ok(new { code = 200, data = new { message = "后台服务维护中,使用本地缓存数据", timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds() } }); } } ``` ### 关键改进说明: 1. **健康检查机制**: - 添加`BackendHealthCheck`服务定期检测后台可用性 - 通过`/health`端点暴露服务状态[^1] ```csharp app.MapHealthChecks("/health"); ``` 2. **动态降级处理**: - 中间件根据健康状态重写请求路径(`/fallback`前缀) - 降级控制器返回预置数据或友好提示 ```csharp if (healthReport.Status == HealthStatus.Unhealthy) { context.Request.Path = "/fallback" + context.Request.Path; } ``` 3. **代理错误处理**: - 捕获代理请求异常(连接超时/服务不可用) - 返回标准化错误响应(HTTP 503) ```csharp catch (Exception ex) when (ex is HttpRequestException || ex is OperationCanceledException) { context.Response.StatusCode = 503; await context.Response.WriteAsync(...); } ``` 4. **职责分离**: - 将静态文件处理与业务逻辑分离 - 降级控制器独立处理异常场景 ### 部署验证: 1. **正常场景**: - 启动后台服务:访问`/dev/api/blade-visual` → 正常代理 - 健康检查:`/health`返回200 OK 2. **降级场景**: - 关闭后台服务:访问`/dev/api/blade-visual` → 返回降级数据 - 健康检查:`/health`返回503 Unhealthy --- ### 相关问题 1. 如何实现不同级别的服务降级策略? 2. 如何设计有效的健康检查协议? 3. 如何动态更新反向代理配置而不重启应用? 4. 服务熔断机制在反向代理中的实现方法? 5. 如何处理跨微服务的健康状态传播?
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值