本節介紹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;
}
}