本節(jié)介紹Util應(yīng)用框架對(duì)AspectCore AOP的使用.
概述
有些問題需要在系統(tǒng)中全局處理,比如記錄異常錯(cuò)誤日志.
如果在每個(gè)出現(xiàn)問題的地方進(jìn)行處理,不僅費(fèi)力,還可能產(chǎn)生大量冗余代碼,并打斷業(yè)務(wù)邏輯的編寫.
這類跨多個(gè)業(yè)務(wù)模塊的非功能需求,被稱為橫切關(guān)注點(diǎn).
我們需要把橫切關(guān)注點(diǎn)集中管理起來.
Asp.Net Core 提供的過濾器可以處理這類需求.
過濾器有異常過濾器和操作過濾器等類型.
異常過濾器可以全局處理異常.
操作過濾器可以攔截控制器操作,在操作前和操作后執(zhí)行特定代碼.
過濾器很易用,但它必須配合控制器使用,所以只能解決部分問題.
你不能將過濾器特性打在應(yīng)用服務(wù)的方法上,那不會(huì)產(chǎn)生作用.
我們需要引入一種類似 Asp.Net Core 過濾器的機(jī)制,在控制器范圍外處理橫切關(guān)注點(diǎn).
AOP框架
AOP 是 Aspect Oriented Programming 的縮寫,即面向切面編程.
AOP 框架提供了類似 Asp.Net Core 過濾器的功能,能夠攔截方法,在方法執(zhí)行前后插入自定義代碼.
.Net AOP框架有動(dòng)態(tài)代理和靜態(tài)織入兩種實(shí)現(xiàn)方式.
動(dòng)態(tài)代理 AOP 框架
動(dòng)態(tài)代理 AOP 框架在運(yùn)行時(shí)動(dòng)態(tài)創(chuàng)建代理類,從而為方法提供自定義代碼插入點(diǎn).
動(dòng)態(tài)代理 AOP 框架有一些限制.
-
要攔截的方法必須在接口中定義,或是虛方法.
-
代理類過多,特別是啟用了參數(shù)攔截,會(huì)導(dǎo)致啟動(dòng)性能下降.
.Net 動(dòng)態(tài)代理 AOP 框架有Castle 和 AspectCore 等.
Util應(yīng)用框架使用 AspectCore ,選擇 AspectCore 是因?yàn)樗右子?
Util 對(duì) AspectCore 僅簡單包裝.
靜態(tài)織入 AOP 框架
靜態(tài)織入 AOP 框架在編譯時(shí)修改.Net IL中間代碼.
與動(dòng)態(tài)代理AOP相比,靜態(tài)織入AOP框架有一些優(yōu)勢.
-
不必是虛方法.
-
支持靜態(tài)方法.
-
更高的啟動(dòng)性能.
但是成熟的 .Net 靜態(tài)織入 AOP 框架大多是收費(fèi)的.
Rougamo.Fody 是一個(gè)免費(fèi)的靜態(tài)織入 AOP 框架,可以關(guān)注.
基礎(chǔ)用法
引用Nuget包
Nuget包名: Util.Aop.AspectCore
啟用Aop
需要明確調(diào)用 AddAop 擴(kuò)展方法啟用 AOP 服務(wù).
var builder = WebApplication.CreateBuilder( args );
builder.AsBuild().AddAop();
使用要點(diǎn)
-
定義服務(wù)接口
如果使用抽象基類,應(yīng)將需要攔截的方法設(shè)置為虛方法.
-
配置服務(wù)接口的依賴注入關(guān)系
AspectCore AOP依賴Ioc對(duì)象容器,只有在對(duì)象容器中注冊的服務(wù)接口才能創(chuàng)建服務(wù)代理.
-
將方法攔截器放在接口方法上.
AspectCore AOP攔截器是一種.Net特性 Attribute,遵循 Attribute 使用約定.
下面的例子將 CacheAttribute 方法攔截器添加到 ITestService 接口的 Test 方法上.
注意: 應(yīng)將攔截器放在接口方法上,而不是實(shí)現(xiàn)類上.
按照約定, CacheAttribute 需要去掉 Attribute 后綴,并放到 [] 中.
public interface ITestService : ISingletonDependency { [Cache] List<string> Test( string value ); }
-
將參數(shù)攔截器放在接口方法參數(shù)上.
AspectCore AOP 支持?jǐn)r截特定參數(shù).
下面的例子在參數(shù) value 上施加了 NotNullAttribute 參數(shù)攔截器.
public interface ITestService : ISingletonDependency { void Test( [NotNull] string value ); }
Util內(nèi)置攔截器
Util應(yīng)用框架使用 Asp.Net Core 過濾器處理全局異常,全局錯(cuò)誤日志,授權(quán)等需求,僅定義少量 AOP 攔截器.
Util應(yīng)用框架定義了幾個(gè)參數(shù)攔截器,用于驗(yàn)證.
-
NotNullAttribute
-
驗(yàn)證是否為 null,如果為 null 拋出 ArgumentNullException 異常.
-
使用范例:
public interface ITestService : ISingletonDependency { void Test( [NotNull] string value ); }
-
-
NotEmptyAttribute
-
使用 string.IsNullOrWhiteSpace 驗(yàn)證是否為空字符串,如果為空則拋出 ArgumentNullException 異常.
-
使用范例:
public interface ITestService : ISingletonDependency { void Test( [NotEmpty] string value ); }
-
-
ValidAttribute
-
如果對(duì)象實(shí)現(xiàn)了 IValidation 驗(yàn)證接口,則自動(dòng)調(diào)用對(duì)象的 Validate 方法進(jìn)行驗(yàn)證.
Util應(yīng)用框架實(shí)體,值對(duì)象,DTO等基礎(chǔ)對(duì)象均已實(shí)現(xiàn) IValidation 接口.
-
使用范例:
驗(yàn)證單個(gè)對(duì)象.
public interface ITestService : ISingletonDependency { void Test( [Valid] CustomerDto dto ); }
驗(yàn)證對(duì)象集合.
public interface ITestService : ISingletonDependency { void Test( [Valid] List<CustomerDto> dto ); }
-
Util應(yīng)用框架為緩存定義了方法攔截器.
-
CacheAttribute
- 使用范例:
public interface ITestService : ISingletonDependency { [Cache] List<string> Test( string value ); }
禁止創(chuàng)建服務(wù)代理
有些時(shí)候,你不希望為某些接口創(chuàng)建代理類.
使用 Util.Aop.IgnoreAttribute 特性標(biāo)記接口即可.
下面演示了從 AspectCore AOP 排除工作單元接口.
[Util.Aop.Ignore]
public interface IUnitOfWork {
Task<int> CommitAsync();
}
創(chuàng)建自定義攔截器
除了內(nèi)置的攔截器外,你可以根據(jù)需要?jiǎng)?chuàng)建自定義攔截器.
創(chuàng)建方法攔截器
繼承 Util.Aop.InterceptorBase 基類,重寫 Invoke 方法.
下面以緩存攔截器為例講解創(chuàng)建方法攔截器的要點(diǎn).
-
緩存攔截器獲取 ICache 依賴服務(wù)并創(chuàng)建緩存鍵.
-
通過緩存鍵和返回類型查找緩存是否存在.
-
如果緩存已經(jīng)存在,則設(shè)置返回值,不需要執(zhí)行攔截的方法.
-
如果緩存不存在,執(zhí)行方法獲取返回值并設(shè)置緩存.
Invoke 方法有兩個(gè)參數(shù) AspectContext 和 AspectDelegate.
-
AspectContext上下文提供了方法元數(shù)據(jù)信息和服務(wù)提供程序.
-
使用 AspectContext 上下文獲取方法元數(shù)據(jù).
AspectContext 上下文提供了攔截方法相關(guān)的大量元數(shù)據(jù)信息.
本例使用 context.ServiceMethod.ReturnType 獲取返回類型.
-
使用 AspectContext 上下文獲取依賴的服務(wù).
AspectContext上下文提供了 ServiceProvider 服務(wù)提供器,可以使用它獲取依賴服務(wù).
本例需要獲取緩存操作接口 ICache ,使用 context.ServiceProvider.GetService<ICache>() 獲取依賴.
-
-
AspectDelegate表示攔截的方法.
await next( context ); 執(zhí)行攔截方法.
如果需要在方法執(zhí)行前插入自定義代碼,只需將代碼放在 await next( context ); 之前即可.
/// <summary>
/// 緩存攔截器
/// </summary>
public class CacheAttribute : InterceptorBase {
/// <summary>
/// 緩存鍵前綴
/// </summary>
public string CacheKeyPrefix { get; set; }
/// <summary>
/// 緩存過期間隔,單位:秒,默認(rèn)值:36000
/// </summary>
public int Expiration { get; set; } = 36000;
/// <summary>
/// 執(zhí)行
/// </summary>
public override async Task Invoke( AspectContext context, AspectDelegate next ) {
var cache = GetCache( context );
var returnType = GetReturnType( context );
var key = CreateCacheKey( context );
var value = await GetCacheValue( cache, returnType, key );
if ( value != null ) {
SetReturnValue( context, returnType, value );
return;
}
await next( context );
await SetCache( context, cache, key );
}
/// <summary>
/// 獲取緩存服務(wù)
/// </summary>
protected virtual ICache GetCache( AspectContext context ) {
return context.ServiceProvider.GetService<ICache>();
}
/// <summary>
/// 獲取返回類型
/// </summary>
private Type GetReturnType( AspectContext context ) {
return context.IsAsync() ? context.ServiceMethod.ReturnType.GetGenericArguments().First() : context.ServiceMethod.ReturnType;
}
/// <summary>
/// 創(chuàng)建緩存鍵
/// </summary>
private string CreateCacheKey( AspectContext context ) {
var keyGenerator = context.ServiceProvider.GetService<ICacheKeyGenerator>();
return keyGenerator.CreateCacheKey( context.ServiceMethod, context.Parameters, CacheKeyPrefix );
}
/// <summary>
/// 獲取緩存值
/// </summary>
private async Task<object> GetCacheValue( ICache cache, Type returnType, string key ) {
return await cache.GetAsync( key, returnType );
}
/// <summary>
/// 設(shè)置返回值
/// </summary>
private void SetReturnValue( AspectContext context, Type returnType, object value ) {
if ( context.IsAsync() ) {
context.ReturnValue = typeof( Task ).GetMethods()
.First( p => p.Name == "FromResult" && p.ContainsGenericParameters )
.MakeGenericMethod( returnType ).Invoke( null, new[] { value } );
return;
}
context.ReturnValue = value;
}
/// <summary>
/// 設(shè)置緩存
/// </summary>
private async Task SetCache( AspectContext context, ICache cache, string key ) {
var options = new CacheOptions { Expiration = TimeSpan.FromSeconds( Expiration ) };
var returnValue = context.IsAsync() ? await context.UnwrapAsyncReturnValue() : context.ReturnValue;
await cache.SetAsync( key, returnValue, options );
}
}
創(chuàng)建參數(shù)攔截器
繼承 Util.Aop.ParameterInterceptorBase 基類,重寫 Invoke 方法.
與方法攔截器類似, Invoke 也提供了兩個(gè)參數(shù) ParameterAspectContext 和 ParameterAspectDelegate.
ParameterAspectContext 上下文提供方法元數(shù)據(jù).
ParameterAspectDelegate 表示攔截的方法.
下面演示了 [NotNull] 參數(shù)攔截器.
在方法執(zhí)行前判斷參數(shù)是否為 null,如果為 null 拋出異常,不會(huì)執(zhí)行攔截方法.
/// <summary>
/// 驗(yàn)證參數(shù)不能為null
/// </summary>
public class NotNullAttribute : ParameterInterceptorBase {
/// <summary>
/// 執(zhí)行
/// </summary>
public override Task Invoke( ParameterAspectContext context, ParameterAspectDelegate next ) {
if( context.Parameter.Value == null )
throw new ArgumentNullException( context.Parameter.Name );
return next( context );
}
}
性能優(yōu)化
AddAop 配置方法默認(rèn)不帶參數(shù),所有添加到 Ioc 容器的服務(wù)都會(huì)創(chuàng)建代理類,并啟用參數(shù)攔截器.
AspectCore AOP 參數(shù)攔截器對(duì)啟動(dòng)性能有很大的影響.
默認(rèn)配置適合規(guī)模較小的項(xiàng)目.
當(dāng)你在Ioc容器注冊了上千個(gè)甚至更多的服務(wù)時(shí),啟動(dòng)時(shí)間將顯著增長,因?yàn)閱?dòng)時(shí)需要?jiǎng)?chuàng)建大量的代理類.
有幾個(gè)方法可以優(yōu)化 AspectCore AOP 啟動(dòng)性能.
-
拆分項(xiàng)目
對(duì)于微服務(wù)架構(gòu),單個(gè)項(xiàng)目包含的接口應(yīng)該不會(huì)特別多.
如果發(fā)現(xiàn)由于創(chuàng)建代理類導(dǎo)致啟動(dòng)時(shí)間過長,可以拆分項(xiàng)目.
但對(duì)于單體架構(gòu),不能通過拆分項(xiàng)目的方式解決.
-
減少創(chuàng)建的代理類.
Util定義了一個(gè)AOP標(biāo)記接口 IAopProxy ,只有繼承了 IAopProxy 的接口才會(huì)創(chuàng)建代理類.
要啟用 IAopProxy 標(biāo)記接口,只需向 AddAop 傳遞 true .
var builder = WebApplication.CreateBuilder( args ); builder.AsBuild().AddAop( true );
現(xiàn)在只有明確繼承自 IAopProxy 的接口才會(huì)創(chuàng)建代理類,代理類的數(shù)量將大幅減少.
應(yīng)用服務(wù)和領(lǐng)域服務(wù)接口默認(rèn)繼承了 IAopProxy.
如果你在其它構(gòu)造塊使用了攔截器,比如倉儲(chǔ),需要讓你的倉儲(chǔ)接口繼承 IAopProxy.
-
禁用參數(shù)攔截器.
如果啟用了 IAopProxy 標(biāo)記接口,啟動(dòng)性能依然未達(dá)到你的要求,可以禁用參數(shù)攔截器.
AddAop 擴(kuò)展方法支持傳入 Action<IAspectConfiguration> 參數(shù),可以覆蓋默認(rèn)設(shè)置.
下面的例子禁用了參數(shù)攔截器,并為所有繼承了 IAopProxy 的接口創(chuàng)建代理.
var builder = WebApplication.CreateBuilder( args ); builder.AsBuild().AddAop( options => options.NonAspectPredicates.Add( t => !IsProxy( t.DeclaringType ) ) ); /// <summary> /// 是否創(chuàng)建代理 /// </summary> private static bool IsProxy( Type type ) { if ( type == null ) return false; var interfaces = type.GetInterfaces(); if ( interfaces == null || interfaces.Length == 0 ) return false; foreach ( var item in interfaces ) { if ( item == typeof( IAopProxy ) ) return true; } return false; }
源碼解析
AppBuilderExtensions
擴(kuò)展了 AddAop 配置方法.
isEnableIAopProxy 參數(shù)用于啟用 IAopProxy 標(biāo)記接口.
Action<IAspectConfiguration> 參數(shù)用于覆蓋默認(rèn)配置.
/// <summary>
/// Aop配置擴(kuò)展
/// </summary>
public static class AppBuilderExtensions {
/// <summary>
/// 啟用AspectCore攔截器
/// </summary>
/// <param name="builder">應(yīng)用生成器</param>
public static IAppBuilder AddAop( this IAppBuilder builder ) {
return builder.AddAop( false );
}
/// <summary>
/// 啟用AspectCore攔截器
/// </summary>
/// <param name="builder">應(yīng)用生成器</param>
/// <param name="isEnableIAopProxy">是否啟用IAopProxy接口標(biāo)記</param>
public static IAppBuilder AddAop( this IAppBuilder builder,bool isEnableIAopProxy ) {
return builder.AddAop( null, isEnableIAopProxy );
}
/// <summary>
/// 啟用AspectCore攔截器
/// </summary>
/// <param name="builder">應(yīng)用生成器</param>
/// <param name="setupAction">AspectCore攔截器配置操作</param>
public static IAppBuilder AddAop( this IAppBuilder builder, Action<IAspectConfiguration> setupAction ) {
return builder.AddAop( setupAction, false );
}
/// <summary>
/// 啟用AspectCore攔截器
/// </summary>
/// <param name="builder">應(yīng)用生成器</param>
/// <param name="setupAction">AspectCore攔截器配置操作</param>
/// <param name="isEnableIAopProxy">是否啟用IAopProxy接口標(biāo)記</param>
private static IAppBuilder AddAop( this IAppBuilder builder, Action<IAspectConfiguration> setupAction, bool isEnableIAopProxy ) {
builder.CheckNull( nameof( builder ) );
builder.Host.UseServiceProviderFactory( new DynamicProxyServiceProviderFactory() );
builder.Host.ConfigureServices( ( context, services ) => {
ConfigureDynamicProxy( services, setupAction, isEnableIAopProxy );
RegisterAspectScoped( services );
} );
return builder;
}
/// <summary>
/// 配置攔截器
/// </summary>
private static void ConfigureDynamicProxy( IServiceCollection services, Action<IAspectConfiguration> setupAction, bool isEnableIAopProxy ) {
services.ConfigureDynamicProxy( config => {
if ( setupAction == null ) {
config.NonAspectPredicates.Add( t => !IsProxy( t.DeclaringType, isEnableIAopProxy ) );
config.EnableParameterAspect();
return;
}
setupAction.Invoke( config );
} );
}
/// <summary>
/// 是否創(chuàng)建代理
/// </summary>
private static bool IsProxy( Type type, bool isEnableIAopProxy ) {
if ( type == null )
return false;
if ( isEnableIAopProxy == false ) {
if ( type.SafeString().Contains( "Xunit.DependencyInjection.ITestOutputHelperAccessor" ) )
return false;
return true;
}
var interfaces = type.GetInterfaces();
if ( interfaces == null || interfaces.Length == 0 )
return false;
foreach ( var item in interfaces ) {
if ( item == typeof( IAopProxy ) )
return true;
}
return false;
}
/// <summary>
/// 注冊攔截器服務(wù)
/// </summary>
private static void RegisterAspectScoped( IServiceCollection services ) {
services.AddScoped<IAspectScheduler, ScopeAspectScheduler>();
services.AddScoped<IAspectBuilderFactory, ScopeAspectBuilderFactory>();
services.AddScoped<IAspectContextFactory, ScopeAspectContextFactory>();
}
}
Util.Aop.IAopProxy
IAopProxy 是一個(gè)標(biāo)記接口,繼承了它的接口才會(huì)創(chuàng)建代理類.
/// <summary>
/// Aop代理標(biāo)記
/// </summary>
public interface IAopProxy {
}
Util.Aop.InterceptorBase
InterceptorBase 是方法攔截器基類.
它是一個(gè)簡單抽象層, 未來可能提供一些共享方法.
/// <summary>
/// 攔截器基類
/// </summary>
public abstract class InterceptorBase : AbstractInterceptorAttribute {
}
Util.Aop.ParameterInterceptorBase
ParameterInterceptorBase 是參數(shù)攔截器基類.文章來源:http://www.zghlxwxcb.cn/news/detail-743513.html
/// <summary>
/// 參數(shù)攔截器基類
/// </summary>
public abstract class ParameterInterceptorBase : ParameterInterceptorAttribute {
}
Util.Aop.IgnoreAttribute
[Util.Aop.Ignore] 用于禁止創(chuàng)建代理類.文章來源地址http://www.zghlxwxcb.cn/news/detail-743513.html
/// <summary>
/// 忽略攔截
/// </summary>
public class IgnoreAttribute : NonAspectAttribute {
}
到了這里,關(guān)于Util應(yīng)用框架基礎(chǔ)(三) - 面向切面編程(AspectCore AOP)的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!