• Abp 业务异常源码解读


    Abp 业务异常源码解读

    最近一直在读代码整洁之道,我在读到第三章函数的3.9 使用异常替代返回错误码,其实在我的开发经历中都是使用返回错误码给到前端,之前在阅读ABP官网文档中就有看到过使用异常替代异常的做法,当时自己还是比较抵触,在读完本章之后我们就马上阅读了Abp的异常处理源码


    ABP 提供了一个内置的基础设施,并提供了一个标准模型来处理异常。

    • 自动处理所有异常并向客户端发送标准格式的错误消息以获取 API/AJAX 请求。
    • 自动隐藏内部基础架构错误并返回标准错误消息。
    • 提供一种简单且可配置的方式来本地化异常消息,可以实现多语言返回。
    • 自动将标准异常映射到HTTP 状态代码,并提供一个可配置的选项来映射自定义异常。

    业务异常#

    您自己的大多数异常将是业务异常。该IBusinessException接口用于将异常标记为业务异常。
    BusinessExceptionIBusinessException除了IHasErrorCode,IHasErrorDetails和接口之外,还实现了IHasLogLevel接口。
    默认日志级别是Warning.
    特定业务异常相关的错误代码。例如:

    throw new BusinessException(QaErrorCodes.CanNotVoteYourOwnAnswer);
    

    QaErrorCodes.CanNotVoteYourOwnAnswer只是一个const string。建议使用以下错误代码格式:

    code-namespace是特定于您的模块/应用程序的唯一值。例子:
    Volo.Qa:010002
    Volo.Qa是这里的代码命名空间。然后将在本地化异常消息时使用代码命名空间。

    • 您可以在需要时直接抛出BusinessException派生您自己的异常类型。
    • 该类的所有属性都是可选的BusinessException。但是您通常设置ErrorCodeor Message属性。

    BusinessException(自定义的业务异常)#

    下面是我们实现一个自定义异常的代码逻辑

    [Serializable]
    // 继承异常Exception类(实现自定义异常)
    // IBusinessException (标识业务异常)
    // IHasErrorCode(实现Code字段)
    // IHasErrorDetails(实现Details字段)
    // IHasLogLevel(当前异常实现自定义日志等级)
    public class BusinessException : Exception,
        IBusinessException,
        IHasErrorCode,
        IHasErrorDetails,
        IHasLogLevel
    {
        public string Code { get; set; }
    
        public string Details { get; set; }
    
        public LogLevel LogLevel { get; set; }
    
        public BusinessException(
            string code = null,
            string message = null,
            string details = null,
            Exception innerException = null,
            LogLevel logLevel = LogLevel.Warning)
            : base(message, innerException)
        {
            Code = code;
            Details = details;
            LogLevel = logLevel;
        }
    
        /// <summary>
        /// Constructor for serializing.
        /// </summary>
        public BusinessException(SerializationInfo serializationInfo, StreamingContext context)
            : base(serializationInfo, context)
        {
    
        }
    
        public BusinessException WithData(string name, object value)
        {
            Data[name] = value;
            return this;
        }
    }
    

    本地化资源(实现多语言)#

    不知道大家没有接触过Abp的多语言设计,Abp通过读取不同国家的语言包Json实现多语言设计

    这个是Abp源码中使用多语言的案例,可以看到我们会统一定义一个文件夹保存不同国家的多语言Json

    多语言Json结构案例:

    culture是语言

    texts是Key-Value

    {
      "culture": "zh-Hans",
      "texts": {
        "Volo.Abp.Http.DynamicProxying:10001": "业务异常"
      }
    }
    

    然后在模块中将语言包文件夹中的Json,添加到本地化中

            Configure<AbpLocalizationOptions>(options =>
            {
                options.Resources
                    .Add<HttpClientTestResource>("en")
                    .AddVirtualJson("/Volo/Abp/Http/Localization");
            });
    

    设置异常本地化配置(不同的解决方案一定要进行注册,如果没注册就找不到对应的错误码Key)

            Configure<AbpExceptionLocalizationOptions>(options =>
            {
                // 设置映射解决方案名称,因为考虑到不同的语言包,需要区分模块设计
                options.MapCodeNamespace("Volo.Abp.Http.DynamicProxying", typeof(HttpClientTestResource));
            });
    

    结构如下:
    我们的Key可以通过解决方案加Code的方式(Volo.Abp.Http.DynamicProxying为解决方案:10001是返回给前端的错误Code)

    {
      "culture": "sl",
      "texts": {
        "Volo.Abp.Http.DynamicProxying:10001": "Poslovna izjema s podatki",
        "Volo.Abp.Http.TestProxying:10002": "Poslovna izjema s podatki"
      }
    }
    

    然后可以使用错误代码抛出业务异常:

    // QaDomainErrorCodes.CanNotVoteYourOwnAnswer="Volo.Abp.Http.DynamicProxying:10001"
    // 这样通过一个常量管理异常就简洁明了。
    throw new BusinessException(QaDomainErrorCodes.CanNotVoteYourOwnAnswer);
    

    HTTP 状态码映射#

    ABP 尝试按照以下规则自动确定最适合常见异常类型的 HTTP 状态代码:

    • 对于AbpAuthorizationException:
      • 401如果用户尚未登录,则返回(未经授权)。
      • 如果用户已登录,则返回403(禁止)。
    • 的返回400(错误请求)AbpValidationException。
    • 返回404(未找到)EntityNotFoundException。
    • (并且因为它扩展了)返回403(禁止)。IBusinessExceptionIUserFriendlyExceptionIBusinessException
    • 的返回501(未实现)NotImplementedException。
    • 500其他异常(假定为基础设施异常)的返回(内部服务器错误)。

    IHttpExceptionStatusCodeFinder用于自动确定 HTTP 状态码。默认实现是DefaultHttpExceptionStatusCodeFinder类。它可以根据需要更换或扩展。

    自定义映射#

    自定义映射可以覆盖自动 HTTP 状态代码确定。例如:

    services.Configure<AbpExceptionHttpStatusCodeOptions>(options =>
    {
        options.Map("Volo.Qa:010002", HttpStatusCode.Conflict);
    });
    

    异常事件订阅(ExceptionSubscriber)#

    下面我们会涉及到处理异常,Abp框架的处理异常给我们提供通知入口ExceptionSubscriber

    [ExposeServices(typeof(IExceptionSubscriber))]
    // 继承IExceptionSubscriber接口,注入周期Transient(瞬态)
    public abstract class ExceptionSubscriber : IExceptionSubscriber, ITransientDependency
    {
        public abstract Task HandleAsync(ExceptionNotificationContext context);
    }
    

    我们只需要继承ExceptionSubscriber抽象类,然后Abp将自动注入,一对多的形式进行注入。
    触发通知的代码在ExceptionNotifier源码

    ExceptionNotifier(异常通知)#

    下面的代码就是实现异常通知发生事件的代码,我们只需要在异常过滤器中获取ExceptionNotifier然后调用NotifyAsync方法就可以啦

    // 异常通知
    public class ExceptionNotifier : IExceptionNotifier, ITransientDependency
    {
        public ILogger<ExceptionNotifier> Logger { get; set; }
    
        protected IServiceScopeFactory ServiceScopeFactory { get; }
    
        public ExceptionNotifier(IServiceScopeFactory serviceScopeFactory)
        {
            ServiceScopeFactory = serviceScopeFactory;
            Logger = NullLogger<ExceptionNotifier>.Instance;
        }
    
        // 通知入口
        public virtual async Task NotifyAsync([NotNull] ExceptionNotificationContext context)
        {
            Check.NotNull(context, nameof(context));
    
            using (var scope = ServiceScopeFactory.CreateScope())
            {
                // 1.获取所有实现IExceptionSubscriber接口的实现了类
                var exceptionSubscribers = scope.ServiceProvider
                    .GetServices<IExceptionSubscriber>();
                // 2.批量调用实现类的HandleAsync方法
                foreach (var exceptionSubscriber in exceptionSubscribers)
                {
                    try
                    {
                        await exceptionSubscriber.HandleAsync(context);
                    }
                    catch (Exception e)
                    {
                        Logger.LogWarning($"Exception subscriber of type {exceptionSubscriber.GetType().AssemblyQualifiedName} has thrown an exception!");
                        Logger.LogException(e, LogLevel.Warning);
                    }
                }
            }
        }
    }
    

    AbpExceptionFilter异常拦截器源码#

    我们首先可以看到AbpExceptionFilter继承我们的异常拦截器,依赖注入的生命周期是瞬态的

    // 我们首先可以看到AbpExceptionFilter继承我们的异常拦截器,依赖注入的生命周期是瞬态的
    public class AbpExceptionFilter : IAsyncExceptionFilter, ITransientDependency
    {
       ·····省略代码
    }
    
    

    AbpExceptionFilter如果满足以下任何条件,则处理异常:

    • 异常由返回对象结果(不是视图结果)的控制器操作引发。
    • 该请求是一个 AJAX 请求(X-Requested-WithHTTP 标头值为XMLHttpRequest)。
    • 客户端明确接受application/json内容类型(通过acceptHTTP 标头)。

    如果异常得到处理,它会自动记录下来,并将格式化的JSON 消息返回给客户端。

       // 判断当前请求的异常是否需要自动处理
        protected virtual bool ShouldHandleException(ExceptionContext context)
        {
            // 1.判断当前请求是否是控制器方法
            // 2.并且有返回结果
            if (context.ActionDescriptor.IsControllerAction() &&
                context.ActionDescriptor.HasObjectResult())
            {
                return true;
            }
            // 1.当前请求中头accept是否是application/json内容类型
            if (context.HttpContext.Request.CanAccept(MimeTypes.Application.Json))
            {
                return true;
            }
            // 1.当前请求是否是AJAX 请求
            if (context.HttpContext.Request.IsAjax())
            {
                return true;
            }
    
            return false;
        }
    

    如果ShouldHandleException()方法返回 true就会进入HandleAndWrapException() 自动格式化处理异常方法

        // 自动格式化处理异常
        protected virtual async Task HandleAndWrapException(ExceptionContext context)
        {
            //TODO: Trigger an AbpExceptionHandled event or something like that.
            // 1.首先还是老样子读取当前模块的配置信息
            var exceptionHandlingOptions = context.GetRequiredService<IOptions<AbpExceptionHandlingOptions>>().Value;
            // 2.获取异常格式转换器,因为需要将我们的异常格式化,多语言实现也是在这个格式化转换器中实现的
            var exceptionToErrorInfoConverter = context.GetRequiredService<IExceptionToErrorInfoConverter>();
            // 3.通过格式化转换器,将异常信息转换成为前端展示数据(这里就会使用到我们的配置信息)
            var remoteServiceErrorInfo = exceptionToErrorInfoConverter.Convert(context.Exception, options =>
           {
               // 是否向客户端发送异常详细信息(默认是false)
               options.SendExceptionsDetailsToClients = exceptionHandlingOptions.SendExceptionsDetailsToClients;
               // 发送堆栈跟踪到客户端(默认是true)
               options.SendStackTraceToClients = exceptionHandlingOptions.SendStackTraceToClients;
           });
            // 4.获取我们业务异常日志等级
            var logLevel = context.Exception.GetLogLevel();
            // 5.创建一个StringBuilder对象拼接异常信息
            var remoteServiceErrorInfoBuilder = new StringBuilder();
            remoteServiceErrorInfoBuilder.AppendLine($"---------- {nameof(RemoteServiceErrorInfo)} ----------");
            remoteServiceErrorInfoBuilder.AppendLine(context.GetRequiredService<IJsonSerializer>().Serialize(remoteServiceErrorInfo, indented: true));
            
            // 6.获取日志信息
            var logger = context.GetService<ILogger<AbpExceptionFilter>>(NullLogger<AbpExceptionFilter>.Instance);
            
            logger.LogWithLevel(logLevel, remoteServiceErrorInfoBuilder.ToString());
    
            logger.LogException(context.Exception, logLevel);
    
            // 7.获取注入IExceptionNotifier接口的实现类,给IExceptionSubscriber实现类接口批量发送事件
            await context.GetRequiredService<IExceptionNotifier>().NotifyAsync(new ExceptionNotificationContext(context.Exception));
            
            // 8.判断当前异常是不是身份认证异常
            if (context.Exception is AbpAuthorizationException)
            {
                await context.HttpContext.RequestServices.GetRequiredService<IAbpAuthorizationExceptionHandler>()
                    .HandleAsync(context.Exception.As<AbpAuthorizationException>(), context.HttpContext);
            }
            else
            {
                // 9.添加请求头标识_AbpErrorFormat(给告诉调用者,这次的异常已经是被我们格式化的)
                context.HttpContext.Response.Headers.Add(AbpHttpConsts.AbpErrorFormat, "true");
                // 10.设置返回状态码
                context.HttpContext.Response.StatusCode = (int)context
                    .GetRequiredService<IHttpExceptionStatusCodeFinder>()
                    .GetStatusCode(context.HttpContext, context.Exception);
                // 11.将我们序列化好的错误信息放入请求返回结果中
                context.Result = new ObjectResult(new RemoteServiceErrorResponse(remoteServiceErrorInfo));
            }
            // 12.清空当前请求的异常
            context.Exception = null; //Handled!
        }
    

    参考资料#

  • 相关阅读:
    1. 深度学习——激活函数
    LeetCode刷题(8)
    Nginx学习与使用
    升级到 MySQL 8.4,MySQL 启动报错:io_setup() failed with EAGAIN
    壳聚糖-凝集素|Chitosan-Lectins|凝集素-PEG-壳聚糖|壳聚糖-聚乙二醇-凝集素
    Zabbix
    vue学习记录
    介绍Vue router的history模式以及如何配置history模式
    分布式文件系统HDFS-2
    《猎杀:对决》是适合什么样的人玩 Mac电脑怎么玩《猎杀:对决》
  • 原文地址:https://www.cnblogs.com/chenxi001/p/15942776.html