本文介紹AOP編程的基本概念、Castle DynamicProxy(DP)的基本用法,使用第三方擴展實現對異步(async)的支持,結合Autofac演示如何實現AOP編程。
AOP
百科中關於AOP的解釋:
AOP為Aspect Oriented Programming的縮寫,意為:面向切面編程,通過預編譯方式和運行期動態代理實現程序功能的統一維護的一種技術。AOP是OOP的延續,是軟件開發中的一個熱點……是函數式編程的一種衍生范型。利用AOP可以對業務邏輯的各個部分進行隔離,從而使得業務邏輯各部分之間的耦合度降低,提高程序的可重用性,同時提高了開發的效率。
在AOP中,我們關注橫切點,將通用的處理流程提取出來,我們會提供系統通用功能,並在各業務層中進行使用,例如日志模塊、異常處理模塊等。通過AOP編程實現更加靈活高效的開發體驗。
DynamicProxy的基本用法
動態代理是實現AOP的一種方式,即在開發過程中我們不需要處理切面中(日志等)的工作,而是在運行時,通過動態代理來自動完成。Castle DynamicProxy是一個實現動態代理的框架,被很多優秀的項目用來實現AOP編程,EF Core、Autofac等。
我們來看兩段代碼,演示AOP的好處。在使用AOP之前:
public class ProductRepository : IProductRepository
{
private readonly ILogger logger;
public ProductRepository(ILogger logger)
{
this.logger = logger;
}
public void Update(Product product)
{
//執行更新操作
//......
//記錄日志
logger.WriteLog($"產品{product}已更新");
}
}
在使用AOP之后:
public class ProductRepository : IProductRepository
{
public void Update(Product product)
{
//執行更新操作
//......
}
}
可以明顯的看出,在使用之前我們的ProductRepository依賴於ILogger,並在執行Update操作以后,要寫出記錄日志的代碼;而在使用之后,將日志記錄交給動態代理來處理,降低了不少的開發量,即使遇見略微馬虎的程序員,也不耽誤我們日志的記錄。
那該如何實現這樣的操作呢?
- 首先,引用
Castle.Core
- 然后,定義攔截器,實現
IInterceptor
接口
public class LoggerInterceptor : IInterceptor
{
private readonly ILogger logger;
public LoggerInterceptor(ILogger logger)
{
this.logger = logger;
}
public void Intercept(IInvocation invocation)
{
//獲取執行信息
var methodName = invocation.Method.Name;
//調用業務方法
invocation.Proceed();
//記錄日志
this.logger.WriteLog($"{methodName} 已執行");
}
}
- 最后,添加調用代碼
static void Main(string[] args)
{
ILogger logger = new ConsoleLogger();
Product product = new Product() { Name = "Book" };
IProductRepository target = new ProductRepository();
ProxyGenerator generator = new ProxyGenerator();
IInterceptor loggerIntercept = new LoggerInterceptor(logger);
IProductRepository proxy = generator.CreateInterfaceProxyWithTarget(target, loggerIntercept);
proxy.Update(product);
}
至此,我們已經完成了一個日志攔截器,其它業務需要用到日志記錄的時候,也可通過創建動態代理的方式來進行AOP編程。
但是,調用起來還是比較復雜,需要怎么改進呢?當然是使用依賴注入(DI)了。
Autofac的集成
Autofac集成了對DynamicProxy的支持,我們需要引用Autofac.Extras.DynamicProxy
,然后創建容器、注冊服務、生成實例、調用方法,我們來看下面的代碼:
ContainerBuilder builder = new ContainerBuilder();
//注冊攔截器
builder.RegisterType<LoggerInterceptor>().AsSelf();
//注冊基礎服務
builder.RegisterType<ConsoleLogger>().AsImplementedInterfaces();
//注冊要攔截的服務
builder.RegisterType<ProductRepository>().AsImplementedInterfaces()
.EnableInterfaceInterceptors() //啟用接口攔截
.InterceptedBy(typeof(LoggerInterceptor)); //指定攔截器
var container = builder.Build();
//解析服務
var productRepository = container.Resolve<IProductRepository>();
Product product = new Product() { Name = "Book" };
productRepository.Update(product);
對這段代碼做一下說明:
- 注冊攔截器時,需要注冊為
AsSelf
,因為服務攔截時使用的是攔截器的實例,這種注冊方式可以保證容器能夠解析到攔截器。 - 開啟攔截功能:注冊要攔截的服務時,需要調用
EnableInterfaceInterceptors
方法,表示開啟接口攔截; - 關聯服務與攔截器:
InterceptedBy
方法傳入攔截器,指定攔截器的方式有兩種,一種是我們代碼中的寫法,對服務是無入侵的,因此推薦這種用法。另一種是通過Intercept
特性來進行關聯,例如我們上面的代碼可以寫為ProductRepository
類上添加特性[Intercept(typeof(LoggerInterceptor))]
- 攔截器的注冊,可以注冊為類型攔截器,也可以注冊為命名的攔截器,使用上會有一些差異,主要在攔截器的關聯上,此部分可以參考Autofac官方文檔。我們示例用的是類型注冊。
- 攔截器只對公共的接口方法、類中的虛方法有效,使用時需要特別注意。
DynamicProxy的基本原理
上面我們說到動態代理只對公共接口方法、類中的虛方法生效,你是否想過為什么?
其實,動態代理是在運行時為我們動態生成了一個代理類,通過Generator
生成的時候返回給我們的是代理類的實例,而只有接口中的方法、類中的虛方法才可以在子類中被重寫。
如果不使用動態代理,我們的代理服務應該是什么樣的呢?來看下面的代碼,讓我們手工創建一個代理類:
以下是我對代理類的理解,請大家辯證的看待,如果存在不正確的地方,還望指出。
為接口使用代理:
public class ProductRepositoryProxy : IProductRepository
{
private readonly ILogger logger;
private readonly IProductRepository target;
public ProductRepositoryProxy(ILogger logger, IProductRepository target)
{
this.logger = logger;
this.target = target;
}
public void Update(Product product)
{
//調用IProductRepository的Update操作
target.Update(product);
//記錄日志
this.logger.WriteLog($"{nameof(Update)} 已執行");
}
}
//使用代理類
IProductRepository target = new ProductRepository();
ILogger logger = new ConsoleLogger();
IProductRepository productRepository = new ProductRepositoryProxy(logger, target);
為類使用代理:
public class ProductRepository : IProductRepository
{
//改寫為虛方法
public virtual void Update(Product product)
{
//執行更新操作
//......
}
}
public class ProductRepositoryProxy : ProductRepository
{
private readonly ILogger logger;
public ProductRepositoryProxy(ILogger logger)
{
this.logger = logger;
}
public override void Update(Product product)
{
//調用父類的Update操作
base.Update(product);
//記錄日志
this.logger.WriteLog($"{nameof(Update)} 已執行");
}
}
//使用代理類
ILogger logger = new ConsoleLogger();
ProductRepository productRepository = new ProductRepositoryProxy(logger);
異步(async/await)的支持
如果你站在應用程序的角度來看,異步只是微軟的一個語法糖,使用異步的方法返回結果為一個Task或Task
Castle.Core.AsyncInterceptor
是幫我們處理異步攔截的框架,通過使用該框架可以降低異步處理的難度。
我們本節仍然結合Autofac進行處理,首先對代碼進行改造,將ProductRepository.Update
方法改為異步的。
public class ProductRepository : IProductRepository
{
public virtual Task<int> Update(Product product)
{
Console.WriteLine($"{nameof(Update)} Entry");
//執行更新操作
var task = Task.Run(() =>
{
//......
Thread.Sleep(1000);
Console.WriteLine($"{nameof(Update)} 更新操作已完成");
//返回執行結果
return 1;
});
//返回
return task;
}
}
接下來定義我們的異步攔截器:
public class LoggerAsyncInterceptor : IAsyncInterceptor
{
private readonly ILogger logger;
public LoggerAsyncInterceptor(ILogger logger)
{
this.logger = logger;
}
/// <summary>
/// 同步方法攔截時使用
/// </summary>
/// <param name="invocation"></param>
public void InterceptSynchronous(IInvocation invocation)
{
throw new NotImplementedException();
}
/// <summary>
/// 異步方法返回Task時使用
/// </summary>
/// <param name="invocation"></param>
public void InterceptAsynchronous(IInvocation invocation)
{
throw new NotImplementedException();
}
/// <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)
{
//獲取執行信息
var methodName = invocation.Method.Name;
invocation.Proceed();
var task = (Task<TResult>)invocation.ReturnValue;
TResult result = await task;
//記錄日志
this.logger.WriteLog($"{methodName} 已執行,返回結果:{result}");
return result;
}
}
IAsyncInterceptor
接口是異步攔截器接口,它提供了三個方法:
InterceptSynchronous
:攔截同步執行的方法InterceptAsynchronous
:攔截返回結果為Task的方法InterceptAsynchronous<TResult>
:攔截返回結果為Task的方法
在我們上面的代碼中,只實現了InterceptAsynchronous<TResult>
方法。
由於IAsyncInterceptor
接口和DP框架中的IInterceptor
接口沒有關聯,所以我們還需要一個同步攔截器,此處直接修改舊的同步攔截器:
public class LoggerInterceptor : IInterceptor
{
private readonly LoggerAsyncInterceptor interceptor;
public LoggerInterceptor(LoggerAsyncInterceptor interceptor)
{
this.interceptor = interceptor;
}
public void Intercept(IInvocation invocation)
{
this.interceptor.ToInterceptor().Intercept(invocation);
}
}
從代碼中可以看到,異步攔截器LoggerAsyncInterceptor
具有名為ToInterceptor()
的擴展方法,該方法可以將IAsyncInterceptor
接口的對象轉換為IInterceptor
接口的對象。
接下來我們修改DI的服務注冊部分:
ContainerBuilder builder = new ContainerBuilder();
//注冊攔截器
builder.RegisterType<LoggerInterceptor>().AsSelf();
builder.RegisterType<LoggerAsyncInterceptor>().AsSelf();
//注冊基礎服務
builder.RegisterType<ConsoleLogger>().AsImplementedInterfaces();
//注冊要攔截的服務
builder.RegisterType<ProductRepository>().AsImplementedInterfaces()
.EnableInterfaceInterceptors() //啟用接口攔截
.InterceptedBy(typeof(LoggerInterceptor)); //指定攔截器
var container = builder.Build();
以上便是通過IAsyncInterceptor
實現異步攔截器的方式。除了使用這種方式,我們也可以在在動態攔截器中判斷返回結果手工處理,此處不再贅述。
探討:ASP.NET MVC中的切面編程
通過上面的介紹,我們已經了解了AOP的基本用法,但是如何用在ASP.NET Core
中呢?
- MVC控制器的注冊是在Services中完成的,而Services本身不支持DP。這個問題可以通過整合Autofac重新注冊控制器來完成,但是這樣操作真的好嗎?
- MVC中的控制器是繼承自ControllerBase,Action方法是我們自定義的,不是某個接口的實現,這對實現AOP來說存在一定困難。這個問題可以通過將Action定義為虛方法來解決,但是這樣真的符合我們的編碼習慣嗎?
我們知道,AOP的初衷就是對使用者保持黑盒,通過抽取切面進行編程,而這兩個問題恰恰需要我們對使用者進行修改,違背了SOLID原則。
那么,如果我們要在MVC中使用AOP,有什么方法呢?其實MVC已經為我們提供了兩種實現AOP的方式:
- 中間件(Middleware),這是MVC中的大殺器,提供了日志、Cookie、授權等一系列內置的中間件,從中可以看出,MVC並不想我們通過DP實現AOP,而是要在管道中做文章。
- 過濾器(Filter),Filter是 ASP.NET MVC的產物,曾經一度幫助我們解決了異常、授權等邏輯,在Core時代我們仍然可以采用這種方式。
這兩種方式更符合我們的編碼習慣,也體現了MVC框架的特性。
綜上,不建議在MVC中對Controller使用DP。如果采用NLayer架構,則可以在Application層、Domain層使用DP,來實現類似數據審計、SQL跟蹤等處理。
雖然不推薦,但還是給出代碼,給自己多一條路:
- MVC控制器注冊為服務
services.AddMvc()
.AddControllersAsServices();
- 重新注冊控制器,配置攔截
builder.RegisterType<ProductController>()
.EnableClassInterceptors()
.InterceptedBy(typeof(ControllerInterceptor));
- 控制器中的Action定義為虛方法
[HttpPost]
public virtual Task<int> Update(Product product)
{
return this.productRepository.Update(product);
}
補充內容
- 2019年7月24日補充
在創建代理類時(無論是class或interface),都有兩種寫法:WithTarget和WithoutTarget,這兩種寫法有一定的區別,withTarget需要傳入目標實例,而withoutTarget則不用,只需要傳入類型即可。