最近一直在读代码整洁之道,我在读到第三章函数的3.9 使用异常替代返回错误码,其实在我的开发经历中都是使用返回错误码给到前端,之前在阅读ABP官网文档中就有看到过使用异常替代异常的做法,当时自己还是比较抵触,在读完本章之后我们就马上阅读了Abp的异常处理源码。
ABP 提供了一个内置的基础设施,并提供了一个标准模型来处理异常。
您自己的大多数异常将是业务异常。该IBusinessException接口用于将异常标记为业务异常。
BusinessExceptionIBusinessException除了IHasErrorCode,IHasErrorDetails和接口之外,还实现了IHasLogLevel接口。
默认日志级别是Warning.
特定业务异常相关的错误代码。例如:
throw new BusinessException(QaErrorCodes.CanNotVoteYourOwnAnswer);QaErrorCodes.CanNotVoteYourOwnAnswer只是一个const string。建议使用以下错误代码格式:
code-namespace是特定于您的模块/应用程序的唯一值。例子:
Volo.Qa:010002
Volo.Qa是这里的代码命名空间。然后将在本地化异常消息时使用代码命名空间。
下面是我们实现一个自定义异常的代码逻辑
[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);ABP 尝试按照以下规则自动确定最适合常见异常类型的 HTTP 状态代码:
IHttpExceptionStatusCodeFinder用于自动确定 HTTP 状态码。默认实现是DefaultHttpExceptionStatusCodeFinder类。它可以根据需要更换或扩展。
自定义映射可以覆盖自动 HTTP 状态代码确定。例如:
services.Configure<AbpExceptionHttpStatusCodeOptions>(options =>{ options.Map("Volo.Qa:010002", HttpStatusCode.Conflict);});下面我们会涉及到处理异常,Abp框架的处理异常给我们提供通知入口ExceptionSubscriber
[ExposeServices(typeof(IExceptionSubscriber))]// 继承IExceptionSubscriber接口,注入周期Transient(瞬态)public abstract class ExceptionSubscriber : IExceptionSubscriber, ITransientDependency{ public abstract Task HandleAsync(ExceptionNotificationContext context);}我们只需要继承ExceptionSubscriber抽象类,然后Abp将自动注入,一对多的形式进行注入。
触发通知的代码在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继承我们的异常拦截器,依赖注入的生命周期是瞬态的public class AbpExceptionFilter : IAsyncExceptionFilter, ITransientDependency{ ·····省略代码}AbpExceptionFilter如果满足以下任何条件,则处理异常:
如果异常得到处理,它会自动记录下来,并将格式化的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! }