1. 概述
中间件就是为了解决实际问题的脚手架,在我们开发微服务过程中,经常会碰到一个问题:不同的微服务有不同的开发人员,在处理 HTTP 返回数据时,格式五花八门,难以统一。
主要问题:
- 只返回请求数据、没有错误码;
- 返回的错误码定义规则五花八门;
- 微服务之间的可视化 traceId 不统一或者没有 traceId;
- 直接抛异常到 response
1.1 需求分析
针对以上问题,我们可以自己实现一个 HTTP 返回格式中间件,统一规范。我们希望这个中间件能实现以下功能:
- API 接口只返回请求数据,不需要自己定义 error_code、error_message
- 自动捕获 API 返回结果,根据返回结果封装统一的返回格式,添加 error_code、error_message
- 根据错误码定义,接口逻辑只需抛出对应错误码的异常,中间件自动捕获异常,封装返回结果
- 自动生成 traceId,并返回 traceId,支持可视化观测
1.2 实现方案
针对 1.1 的需求我们可以大概制定实现方案
-
制定接口返回规范文档。
兵马未动,粮草先行,一个合理的规范文档是方案的关键。定义了 code、message、traceId 三个关键元素,并根据 code 决定是否返回 data
-
根据 code 状态码定义异常类型
2.1 定义异常状态码
public static class AIoTDomainErrorCodes { /* You can add your business exception error codes here, as constants */ public static int DeviceOffline = 50001; public static int DeviceNotFound = 50003; public static int RequestNotExecuted = 50006; public static int HwHttpApiException = 50007; }
2.2 实现异常类型(以 DeviceNotFound 为例)
public class DeviceNotFoundException : AbpException, ICustomException { public int ErrorCode { get; set; } = AIoTDomainErrorCodes.DeviceNotFound; public DeviceNotFoundException() { } public DeviceNotFoundException(string? message) : base(message) { } public DeviceNotFoundException(string? message, Exception? innerException) : base(message, innerException) { } public DeviceNotFoundException(SerializationInfo serializationInfo, StreamingContext context) : base(serializationInfo, context) { } }
-
定义 ApiResponse 中间件 ApiResponseMiddleware
使用 ABP 框架 DOTNET CORE 管道配置手柄 IApplicationBuilder,配置自定义的中间件管道。在 ApiResponse 处理 HttpContext 的返回数据,这部分都是处理无异常返回的,通过解析返回数据 Body 来添加对应 code,message,traceId。步骤如下:
3.1 定义中间件管道 ApiResponseMiddleware
namespace MeiYiJia.AIoT.Middlewares { public class ApiResponseMiddleware { private readonly RequestDelegate _next; private readonly IJsonSerializer _jsonSerializer; private readonly ILogger<ApiResponseMiddleware> _logger; public ApiResponseMiddleware(RequestDelegate next, IJsonSerializer jsonSerializer, ILogger<ApiResponseMiddleware> logger) { _next = next; _jsonSerializer = jsonSerializer; _logger = logger; } public async Task InvokeAsync(HttpContext context) { var path = context.Request.Path.ToString().ToLower(); if ((path.Contains("swagger") //|| context.Request.Path.ToString().ToLower().Contains("login") || path.Contains("monitor") || path.Contains("cap") || path.Contains("hangfire") || path == "/" || path.Contains("/remote") || path.Contains("/abp/") || path.Contains("excel") )) { await _next(context); return; } context.Request.EnableBuffering(); var originalBody = context.Response.Body; try { // 统一处理返回结果 if (path.Contains("/api/") && (context.Response.StatusCode == StatusCodes.Status200OK || context.Response.StatusCode == StatusCodes.Status204NoContent) && IsJsonRequest(context)) { using var ms = new MemoryStream(); context.Response.Body = ms; await _next(context); if (context.Response.StatusCode == StatusCodes.Status204NoContent) context.Response.OnStarting(state => { var httpContext = (HttpContext)state; httpContext.Response.StatusCode = StatusCodes.Status200OK; httpContext.Response.ContentType = "application/json"; return Task.CompletedTask; }, context); var responseStr = GetResponseStr(ms); if (!((responseStr?.StartsWith("{\"code\"") ?? false) && (responseStr?.Contains("\"message\"") ?? false) && (responseStr?.Contains("\"data\"") ?? false))) { var isError = StatusCodes.Status400BadRequest <= context.Response.StatusCode && context.Response.StatusCode <= StatusCodes.Status511NetworkAuthenticationRequired; var responseBody = string.IsNullOrEmpty(responseStr) ? null : TryDeserialize(responseStr); var traceIdProvider = context.RequestServices.GetRequiredService<ITraceIdProvider>(); var traceId = traceIdProvider.Get(); var apiResult = new ApiResult<dynamic>(isError ? context.Response.StatusCode : 0, isError ? "error" : "success", responseBody, traceId); if (responseStr?.Contains("\"isAsync\":true") ?? false) { apiResult.Code = 100; } var json = _jsonSerializer.Serialize(apiResult); ms.Position = 0; ms.Write(Encoding.UTF8.GetBytes(json)); } ms.Position = 0; await ms.CopyToAsync(originalBody); } // 处理非josn 文件类型 else { await _next(context); } } catch (Exception ex) { _logger.LogError($"统一封装返回结构失败!{ex.Message}", ex); } finally { context.Response.Body = originalBody; } } private static bool IsJsonRequest(HttpContext context) { // 获取请求的Content-Type头信息 var contentType = context.Request.ContentType ?? "application/json"; // 如果Content-Type为application/json,则认为是Json请求 return contentType != null && contentType.Contains("application/json", StringComparison.OrdinalIgnoreCase); } private static string? GetResponseStr(MemoryStream ms) { ms.Position = 0; var responseStr = new StreamReader(ms).ReadToEnd(); return responseStr; } private dynamic? TryDeserialize(string? str) { if (string.IsNullOrWhiteSpace(str)) return null; try { return _jsonSerializer.Deserialize<dynamic>(str); } catch (Exception) { return str; } } } }
3.2 中间件管道配置扩展
public static partial class ApplicationBuilderExtensions { /// <summary> /// 统一返回格式 /// </summary> /// <returns></returns> public static IApplicationBuilder UseApiResponse(this IApplicationBuilder app) { return app.UseMiddleware<ApiResponseMiddleware>(); } }
3.3 调用中间件配置
public override void OnApplicationInitialization(ApplicationInitializationContext context) { var app = context.GetApplicationBuilder(); var env = context.GetEnvironment(); if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } app.UseAbpRequestLocalization(); app.UseCorrelationId(); app.UseApiResponse(); app.UseRequestLog(); }
-
定义 HTTP 异常捕获过滤器 ResultExceptionFilter
ResultExceptionFilter.cs
public class ResultExceptionFilter : IAsyncExceptionFilter, ITransientDependency
{
public virtual async Task OnExceptionAsync(ExceptionContext context)
{
if (!ShouldHandleException(context))
{
return;
}
await HandleAndWrapException(context);
}
protected virtual bool ShouldHandleException(ExceptionContext context)
{
//TODO: Create DontWrap attribute to control wrapping..?
if (context.ActionDescriptor.IsControllerAction() &&
context.ActionDescriptor.HasObjectResult())
{
return true;
}
if (context.HttpContext.Request.CanAccept(MimeTypes.Application.Json))
{
return true;
}
if (context.HttpContext.Request.IsAjax())
{
return true;
}
return false;
}
protected virtual async Task HandleAndWrapException(ExceptionContext context)
{
//TODO: Trigger an AbpExceptionHandled event or something like that.
var exceptionHandlingOptions = context.GetRequiredService<IOptions<AbpExceptionHandlingOptions>>().Value;
var exceptionToErrorInfoConverter = context.GetRequiredService<IExceptionToErrorInfoConverter>();
var remoteServiceErrorInfo = exceptionToErrorInfoConverter.Convert(context.Exception, options =>
{
options.SendExceptionsDetailsToClients = exceptionHandlingOptions.SendExceptionsDetailsToClients;
options.SendStackTraceToClients = exceptionHandlingOptions.SendStackTraceToClients;
});
var logLevel = context.Exception.GetLogLevel();
var remoteServiceErrorInfoBuilder = new StringBuilder();
remoteServiceErrorInfoBuilder.AppendLine($"---------- {nameof(RemoteServiceErrorInfo)} ----------");
remoteServiceErrorInfoBuilder.AppendLine(context.GetRequiredService<IJsonSerializer>().Serialize(remoteServiceErrorInfo, indented: true));
var logger = context.GetService<ILogger<ResultExceptionFilter>>(NullLogger<ResultExceptionFilter>.Instance);
logger.LogWithLevel(logLevel, remoteServiceErrorInfoBuilder.ToString());
logger.LogException(context.Exception, logLevel);
await context.GetRequiredService<IExceptionNotifier>().NotifyAsync(new ExceptionNotificationContext(context.Exception));
if (context.Exception is AbpAuthorizationException)
{
await context.HttpContext.RequestServices.GetRequiredService<IAbpAuthorizationExceptionHandler>()
.HandleAsync(context.Exception.As<AbpAuthorizationException>(), context.HttpContext);
}
else
{
context.HttpContext.Response.Headers.Add(AbpHttpConsts.AbpErrorFormat, "true");
var statusCode = (int)context
.GetRequiredService<IHttpExceptionStatusCodeFinder>()
.GetStatusCode(context.HttpContext, context.Exception);
context.HttpContext.Response.StatusCode = statusCode;
var errorCode = 50000;
if (context.Exception is ICustomException)
{
errorCode = (context.Exception as ICustomException)!.ErrorCode;
}
var traceIdProvider = context.GetRequiredService<ITraceIdProvider>();
var traceId = traceIdProvider.Get();
var result = new ApiResult(errorCode, context.Exception.Message, null, traceId, remoteServiceErrorInfo);
context.Result = result;
}
context.Exception = null; //Handled!
}
}
OnExceptionAsync
是异常处理重写方法,包含是否需要处理方法 ShouldHandleException
和具体处理方法 HandleAndWrapException
主要关注异常的具体处理方法中分为两步:
- 授权异常问题处理
context.HttpContext.RequestServices.GetRequiredService<IAbpAuthorizationExceptionHandler>() .HandleAsync(context.Exception.As<AbpAuthorizationException>(), context.HttpContext)
- 业务异常问题处理,我们已定义了若干自定义异常继承接口
ICustomException
通过context.Exception is ICustomException
就能判断是否是自定义异常,并匹配对应的 ErrorCode,最后再使用traceIdProvider
生成 traceId - 服务依赖注入
/// <summary>
/// 异常处理
/// </summary>
/// <param name="context"></param>
private void ConfigureAbpExceptions(ServiceConfigurationContext context)
{
context.Services.AddMvc(options => { options.Filters.Add(typeof(ResultExceptionFilter)); });
}
欢迎来到这里!
我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。
注册 关于