Util应用框架基础(五) - 异常处理

本节介绍Util应用框架如何处理系统错误.

概述

系统在运行过程中可能发生错误.

系统错误可以简单分为两类:

  • 系统异常

    系统本身出现的错误.

  • 业务异常

    不满足业务规则出现的错误.

如何处理系统异常

如果发生系统异常,大多数情况下,你除了记录异常日志外,可能无法处理它们.

一个例外是并发异常.

当发生并发异常,可以通过重试再次提交,有可能成功处理.

另外一个问题,是否应该将系统异常消息返回给客户端?

系统异常消息与技术相关,客户无法理解它们.

而且系统异常消息可能包含敏感信息,返回给客户端可能更易受到攻击.

如何处理业务异常

业务异常表明没有满足某些业务规则,通常也无法自动处理.

如果能够自动处理的业务异常,应定义专用异常类型.

对于业务异常,除了记录异常日志外,还应把业务异常消息返回给客户端,以指示用户调整操作.

基础用法

.Net 使用异常 Exception 及派生异常来处理系统异常,但没有明确规定处理业务异常的类型.

Warning 业务异常

Util应用框架定义了 Util.Exceptions.Warning 异常类型,Warning 从 Exception 派生,代表业务异常.

当你抛出 Exception 或派生异常类型时,异常消息仅在开发阶段返回给客户端.

一旦发布到生产环境,系统异常消息将被屏蔽,客户端收到消息: 系统忙,请稍后再试 .

c# 复制代码
throw new Exception( "未将对象引用设置到对象的实例" );

对于业务规则导致的错误,你需要抛出 Warning 异常.

Warning 抛出的异常消息将返回到客户端,提示用户进行修改.

c# 复制代码
throw new Warning( "必须填写姓名" );

GetMessage 工具方法

Warning 除了代表业务异常外,还提供了一个静态工具方法 GetMessage.

异常可能被其它异常包裹,要获得异常真正的消息,需要使用递归.

Warning.GetMessage 工具方法传入异常实例,递归获取异常消息,

c# 复制代码
var message = Warning.GetMessage( exception );

ConcurrencyException 并发异常

Util应用框架定义了并发异常 Util.Exceptions.ConcurrencyException.

不同的 .Net 组件抛出的并发异常类型可能不同, Util使用 ConcurrencyException 进行统一包装.

可以通过重试的方式来解决并发异常.

下面是Util应用框架Dapr集成事件增加计数时并发处理的代码片断.

复制代码
public virtual async Task IncrementAsync( CancellationToken cancellationToken = default ) {
    try {
        await Store.IncrementAsync( cancellationToken );
    }
    catch ( ConcurrencyException ) {
        Log.LogDebug( "更新集成事件计数出现并发异常,即将重试" );
        await IncrementAsync( cancellationToken );
    }
    catch ( Exception exception ) {
        Log.LogError( exception, "更新集成事件计数失败" );
    }
}

全局错误日志记录

Util应用框架使用 ErrorLogFilterAttribute 过滤器来记录全局错误日志.

已在 Web Api控制器基类 WebApiControllerBase 设置 ErrorLogFilter 过滤器.

全局异常处理

Util应用框架使用 ExceptionHandlerAttribute 过滤器来处理全局异常.

已在 Web Api控制器基类 WebApiControllerBase 设置 ExceptionHandler 过滤器.

ExceptionHandler 过滤器对异常消息进行处理,只有 Warning 异常消息才会返回给客户端.

源码解析

Warning 业务异常

Warning 代表业务异常,它的异常消息会返回给客户端.

GetMessage 方法使用递归获取内部异常消息.

c# 复制代码
/// <summary>
/// 应用程序异常
/// </summary>
public class Warning : Exception {
    /// <summary>
    /// 初始化应用程序异常
    /// </summary>
    /// <param name="exception">异常</param>
    public Warning( Exception exception )
        : this( null, exception ) {
    }

    /// <summary>
    /// 初始化应用程序异常
    /// </summary>
    /// <param name="message">错误消息</param>
    /// <param name="exception">异常</param>
    /// <param name="code">错误码</param>
    /// <param name="httpStatusCode">Http状态码</param>
    public Warning( string message, Exception exception = null, string code = null, int? httpStatusCode = null )
        : base( message ?? "", exception ) {
        Code = code;
        HttpStatusCode = httpStatusCode;
        IsLocalization = true;
    }

    /// <summary>
    /// 错误码
    /// </summary>
    public string Code { get; set; }

    /// <summary>
    /// Http状态码
    /// </summary>
    public int? HttpStatusCode { get; set; }

    /// <summary>
    /// 是否本地化异常消息
    /// </summary>
    public bool IsLocalization { get; set; }

    /// <summary>
    /// 获取错误消息
    /// </summary>
    /// <param name="isProduction">是否生产环境</param>
    public virtual string GetMessage( bool isProduction = false ) {
        return GetMessage( this );
    }

    /// <summary>
    /// 获取错误消息
    /// </summary>
    public static string GetMessage( Exception ex ) {
        var result = new StringBuilder();
        var list = GetExceptions( ex );
        foreach( var exception in list )
            AppendMessage( result, exception );
        return result.ToString().Trim( Environment.NewLine.ToCharArray() );
    }

    /// <summary>
    /// 添加异常消息
    /// </summary>
    private static void AppendMessage( StringBuilder result, Exception exception ) {
        if( exception == null )
            return;
        result.AppendLine( exception.Message );
    }

    /// <summary>
    /// 获取异常列表
    /// </summary>
    public IList<Exception> GetExceptions() {
        return GetExceptions( this );
    }

    /// <summary>
    /// 获取异常列表
    /// </summary>
    /// <param name="ex">异常</param>
    public static IList<Exception> GetExceptions( Exception ex ) {
        var result = new List<Exception>();
        AddException( result, ex );
        return result;
    }

    /// <summary>
    /// 添加内部异常
    /// </summary>
    private static void AddException( List<Exception> result, Exception exception ) {
        if( exception == null )
            return;
        result.Add( exception );
        AddException( result, exception.InnerException );
    }
}

ConcurrencyException 并发异常

ConcurrencyException 表示并发异常,统一包装其它组件产生的并发异常,并处理异常消息.

c# 复制代码
/// <summary>
/// 并发异常
/// </summary>
public class ConcurrencyException : Warning {
    /// <summary>
    /// 消息
    /// </summary>
    private readonly string _message;

    /// <summary>
    /// 初始化并发异常
    /// </summary>
    public ConcurrencyException()
        : this( "" ) {
    }

    /// <summary>
    /// 初始化并发异常
    /// </summary>
    /// <param name="exception">异常</param>
    public ConcurrencyException( Exception exception )
        : this( "", exception ) {
    }

    /// <summary>
    /// 初始化并发异常
    /// </summary>
    /// <param name="message">错误消息</param>
    /// <param name="exception">异常</param>
    /// <param name="code">错误码</param>
    /// <param name="httpStatusCode">Http状态码</param>
    public ConcurrencyException( string message, Exception exception = null, string code = null, int? httpStatusCode = null )
        : base( message, exception, code, httpStatusCode ) {
        _message = message;
    }

    /// <inheritdoc />
    public override string Message => $"{R.ConcurrencyExceptionMessage}.{_message}";

    /// <inheritdoc />
    public override string GetMessage( bool isProduction = false ) {
        if( isProduction )
            return R.ConcurrencyExceptionMessage;
        return GetMessage(this);
    }
}

ErrorLogFilterAttribute 错误日志过滤器

ErrorLogFilter 错误日志过滤器记录全局异常日志.

c# 复制代码
/// <summary>
/// 错误日志过滤器
/// </summary>
public class ErrorLogFilterAttribute : ExceptionFilterAttribute {
    /// <summary>
    /// 异常处理
    /// </summary>
    public override void OnException( ExceptionContext context ) {
        if( context == null )
            return;
        var log = context.HttpContext.RequestServices.GetService<ILogger<ErrorLogFilterAttribute>>();
        var exception = context.Exception.GetRawException();
        if( exception is Warning warning ) {
            log.LogWarning( warning, exception.Message );
            return;
        }
        log.LogError( exception, exception.Message );
    }
}

ExceptionHandlerAttribute 异常处理过滤器

ExceptionHandler 过滤器处理全局异常.

Exception 的扩展方法 GetPrompt 获取客户端友好的异常消息.

对于生产环境, Exception 异常消息将被替换为 系统忙,请稍后再试.

ExceptionHandler 过滤器还对异常消息的本地化进行了处理.

c# 复制代码
/// <summary>
/// 异常处理过滤器
/// </summary>
public class ExceptionHandlerAttribute : ExceptionFilterAttribute {
    /// <summary>
    /// 异常处理
    /// </summary>
    public override void OnException( ExceptionContext context ) {
        context.ExceptionHandled = true;
        var message = context.Exception.GetPrompt( Web.Environment.IsProduction() );
        message = GetLocalizedMessages( context, message );
        var errorCode = context.Exception.GetErrorCode() ?? StateCode.Fail;
        var httpStatusCode = context.Exception.GetHttpStatusCode() ?? 200;
        context.Result = GetResult( context, errorCode, message, httpStatusCode );
    }

    /// <summary>
    /// 获取本地化异常消息
    /// </summary>
    protected virtual string GetLocalizedMessages( ExceptionContext context, string message ) {
        var exception = context.Exception.GetRawException();
        if ( exception is Warning { IsLocalization: false } ) 
            return message;
        var stringLocalizerFactory = context.HttpContext.RequestServices.GetService<IStringLocalizerFactory>();
        if ( stringLocalizerFactory == null )
            return message;
        var stringLocalizer = stringLocalizerFactory.Create( "Warning",null );
        var localizedString = stringLocalizer[message];
        if ( localizedString.ResourceNotFound == false )
            return localizedString.Value;
        stringLocalizer = context.HttpContext.RequestServices.GetService<IStringLocalizer>();
        if ( stringLocalizer == null )
            return message;
        return stringLocalizer[message];
    }

    /// <summary>
    /// 获取结果
    /// </summary>
    protected virtual IActionResult GetResult( ExceptionContext context, string code, string message, int? httpStatusCode ) {
        var options = GetJsonSerializerOptions( context );
        var resultFactory = context.HttpContext.RequestServices.GetService<IResultFactory>();
        if ( resultFactory == null )
            return new Result( code, message, null, httpStatusCode, options );
        return resultFactory.CreateResult( code, message, null, httpStatusCode, options );
    }

    /// <summary>
    /// 获取Json序列化配置
    /// </summary>
    private JsonSerializerOptions GetJsonSerializerOptions( ExceptionContext context ) {
        var factory = context.HttpContext.RequestServices.GetService<IJsonSerializerOptionsFactory>();
        if( factory != null )
            return factory.CreateOptions();
        return new JsonSerializerOptions {
            PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
            Encoder = JavaScriptEncoder.Create( UnicodeRanges.All ),
            DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
            Converters = {
                new DateTimeJsonConverter(),
                new NullableDateTimeJsonConverter()
            }
        };
    }
}

/// <summary>
/// 异常扩展
/// </summary>
public static class ExceptionExtensions {
    /// <summary>
    /// 获取异常提示
    /// </summary>
    /// <param name="exception">异常</param>
    /// <param name="isProduction">是否生产环境</param>
    public static string GetPrompt( this Exception exception, bool isProduction = false ) {
        if( exception == null )
            return null;
        exception = exception.GetRawException();
        if( exception == null )
            return null;
        if( exception is Warning warning )
            return warning.GetMessage( isProduction );
        return isProduction ? R.SystemError : exception.Message;
    }

    /// <summary>
    /// 获取Http状态码
    /// </summary>
    /// <param name="exception">异常</param>
    public static int? GetHttpStatusCode( this Exception exception ) {
        if ( exception == null )
            return null;
        exception = exception.GetRawException();
        if ( exception == null )
            return null;
        if ( exception is Warning warning )
            return warning.HttpStatusCode;
        return null;
    }

    /// <summary>
    /// 获取错误码
    /// </summary>
    /// <param name="exception">异常</param>
    public static string GetErrorCode( this Exception exception ) {
        if ( exception == null )
            return null;
        exception = exception.GetRawException();
        if ( exception == null )
            return null;
        if ( exception is Warning warning )
            return warning.Code;
        return null;
    }
}