Util應用框架基礎(五) - 異常處理

何鎮汐發表於2023-11-07

本節介紹Util應用框架如何處理系統錯誤.

概述

系統在執行過程中可能發生錯誤.

系統錯誤可以簡單分為兩類:

  • 系統異常

    系統本身出現的錯誤.

  • 業務異常

    不滿足業務規則出現的錯誤.

如何處理系統異常

如果發生系統異常,大多數情況下,你除了記錄異常日誌外,可能無法處理它們.

一個例外是併發異常.

當發生併發異常,可以透過重試再次提交,有可能成功處理.

另外一個問題,是否應該將系統異常訊息返回給客戶端?

系統異常訊息與技術相關,客戶無法理解它們.

而且系統異常訊息可能包含敏感資訊,返回給客戶端可能更易受到攻擊.

如何處理業務異常

業務異常表明沒有滿足某些業務規則,通常也無法自動處理.

如果能夠自動處理的業務異常,應定義專用異常型別.

對於業務異常,除了記錄異常日誌外,還應把業務異常訊息返回給客戶端,以指示使用者調整操作.

基礎用法

.Net 使用異常 Exception 及派生異常來處理系統異常,但沒有明確規定處理業務異常的型別.

Warning 業務異常

Util應用框架定義了 Util.Exceptions.Warning 異常型別,Warning 從 Exception 派生,代表業務異常.

當你丟擲 Exception 或派生異常型別時,異常訊息僅在開發階段返回給客戶端.

一旦釋出到生產環境,系統異常訊息將被遮蔽,客戶端收到訊息: 系統忙,請稍後再試 .

throw new Exception( "未將物件引用設定到物件的例項" );

對於業務規則導致的錯誤,你需要丟擲 Warning 異常.

Warning 丟擲的異常訊息將返回到客戶端,提示使用者進行修改.

throw new Warning( "必須填寫姓名" );

GetMessage 工具方法

Warning 除了代表業務異常外,還提供了一個靜態工具方法 GetMessage.

異常可能被其它異常包裹,要獲得異常真正的訊息,需要使用遞迴.

Warning.GetMessage 工具方法傳入異常例項,遞迴獲取異常訊息,

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 方法使用遞迴獲取內部異常訊息.

/// <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 表示併發異常,統一包裝其它元件產生的併發異常,並處理異常訊息.

/// <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] 錯誤日誌過濾器記錄全域性異常日誌.

/// <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] 過濾器還對異常訊息的本地化進行了處理.

/// <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;
    }
}

相關文章