在業務系統,異常處理是所有開發人員必須面對的問題,在一定程度上,異常處理的能力反映出開發者對業務的駕馭水平;本章將著重介紹如何在 WebApi 程式中對異常進行捕獲,然後利用 Nlog 組件進行記錄;同時,還將介紹兩種不同的 異常捕獲方式:管道捕獲/服務過濾;通過本練習,將學習到如何捕獲異常、處理異... ...
前言
在業務系統,異常處理是所有開發人員必須面對的問題,在一定程度上,異常處理的能力反映出開發者對業務的駕馭水平;本章將著重介紹如何在 WebApi 程式中對異常進行捕獲,然後利用 Nlog 組件進行記錄;同時,還將介紹兩種不同的
異常捕獲方式:管道捕獲/服務過濾;通過本練習,將學習到如何捕獲異常、處理異常跳轉、記錄異常信息。
搭建框架
首先,創建一個 WebApi 項目,選擇 Asp.Net Core Web 應用程式;
- 進一步選擇 Api 模板,這裡使用的 .netcore 版本為 2.1
- 取消勾選 “啟用 Docker 支持(E)” 和 “為 Https 配置(C)”,點擊確定,得到一個完整的 WebApi 項目框架,如圖
- 直接按 F5 運行項目,一切正常,程式啟動後進入預設路由調用,並輸出結果
異常路由
- 一切看起來都非常正常和美好,但,禍之福所倚;接下來我們在 介面 Get() 中人為的製造一點麻煩。
[HttpGet]
public ActionResult<IEnumerable<string>> Get()
{
throw new Exception("出錯了.....");
return new string[] { "value1", "value2" };
}
- 這是由於項目配置了運行環境變數 ASPNETCORE_ENVIRONMENT=Development 後,Startup.cs 中配置了開發環境下,使用系統預設頁,所以我們才可以看到上面的異常信息
- 如果你把環境變數設置為 ASPNETCORE_ENVIRONMENT=Production ,你會發現,在異常發生的時候,你得到了一個空白頁。
異常處理方式一:服務過濾
在傳統的 Asp.Net MVC 應用程式中,我們一般都使用服務過濾的方式去捕獲和處理異常,這種方式非常常見,而且可用性來說,體驗也不錯,幸運的是 Asp.Net Core 也完整的支持該方式,接下來創建一個全局異常處理類 CustomerExceptionFilter
public class CustomerExceptionFilter : Attribute, IExceptionFilter
{
private readonly ILogger logger = null;
private readonly IHostingEnvironment environment = null;
public CustomerExceptionFilter(ILogger<CustomerExceptionFilter> logger, IHostingEnvironment environment)
{
this.logger = logger;
this.environment = environment;
}
public void OnException(ExceptionContext context)
{
Exception exception = context.Exception;
string error = string.Empty;
void ReadException(Exception ex)
{
error += string.Format("{0} | {1} | {2}", ex.Message, ex.StackTrace, ex.InnerException);
if (ex.InnerException != null)
{
ReadException(ex.InnerException);
}
}
ReadException(context.Exception);
logger.LogError(error);
ContentResult result = new ContentResult
{
StatusCode = 500,
ContentType = "text/json;charset=utf-8;"
};
if (environment.IsDevelopment())
{
var json = new { message = exception.Message, detail = error };
result.Content = JsonConvert.SerializeObject(json);
}
else
{
result.Content = "抱歉,出錯了";
}
context.Result = result;
context.ExceptionHandled = true;
}
}
- CustomerExceptionFilter 繼承自 IExceptionFilter 介面,並實現 void OnException(ExceptionContext context) 方法,在 CustomerExceptionFilter
構造方法中,定義了兩個參數,用於記錄異常日誌和獲取程式運行環境變數
private readonly ILogger logger = null;
private readonly IHostingEnvironment environment = null;
public CustomerExceptionFilter(ILogger<CustomerExceptionFilter> logger, IHostingEnvironment environment)
{
this.logger = logger;
this.environment = environment;
}
- 在接下來的 OnException 方法中,利用 environment 進行產品環境的判斷,並使用 logger 將日誌寫入硬碟文件中,為了將日誌寫入硬碟,
需要引用 Nuget 包 NLog.Extensions.Logging/NLog.Web.AspNetCore ,併在 Startup.cs 文件的 Configure 方法中添加擴展
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory factory)
{
// 將 NLog
factory.AddConsole(Configuration.GetSection("Logging"))
.AddNLog()
.AddDebug();
var nlogFile = System.IO.Path.Combine(env.ContentRootPath, "nlog.config");
env.ConfigureNLog(nlogFile);
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseMvc();
}
- 上面的代碼讀取了配置文件 nlog.config 並設置為 NLog 的配置,該文件定義如下
<?xml version="1.0" encoding="utf-8" ?>
<nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" autoReload="true" internalLogLevel="info">
<!-- Load the ASP.NET Core plugin -->
<extensions>
<add assembly="NLog.Web.AspNetCore"/>
</extensions>
<!-- Layout: https://github.com/NLog/NLog/wiki/Layout%20Renderers -->
<targets>
<target xsi:type="File" name="errorfile" fileName="/data/logs/logfilter/error-${shortdate}.log" layout="${longdate}|${logger}|${uppercase:${level}}| ${message} ${exception}|${aspnet-Request-Url}" />
<target xsi:type="Null" name="blackhole" />
</targets>
<rules>
<logger name="Microsoft.*" minlevel="Error" writeTo="blackhole" final="true" />
<logger name="*" minlevel="Error" writeTo="errorfile" />
</rules>
</nlog>
- 為了在 WebApi 控制器中使用 CustomerExceptionFilter 過濾器,我們還需要在 Startup.cs 將 CustomerExceptionFilter 註入到容器中
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
// 將異常過濾器註入到容器中
services.AddScoped<CustomerExceptionFilter>();
services.AddMvc()
.SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
}
- 最後,在控制器 ValuesController 中應用該異常過濾器
[ServiceFilter(typeof(CustomerExceptionFilter))]
[Route("api/[controller]"), ApiController]
public class ValuesController : ControllerBase
{
// GET api/values
[HttpGet]
public ActionResult<IEnumerable<string>> Get()
{
throw new Exception("出錯了.....");
return new string[] { "value1", "value2" };
}
}
- 現在,按 F5 啟動程式,如預期所料,報錯信息被 CustomerExceptionFilter 捕獲,並轉換為 json 格式輸出
- 同時,NLog 組件也將日誌信息記錄到了硬碟中
異常處理方式二:中間件捕獲
接下來利用 .NetCore 的管道模式,在中間件中對異常進行捕獲,首先,創建一個中間件
public class ExceptionMiddleware
{
private readonly RequestDelegate next;
private readonly ILogger logger;
private IHostingEnvironment environment;
public ExceptionMiddleware(RequestDelegate next, ILogger<ExceptionMiddleware> logger, IHostingEnvironment environment)
{
this.next = next;
this.logger = logger;
this.environment = environment;
}
public async Task Invoke(HttpContext context)
{
try
{
await next.Invoke(context);
var features = context.Features;
}
catch (Exception e)
{
await HandleException(context, e);
}
}
private async Task HandleException(HttpContext context, Exception e)
{
context.Response.StatusCode = 500;
context.Response.ContentType = "text/json;charset=utf-8;";
string error = "";
void ReadException(Exception ex)
{
error += string.Format("{0} | {1} | {2}", ex.Message, ex.StackTrace, ex.InnerException);
if (ex.InnerException != null)
{
ReadException(ex.InnerException);
}
}
ReadException(e);
if (environment.IsDevelopment())
{
var json = new { message = e.Message, detail = error };
error = JsonConvert.SerializeObject(json);
}
else
error = "抱歉,出錯了";
await context.Response.WriteAsync(error);
}
}
- 代碼比較簡單,在管道中使用 try/catch 進行捕獲異常,創建 HandleException(HttpContext context, Exception e) 處理異常,判斷是 Development 環境下,輸出詳細的錯誤信息,非 Development 環境僅提示調用者“抱歉,出錯了”,同時使用 NLog 組件將日誌寫入硬碟;
同樣,在 Startup.cs 中將 ExceptionMiddleware 加入管道中
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory factory)
{
// 將 NLog
factory.AddConsole(Configuration.GetSection("Logging"))
.AddNLog()
.AddDebug();
var nlogFile = System.IO.Path.Combine(env.ContentRootPath, "nlog.config");
env.ConfigureNLog(nlogFile);
// ExceptionMiddleware 加入管道
app.UseMiddleware<ExceptionMiddleware>();
//if (env.IsDevelopment())
//{
// app.UseDeveloperExceptionPage();
//}
app.UseMvc();
}
- 一切就緒,按 F5 運行程式,網頁中輸出了期望中的 json 格式錯誤信息,同時 NLog 組件也將日誌寫入了硬碟
結語
在本例中,通過依賴註入和管道中間件的方式,演示了兩種不同的全局捕獲異常處理的過程;值得註意到是,兩種方式對於 NLog 的使用,都是一樣的,沒有任何差別,代碼無需改動;實際項目中,也是應當區分不同的業務場景,輸出不同的
日誌信息,不管是從安全或者是用戶體驗友好性上面來說,都是非常值得推薦的方式,全局異常捕獲處理,完全和業務剝離。
- 努力為開源社區作貢獻,推薦一個自己開發的基於 .netcore+pgsql 的快速開發腳手架,內置 ORM框架,github地址:https://github.com/lianggx/mystaging
- 文中如有疏漏之處,歡迎指正。
- 源碼下載:https://files.cnblogs.com/files/viter/Ron.LogFilter.zip