系列文章
- 基於 abp vNext 和 .NET Core 開發博客項目 - 使用 abp cli 搭建項目
- 基於 abp vNext 和 .NET Core 開發博客項目 - 給項目瘦身,讓它跑起來
- 基於 abp vNext 和 .NET Core 開發博客項目 - 完善與美化,Swagger登場
- 基於 abp vNext 和 .NET Core 開發博客項目 - 數據訪問和代碼優先
- 基於 abp vNext 和 .NET Core 開發博客項目 - 自定義倉儲之增刪改查
- 基於 abp vNext 和 .NET Core 開發博客項目 - 統一規范API,包裝返回模型
- 基於 abp vNext 和 .NET Core 開發博客項目 - 再說Swagger,分組、描述、小綠鎖
- 基於 abp vNext 和 .NET Core 開發博客項目 - 接入GitHub,用JWT保護你的API
- 基於 abp vNext 和 .NET Core 開發博客項目 - 異常處理和日志記錄
- 基於 abp vNext 和 .NET Core 開發博客項目 - 使用Redis緩存數據
- 基於 abp vNext 和 .NET Core 開發博客項目 - 集成Hangfire實現定時任務處理
- 基於 abp vNext 和 .NET Core 開發博客項目 - 用AutoMapper搞定對象映射
- 基於 abp vNext 和 .NET Core 開發博客項目 - 定時任務最佳實戰(一)
- 基於 abp vNext 和 .NET Core 開發博客項目 - 定時任務最佳實戰(二)
- 基於 abp vNext 和 .NET Core 開發博客項目 - 定時任務最佳實戰(三)
- 基於 abp vNext 和 .NET Core 開發博客項目 - 博客接口實戰篇(一)
- 基於 abp vNext 和 .NET Core 開發博客項目 - 博客接口實戰篇(二)
- 基於 abp vNext 和 .NET Core 開發博客項目 - 博客接口實戰篇(三)
- 基於 abp vNext 和 .NET Core 開發博客項目 - 博客接口實戰篇(四)
- 基於 abp vNext 和 .NET Core 開發博客項目 - 博客接口實戰篇(五)
- 基於 abp vNext 和 .NET Core 開發博客項目 - Blazor 實戰系列(一)
- 基於 abp vNext 和 .NET Core 開發博客項目 - Blazor 實戰系列(二)
- 基於 abp vNext 和 .NET Core 開發博客項目 - Blazor 實戰系列(三)
- 基於 abp vNext 和 .NET Core 開發博客項目 - Blazor 實戰系列(四)
- 基於 abp vNext 和 .NET Core 開發博客項目 - Blazor 實戰系列(五)
- 基於 abp vNext 和 .NET Core 開發博客項目 - Blazor 實戰系列(六)
- 基於 abp vNext 和 .NET Core 開發博客項目 - Blazor 實戰系列(七)
- 基於 abp vNext 和 .NET Core 開發博客項目 - Blazor 實戰系列(八)
- 基於 abp vNext 和 .NET Core 開發博客項目 - Blazor 實戰系列(九)
- 基於 abp vNext 和 .NET Core 開發博客項目 - 終結篇之發布項目
在開始之前,我們實現一個之前的遺留問題,這個問題是有人在GitHub Issues(https://github.com/Meowv/Blog/issues/8)上提出來的,就是當我們對Swagger進行分組,實現IDocumentFilter
接口添加了文檔描述信息后,切換分組時會顯示不屬於當前分組的Tag。
經過研究和分析發現,是可以解決的,我不知道大家有沒有更好的辦法,我的實現方法請看:
//SwaggerDocumentFilter.cs
...
public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
{
var tags = new List<OpenApiTag>{...}
#region 實現添加自定義描述時過濾不屬於同一個分組的API
var groupName = context.ApiDescriptions.FirstOrDefault().GroupName;
var apis = context.ApiDescriptions.GetType().GetField("_source", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(context.ApiDescriptions) as IEnumerable<ApiDescription>;
var controllers = apis.Where(x => x.GroupName != groupName).Select(x => ((ControllerActionDescriptor)x.ActionDescriptor).ControllerName).Distinct();
swaggerDoc.Tags = tags.Where(x => !controllers.Contains(x.Name)).OrderBy(x => x.Name).ToList();
#endregion
}
...
根據調試代碼發現,我們可以從context.ApiDescriptions
獲取到當前顯示的是哪一個分組下的API。
然后使用GetType().GetField(string name, BindingFlags bindingAttr)
獲取到_source
,當前項目的所有API,里面同時也包含了ABP默認生成的一些接口。
再將API中不屬於當前分組的API篩選掉,用Select查詢出所有的Controller名稱進行去重。
因為OpenApiTag
中的Name名稱與Controller的Name是一致的,所以最后將包含controllers
名稱的tag查詢出來取反,即可滿足需求。
上一篇文章(https://www.cnblogs.com/meowv/p/12935693.html)集成了GitHub,使用JWT的方式完成了身份認證和授權,保護了我們寫的API接口。
本篇主要實現對項目中出現的異常僅需處理,當出現不可避免的錯誤時,或者未授權用戶調用接口時,可以進行有效的監控和日志記錄。
目前調用未授權接口,會直接返回一個狀態碼為401的錯誤頁面,這樣顯得太不友好,我們還是用之前寫的統一返回模型來告訴調用者,你是未授權的,調不了我的接口,上篇也有提到過,我們將用兩種方式來解決。
方式一 :使用AddJwtBearer()
擴展方法下面的options.Events
事件機制。
//MeowvBlogHttpApiHostingModule.cs
...
//應用程序提供的對象,用於處理承載引發的事件,身份驗證處理程序
options.Events = new JwtBearerEvents
{
OnChallenge = async context =>
{
// 跳過默認的處理邏輯,返回下面的模型數據
context.HandleResponse();
context.Response.ContentType = "application/json;charset=utf-8";
context.Response.StatusCode = StatusCodes.Status200OK;
var result = new ServiceResult();
result.IsFailed("UnAuthorized");
await context.Response.WriteAsync(result.ToJson());
}
};
...
在項目啟動時,實例化了OnChallenge
,如果用戶調用未授權,將請求的狀態碼賦值為200,並返回模型數據。
如圖所示,可以看到已經成功返回了一段比較友好的JSON數據。
{
"Code": 1,
"Message": "UnAuthorized",
"Success": false,
"Timestamp": 1590226085318
}
方式二 :使用中間件的方式。
我們注釋掉上面的代碼,在.HttpApi.Hosting
添加文件夾Middleware,新建一個中間件ExceptionHandlerMiddleware.cs
using Meowv.Blog.ToolKits.Base;
using Meowv.Blog.ToolKits.Extensions;
using Microsoft.AspNetCore.Http;
using System;
using System.Net;
using System.Threading.Tasks;
namespace Meowv.Blog.HttpApi.Hosting.Middleware
{
/// <summary>
/// 異常處理中間件
/// </summary>
public class ExceptionHandlerMiddleware
{
private readonly RequestDelegate next;
public ExceptionHandlerMiddleware(RequestDelegate next)
{
this.next = next;
}
/// <summary>
/// Invoke
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
public async Task Invoke(HttpContext context)
{
try
{
await next(context);
}
catch (Exception ex)
{
await ExceptionHandlerAsync(context, ex.Message);
}
finally
{
var statusCode = context.Response.StatusCode;
if (statusCode != StatusCodes.Status200OK)
{
Enum.TryParse(typeof(HttpStatusCode), statusCode.ToString(), out object message);
await ExceptionHandlerAsync(context, message.ToString());
}
}
}
/// <summary>
/// 異常處理,返回JSON
/// </summary>
/// <param name="context"></param>
/// <param name="message"></param>
/// <returns></returns>
private async Task ExceptionHandlerAsync(HttpContext context, string message)
{
context.Response.ContentType = "application/json;charset=utf-8";
var result = new ServiceResult();
result.IsFailed(message);
await context.Response.WriteAsync(result.ToJson());
}
}
}
RequestDelegate
是一種請求委托類型,用來處理HTTP請求的函數,返回的是delegate
,實現異步的Invoke
方法。
這里我寫了一個比較通用的方法,當出現異常時直接執行ExceptionHandlerAsync()
方法,當沒有異常發生時,在finally
中判斷當前請求狀態,可能是200?404?401?等等,不管它是什么,反正不是200,獲取到狀態碼枚舉的Key值用來當作錯誤信息返回,最后也執行ExceptionHandlerAsync()
方法,返回我們自定義的模型。
寫好了中間件,然后在OnApplicationInitialization(...)
中使用它。
public override void OnApplicationInitialization(ApplicationInitializationContext context)
{
...
// 異常處理中間件
app.UseMiddleware<ExceptionHandlerMiddleware>();
...
}
同樣可以達到效果,相比之下他還支持狀態非401的錯誤返回,比如我們訪問一個不存在的頁面:https://localhost:44388/aaa ,也可以友好的進行處理。
當然這兩種方式可以共存,互不影響。
還有一種處理異常的方式,就是我們的過濾器Filter,abp已經默認為我們實現了全局的異常模塊,詳情可以看其文檔:https://docs.abp.io/zh-Hans/abp/latest/Exception-Handling ,在這里,我准備移除abp提供的異常處理模塊,自己實現一個。
先看一下目前的異常顯示情況,我們在HelloWorldController
中寫一個異常接口。
//HelloWorldController.cs
...
[HttpGet]
[Route("Exception")]
public string Exception()
{
throw new NotImplementedException("這是一個未實現的異常接口");
}
...
按理說,他應該會執行到我們寫的ExceptionHandlerMiddleware
中間件中去,但是被我們的Filter進行攔截了,現在我們移除默認的攔截器AbpExceptionFilter
還是在模塊類MeowvBlogHttpApiHostingModule
,ConfigureServices()
方法中。
Configure<MvcOptions>(options =>
{
var filterMetadata = options.Filters.FirstOrDefault(x => x is ServiceFilterAttribute attribute && attribute.ServiceType.Equals(typeof(AbpExceptionFilter)));
// 移除 AbpExceptionFilter
options.Filters.Remove(filterMetadata);
});
從options.Filters
中找到AbpExceptionFilter
,然后Remove掉,此時再看一下有異常的接口。
當我們注釋掉我們的中間件時,他就會顯示如下圖這樣。
這個頁面有沒有很熟悉的感覺?相信做過.net core開發的都遇到過吧。
ok,現在為止已經完美顯示了。但到這里還遠遠不夠,說好的自己實現Filter呢?我們現在實現Filter又有什么用呢?我們可以在Filter中可以做一些日志記錄。
在.HttpApi.Hosting
層添加文件夾Filters,新建一個MeowvBlogExceptionFilter.cs
的Filter,他需要實現我們的IExceptionFilter
接口的OnExceptionAsync()
方法即可。
//MeowvBlogExceptionFilter.cs
using Meowv.Blog.ToolKits.Helper;
using Microsoft.AspNetCore.Mvc.Filters;
namespace Meowv.Blog.HttpApi.Hosting.Filters
{
public class MeowvBlogExceptionFilter : IExceptionFilter
{
/// <summary>
/// 異常處理
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
public void OnException(ExceptionContext context)
{
// 日志記錄
LoggerHelper.WriteToFile($"{context.HttpContext.Request.Path}|{context.Exception.Message}", context.Exception);
}
}
}
OnException(...)
方法很簡單,這里只做了記錄日志的操作,剩下的交給我們中間件去處理吧。
注意,一定要在移除默認AbpExceptionFilter
后,將我們自己實現的MeowvBlogExceptionFilter
在模塊類ConfigureServices()
方法中注入到系統。
...
Configure<MvcOptions>(options =>
{
...
// 添加自己實現的 MeowvBlogExceptionFilter
options.Filters.Add(typeof(MeowvBlogExceptionFilter));
});
...
說到日志,就有很多種處理方式,請選擇你熟悉的方式,我這里將使用log4net
進行處理,僅供參考。
在.ToolKits
層添加log4net
包,使用命令安裝:Install-Package log4net
,然后添加文件夾Helper,新建一個LoggerHelper.cs
。
//LoggerHelper.cs
using log4net;
using log4net.Config;
using log4net.Repository;
using System;
using System.IO;
namespace Meowv.Blog.ToolKits.Helper
{
public static class LoggerHelper
{
private static readonly ILoggerRepository Repository = LogManager.CreateRepository("NETCoreRepository");
private static readonly ILog Log = LogManager.GetLogger(Repository.Name, "NETCorelog4net");
static LoggerHelper()
{
XmlConfigurator.Configure(Repository, new FileInfo("log4net.config"));
}
/// <summary>
/// 寫日志
/// </summary>
/// <param name="message"></param>
/// <param name="ex"></param>
public static void WriteToFile(string message)
{
Log.Info(message);
}
/// <summary>
/// 寫日志
/// </summary>
/// <param name="message"></param>
/// <param name="ex"></param>
public static void WriteToFile(string message, Exception ex)
{
if (string.IsNullOrEmpty(message))
message = ex.Message;
Log.Error(message, ex);
}
}
}
在.HttpApi.Hosting
中添加log4net配置文件,log4net.config
配置文件如下:
//log4net.config
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<configSections>
<section name="log4net" type="log4net.Config.Log4NetConfigurationSectionHandler, log4net"/>
</configSections>
<log4net debug="false">
<appender name="info" type="log4net.Appender.RollingFileAppender,log4net">
<param name="File" value="log4net/info/" />
<param name="AppendToFile" value="true" />
<param name="MaxSizeRollBackups" value="-1"/>
<param name="MaximumFileSize" value="5MB"/>
<param name="RollingStyle" value="Composite" />
<param name="DatePattern" value="yyyyMMdd\\HH".log"" />
<param name="StaticLogFileName" value="false" />
<layout type="log4net.Layout.PatternLayout,log4net">
<param name="ConversionPattern" value="%n
{
"system": "Meowv.Blog",
"datetime": "%d",
"description": "%m",
"level": "%p",
"info": "%exception"
}" />
</layout>
<filter type="log4net.Filter.LevelRangeFilter">
<levelMin value="INFO" />
<levelMax value="INFO" />
</filter>
</appender>
<appender name="error" type="log4net.Appender.RollingFileAppender,log4net">
<param name="File" value="log4net/error/" />
<param name="AppendToFile" value="true" />
<param name="MaxSizeRollBackups" value="-1"/>
<param name="MaximumFileSize" value="5MB"/>
<param name="RollingStyle" value="Composite" />
<param name="DatePattern" value="yyyyMMdd\\HH".log"" />
<param name="StaticLogFileName" value="false" />
<layout type="log4net.Layout.PatternLayout,log4net">
<param name="ConversionPattern" value="%n
{
"system": "Meowv.Blog",
"datetime": "%d",
"description": "%m",
"level": "%p",
"info": "%exception"
}" />
</layout>
<filter type="log4net.Filter.LevelRangeFilter">
<levelMin value="ERROR" />
<levelMax value="ERROR" />
</filter>
</appender>
<root>
<level value="ALL"></level>
<appender-ref ref="info"/>
<appender-ref ref="error"/>
</root>
</log4net>
</configuration>
此時再去調用 .../HelloWorld/Exception,將會得到日志文件,內容是以JSON格式進行存儲的。
關於Filter的更多用法可以參考微軟官方文檔:https://docs.microsoft.com/zh-cn/aspnet/core/mvc/controllers/filters
到這里,系統的異常處理和日志記錄便完成了,你學會了嗎?😁😁😁