2020/01/31, ASP.NET Core 3.1, VS2019, Autofac.Extras.DynamicProxy 4.5.0, Castle.Core.AsyncInterceptor 1.7.0
摘要:基於ASP.NET Core 3.1 WebApi搭建后端多層網站架構【9.2-使用Castle.Core實現動態代理攔截器】
介紹了如何對業務層方法進行攔截,捕獲業務方法發生的錯誤,然后統一進行日志記錄,避免在每個業務方法中進行try catch捕獲異常
本章節介紹了如何對業務層方法進行攔截,捕獲業務方法發生的錯誤,然后統一進行日志記錄,避免在每個業務方法中進行try catch捕獲異常。借助Autofac和Castle.Core實現動態代理攔截器,其中使用Castle.Core.AsyncInterceptor包實現異步攔截器。
添加包引用
向MS.Component.Aop
類庫中添加以下包引用:
<ItemGroup>
<PackageReference Include="Autofac.Extras.DynamicProxy" Version="4.5.0" />
<PackageReference Include="Castle.Core.AsyncInterceptor" Version="1.7.0" />
</ItemGroup>
- Autofac.Extras.DynamicProxy包和Autofac.Extensions.DependencyInjection包中的autofac版本最好一致,否則可能會出現代理攔截器注冊失敗的情況
- 可以先按我的包版本使用,確保攔截器無問題,再把包更新到最新版
- Autofac.Extras.DynamicProxy包是使用Autofac和Castle.Core實現動態代理攔截器
- Castle.Core.AsyncInterceptor包是實現異步攔截器(否則只能對同步方法進行攔截)
MS.Component.Aop
類庫要依賴MS.WebCore
類庫。
編寫服務攔截器
在MS.Component.Aop
類庫中添加LogAop文件夾,在該文件夾下新建AopHandledException.cs
、LogInterceptor.cs
、LogInterceptorAsync.cs
AopHandledException.cs
using System;
namespace MS.Component.Aop
{
/// <summary>
/// 使用自定義的Exception,用於在aop中已經處理過的異常,在其他地方不用重復記錄日志
/// </summary>
public class AopHandledException : ApplicationException
{
public string ErrorMessage { get; private set; }
public Exception InnerHandledException { get; private set; }
//無參數構造函數
public AopHandledException()
{
}
//帶一個字符串參數的構造函數,作用:當程序員用Exception類獲取異常信息而非 MyException時把自定義異常信息傳遞過去
public AopHandledException(string msg) : base(msg)
{
this.ErrorMessage = msg;
}
//帶有一個字符串參數和一個內部異常信息參數的構造函數
public AopHandledException(string msg, Exception innerException) : base(msg)
{
this.InnerHandledException = innerException;
this.ErrorMessage = msg;
}
public string GetError()
{
return ErrorMessage;
}
}
}
這里自定義了一個AopHandledException異常類型,目的是為了:
- 攔截器捕獲異常后已經進行了日志記錄,這個異常可能需要繼續拋出去,此時用AopHandledException異常類型包一層異常,這樣后面再捕獲到的異常就能判斷是從Aop中拋出來的異常了,不再重復記錄日志
LogInterceptor.cs
using Castle.DynamicProxy;
namespace MS.Component.Aop
{
public class LogInterceptor : IInterceptor
{
private readonly LogInterceptorAsync _logInterceptorAsync;
public LogInterceptor(LogInterceptorAsync logInterceptorAsync)
{
_logInterceptorAsync = logInterceptorAsync;
}
public void Intercept(IInvocation invocation)
{
_logInterceptorAsync.ToInterceptor().Intercept(invocation);
}
}
}
這個是主攔截器,繼承自IInterceptor,不管是同步方法還是異步方法,都將會走其中的Intercept方法,然后會在LogInterceptorAsync中再去區分是異步方法還是同步方法
LogInterceptorAsync.cs
using Castle.DynamicProxy;
using Microsoft.Extensions.Logging;
using MS.Common.Extensions;
using System;
using System.Linq;
using System.Threading.Tasks;
namespace MS.Component.Aop
{
public class LogInterceptorAsync : IAsyncInterceptor
{
private readonly ILogger<LogInterceptorAsync> _logger;
public LogInterceptorAsync(ILogger<LogInterceptorAsync> logger)
{
_logger = logger;
}
/// <summary>
/// 同步方法攔截時使用
/// </summary>
/// <param name="invocation"></param>
public void InterceptSynchronous(IInvocation invocation)
{
try
{
//調用業務方法
invocation.Proceed();
LogExecuteInfo(invocation, invocation.ReturnValue.ToJsonString());//記錄日志
}
catch (Exception ex)
{
LogExecuteError(ex, invocation);
throw new AopHandledException();
}
}
/// <summary>
/// 異步方法返回Task時使用
/// </summary>
/// <param name="invocation"></param>
public void InterceptAsynchronous(IInvocation invocation)
{
try
{
//調用業務方法
invocation.Proceed();
LogExecuteInfo(invocation, invocation.ReturnValue.ToJsonString());//記錄日志
}
catch (Exception ex)
{
LogExecuteError(ex, invocation);
throw new AopHandledException();
}
}
/// <summary>
/// 異步方法返回Task<T>時使用
/// </summary>
/// <typeparam name="TResult"></typeparam>
/// <param name="invocation"></param>
public void InterceptAsynchronous<TResult>(IInvocation invocation)
{
//調用業務方法
invocation.ReturnValue = InternalInterceptAsynchronous<TResult>(invocation);
}
private async Task<TResult> InternalInterceptAsynchronous<TResult>(IInvocation invocation)
{
try
{
//調用業務方法
invocation.Proceed();
Task<TResult> task = (Task<TResult>)invocation.ReturnValue;
TResult result = await task;//獲得返回結果
LogExecuteInfo(invocation, result.ToJsonString());
return result;
}
catch (Exception ex)
{
LogExecuteError(ex, invocation);
throw new AopHandledException();
}
}
#region helpMethod
/// <summary>
/// 獲取攔截方法信息(類名、方法名、參數)
/// </summary>
/// <param name="invocation"></param>
/// <returns></returns>
private string GetMethodInfo(IInvocation invocation)
{
//方法類名
string className = invocation.Method.DeclaringType.Name;
//方法名
string methodName = invocation.Method.Name;
//參數
string args = string.Join(", ", invocation.Arguments.Select(a => (a ?? "").ToString()).ToArray());
if (string.IsNullOrWhiteSpace(args))
{
return $"{className}.{methodName}";
}
else
{
return $"{className}.{methodName}:{args}";
}
}
private void LogExecuteInfo(IInvocation invocation, string result)
{
_logger.LogDebug("方法{0},返回值{1}", GetMethodInfo(invocation), result);
}
private void LogExecuteError(Exception ex, IInvocation invocation)
{
_logger.LogError(ex, "執行{0}時發生錯誤!", GetMethodInfo(invocation));
}
#endregion
}
}
這里是對方法攔截的主要實現:
- InterceptSynchronous是同步方法攔截時使用
- InterceptAsynchronous是異步方法返回Task時使用
- InterceptAsynchronous
是異步方法返回Task(包含返回值)時使用 invocation.Proceed();
這句話即是調用真正的業務方法,所以在該方法之前可以做一些權限判斷的內容,在該方法之后可以獲取記錄業務返回結果- 可以在invocation參數中獲取方法名稱、傳遞的參數等信息
- 如果做了用戶登錄,可以通過構造函數依賴注入IHttpContextAccessor以拿到執行業務者的信息(其實使用NLog記錄日志時,NLog也能獲取到登錄用戶的信息,就是NetUserIdentity這個參數)
- 在攔截器中,我對每個業務方法都以Debug的日志等級記錄了調用業務方法的返回結果
至此對業務方法進行攔截,以Debug的日志等級記錄了調用業務方法的返回結果,並捕獲所有業務方法的異常已經完成。
封裝Ioc注冊
在MS.Component.Aop
類庫中添加AopServiceExtensions.cs
類:
using Autofac;
using Autofac.Extras.DynamicProxy;
using System;
using System.Reflection;
namespace MS.Component.Aop
{
public static class AopServiceExtension
{
/// <summary>
/// 注冊aop服務攔截器
/// 同時注冊了各業務層接口與實現
/// </summary>
/// <param name="builder"></param>
/// <param name="serviceAssemblyName">業務層程序集名稱</param>
public static void AddAopService(this ContainerBuilder builder, string serviceAssemblyName)
{
//注冊攔截器,同步異步都要
builder.RegisterType<LogInterceptor>().AsSelf();
builder.RegisterType<LogInterceptorAsync>().AsSelf();
//注冊業務層,同時對業務層的方法進行攔截
builder.RegisterAssemblyTypes(Assembly.Load(serviceAssemblyName))
.AsImplementedInterfaces().InstancePerLifetimeScope()
.EnableInterfaceInterceptors()//引用Autofac.Extras.DynamicProxy;
.InterceptedBy(new Type[] { typeof(LogInterceptor) })//這里只有同步的,因為異步方法攔截器還是先走同步攔截器
;
//業務層注冊攔截器也可以使用[Intercept(typeof(LogInterceptor))]加在類上,但是上面的方法比較好,沒有侵入性
}
}
}
說明:
- 在AddAopService方法中,傳遞了業務層程序集名稱,使用Autofac統一注冊業務層所有的接口和實現(這樣就不用每個接口都寫一次注冊了),注冊類型為InstancePerLifetimeScope
- 注冊業務的同時,開啟了代理攔截器:EnableInterfaceInterceptors
- 開啟代理攔截器的同時,InterceptedBy傳遞了要使用哪些攔截器進行攔截,這里只有同步的,因為異步方法攔截器還是先走同步攔截器
- 業務層注冊攔截器也可以使用[Intercept(typeof(LogInterceptor))]加在類上,但是上面的方法比較好,沒有侵入性
獲取業務層程序集名稱
在MS.Services
中添加ServiceExtensions.cs
類:
using System.Reflection;
namespace MS.Services
{
public static class ServiceExtensions
{
/// <summary>
/// 獲取程序集名稱
/// </summary>
/// <returns></returns>
public static string GetAssemblyName()
{
return Assembly.GetExecutingAssembly().GetName().Name;
}
}
}
用於獲取業務層的程序集名稱,提供給Autofac進行批量的注冊接口和實現。
注冊Aop服務
在MS.WebApi
應用程序中,Startup.cs:
在ConfigureContainer方法里刪掉原先對IBaseService、IRoleService的接口注冊,使用剛寫的Aop注冊,給業務層批量注冊同時開啟代理攔截:
//using MS.Services;
//以上代碼添加至using
//注冊aop攔截器
//將業務層程序集名稱傳了進去,給業務層接口和實現做了注冊,也給業務層各方法開啟了代理
builder.AddAopService(ServiceExtensions.GetAssemblyName());
完成后,代碼如下圖所示
至此,業務層方法的代理攔截都完成了,執行業務時,會在控制台顯示每次業務的調用返回結果,如果遇到異常,會統一捕獲並記錄日志然后把異常包裝一次再拋出去
啟動項目,打開Postman測試接口:
可以看到返回結果顯示出來了,可以打斷點看看程序到底怎么通過攔截器的。
這里只是實現了業務層方法攔截,異常日志的捕獲,可以做更多例如權限驗證的攔截器。
項目完成后,如下圖所示