Norns.Urd輕量級 AOP 框架
Norns.Urd 是一個基于 emit 實現(xiàn)動態(tài)代理的輕量級 AOP 框架。
版本基于 netstandard2.0. 所以哪些.net 版本能用你懂的。
完成這個框架的目的主要出自于個人以下意愿:
- 靜態(tài)AOP和動態(tài)AOP都實現(xiàn)一次
- 如果不實現(xiàn)DI,怎么將AOP框架實現(xiàn)與其他現(xiàn)有DI框架集成
- 一個AOP 如何將 sync 和 async 方法同時兼容且如何將實現(xiàn)選擇權(quán)完全交予用戶
希望該庫能對大家有些小小的作用
對了,如果不了解AOP的同學(xué),可以看看這些文章:
Simple Benchmark
只是一個簡單性能測試,不代表全部場景,也沒有故意對比,
Castle 和 AspectCore 都是非常優(yōu)秀的庫,
Norns.Urd 很多實現(xiàn)都是參考了Castle 和 AspectCore的源碼的。
BenchmarkDotNet=v0.12.1, OS=Windows 10.0.18363.1198 (1909/November2018Update/19H2)
Intel Core i7-9750H CPU 2.60GHz, 1 CPU, 12 logical and 6 physical cores
.NET Core SDK=5.0.100
[Host] : .NET Core 5.0.0 (CoreCLR 5.0.20.51904, CoreFX 5.0.20.51904), X64 RyuJIT
DefaultJob : .NET Core 5.0.0 (CoreCLR 5.0.20.51904, CoreFX 5.0.20.51904), X64 RyuJIT
| Method | Mean | Error | StdDev | Median | Gen 0 | Gen 1 | Gen 2 | Allocated |
|---|---|---|---|---|---|---|---|---|
| TransientInstanceCallSyncMethodWhenNoAop | 69.10 ns | 1.393 ns | 2.512 ns | 69.70 ns | 0.0178 | - | - | 112 B |
| TransientInstanceCallSyncMethodWhenNornsUrd | 148.38 ns | 2.975 ns | 5.588 ns | 145.76 ns | 0.0534 | - | - | 336 B |
| TransientInstanceCallSyncMethodWhenCastle | 222.48 ns | 0.399 ns | 0.312 ns | 222.50 ns | 0.0815 | - | - | 512 B |
| TransientInstanceCallSyncMethodWhenAspectCore | 576.04 ns | 7.132 ns | 10.229 ns | 573.46 ns | 0.1030 | - | - | 648 B |
| TransientInstanceCallAsyncMethodWhenNoAop | 114.61 ns | 0.597 ns | 0.499 ns | 114.58 ns | 0.0408 | - | - | 256 B |
| TransientInstanceCallAsyncMethodWhenNornsUrd | 206.36 ns | 0.937 ns | 0.830 ns | 206.18 ns | 0.0763 | - | - | 480 B |
| TransientInstanceCallAsyncMethodWhenCastle | 250.98 ns | 3.315 ns | 3.101 ns | 252.16 ns | 0.1044 | - | - | 656 B |
| TransientInstanceCallAsyncMethodWhenAspectCore | 576.00 ns | 4.160 ns | 3.891 ns | 574.99 ns | 0.1373 | - | - | 864 B |
快速入門指南
這是一個簡單的全局AOP攔截的簡單示例,具體詳細(xì)示例代碼可以參閱Examples.WebApi
-
創(chuàng)建 ConsoleInterceptor.cs
using Norns.Urd; using Norns.Urd.Reflection; using System; using System.Threading.Tasks; namespace Examples.WebApi { public class ConsoleInterceptor : AbstractInterceptor { public override async Task InvokeAsync(AspectContext context, AsyncAspectDelegate next) { Console.WriteLine($"{context.Service.GetType().GetReflector().FullDisplayName}.{context.Method.GetReflector().DisplayName}"); await next(context); } } } -
設(shè)置 WeatherForecastController 的方法為 virtual
[ApiController] [Route("[controller]")] public class WeatherForecastController : ControllerBase { [HttpGet] public virtual IEnumerable<WeatherForecast> Get() => test.Get(); } -
AddControllersAsServices
// This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { services.AddControllers().AddControllersAsServices(); } -
設(shè)置di 容器啟用aop 功能
// This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { services.AddControllers().AddControllersAsServices(); services.ConfigureAop(i => i.GlobalInterceptors.Add(new ConsoleInterceptor())); } -
運(yùn)行程序
你會在控制臺看見如下輸出
Norns.Urd.DynamicProxy.Generated.WeatherForecastController_Proxy_Inherit.IEnumerable<WeatherForecast> Get()
功能說明
Interceptor 攔截器
在Norns.Urd中,Interceptor 攔截器是用戶可以在方法插入自己的邏輯的核心。
攔截器結(jié)構(gòu)定義
攔截器定義了標(biāo)準(zhǔn)結(jié)構(gòu)為IInterceptor
public interface IInterceptor
{
// 用戶可以通過Order自定義攔截器順序,排序方式為ASC,全局?jǐn)r截器和顯示攔截器都會列入排序中
int Order { get; }
// 同步攔截方法
void Invoke(AspectContext context, AspectDelegate next);
// 異步攔截方法
Task InvokeAsync(AspectContext context, AsyncAspectDelegate next);
// 可以設(shè)置攔截器如何選擇過濾是否攔截方法,除了這里還有NonAspectAttribute 和全局的NonPredicates可以影響過濾
bool CanAspect(MethodInfo method);
}
攔截器結(jié)類型
攔截器實際從設(shè)計上只有IInterceptor這一個統(tǒng)一的定義,不過由于csharp的單繼承和Attribute的語言限制,所以有AbstractInterceptorAttribute 和 AbstractInterceptor兩個類。
AbstractInterceptorAttribute (顯示攔截器)
public abstract class AbstractInterceptorAttribute : Attribute, IInterceptor
{
public virtual int Order { get; set; }
public virtual bool CanAspect(MethodInfo method) => true;
// 默認(rèn)提供在同步攔截器方法中轉(zhuǎn)換異步方法為同步方式調(diào)用,存在一些性能損失,如果用戶想要減少這方面的損耗,可以選擇重載實現(xiàn)。
public virtual void Invoke(AspectContext context, AspectDelegate next)
{
InvokeAsync(context, c =>
{
next(c);
return Task.CompletedTask;
}).ConfigureAwait(false)
.GetAwaiter()
.GetResult();
}
// 默認(rèn)只需要實現(xiàn)異步攔截器方法
public abstract Task InvokeAsync(AspectContext context, AsyncAspectDelegate next);
}
一個攔截器實現(xiàn)舉例:
public class AddTenInterceptorAttribute : AbstractInterceptorAttribute
{
public override void Invoke(AspectContext context, AspectDelegate next)
{
next(context);
AddTen(context);
}
private static void AddTen(AspectContext context)
{
if (context.ReturnValue is int i)
{
context.ReturnValue = i + 10;
}
else if(context.ReturnValue is double d)
{
context.ReturnValue = d + 10.0;
}
}
public override async Task InvokeAsync(AspectContext context, AsyncAspectDelegate next)
{
await next(context);
AddTen(context);
}
}
InterceptorAttribute攔截器使用方式
- interface / class / method 可以設(shè)置
Attribute,如
[AddTenInterceptor]
public interface IGenericTest<T, R> : IDisposable
{
// or
//[AddTenInterceptor]
T GetT();
}
- 全局?jǐn)r截器中也可以設(shè)置
public void ConfigureServices(IServiceCollection services)
{
services.ConfigureAop(i => i.GlobalInterceptors.Add(new AddTenInterceptorAttribute()));
}
AbstractInterceptor
和 AbstractInterceptorAttribute 幾乎一模一樣,不過不是Attribute,不能用于對應(yīng)場景,只能在全局?jǐn)r截器中使用。其實本身就是提供給用戶用于不想Attribute場景簡化Interceptor創(chuàng)建。
Interceptor攔截器使用方式
只能在全局?jǐn)r截器中設(shè)置
public void ConfigureServices(IServiceCollection services)
{
services.ConfigureAop(i => i.GlobalInterceptors.Add(new AddSixInterceptor()));
}
全局?jǐn)r截器 vs 顯示攔截器
- 全局?jǐn)r截器,是針對所有可以代理的方法都會做攔截,只需一次聲明,全局有效
public void ConfigureServices(IServiceCollection services)
{
services.ConfigureAop(i => i.GlobalInterceptors.Add(new AddSixInterceptor()));
}
- 顯示攔截器必須使用
AbstractInterceptorAttribute在所有需要的地方都顯示聲明
[AddTenInterceptor]
public interface IGenericTest<T, R> : IDisposable
{
// or
//[AddTenInterceptor]
T GetT();
}
所以用戶覺得怎么樣方便就怎么用就好了
攔截器的過濾方式
Norns.Urd 提供如下三種過濾方式
- 全局過濾
services.ConfigureAop(i => i.NonPredicates.AddNamespace("Norns")
.AddNamespace("Norns.*")
.AddNamespace("System")
.AddNamespace("System.*")
.AddNamespace("Microsoft.*")
.AddNamespace("Microsoft.Owin.*")
.AddMethod("Microsoft.*", "*"));
- 顯示過濾
[NonAspect]
public interface IGenericTest<T, R> : IDisposable
{
}
- 攔截器本身的過濾
public class ParameterInjectInterceptor : AbstractInterceptor
{
public override bool CanAspect(MethodInfo method)
{
return method.GetReflector().Parameters.Any(i => i.IsDefined<InjectAttribute>());
}
}
AOP限制
- 當(dāng) service type 為 class 時, 只有 virtual 且 子類能有訪問的 方法才能代理攔截
- 有方法參數(shù)為 in readonly struct 的類型無法代理
Interface和Abstract Class的默認(rèn)實現(xiàn)
如果你向DI框架注冊沒有真正有具體實現(xiàn)的 Interface和Abstract Class, Norns.Urd 會實現(xiàn)默認(rèn)的子類型。
為什么提供這樣的功能呢?
這是為聲明式編碼思想提供一些底層實現(xiàn)支持,這樣有更多的同學(xué)可以自定義自己的一些聲明式庫,簡化代碼,比如實現(xiàn)一個 聲明式HttpClient
默認(rèn)實現(xiàn)限制
- 不支持屬性注入
- Norns.Urd 生成的默認(rèn)實現(xiàn)皆為返回類型的默認(rèn)值
demo
后面會完成一個簡單的httpclient作為示例,這里先做個簡單demo
- 假如要加 10 就是我們類似http調(diào)用的邏輯,我們就可以講全部的加10邏輯放在攔截器中
public class AddTenAttribute : AbstractInterceptorAttribute
{
public override void Invoke(AspectContext context, AspectDelegate next)
{
next(context);
AddTen(context);
}
private static void AddTen(AspectContext context)
{
if (context.ReturnValue is int i)
{
context.ReturnValue = i + 10;
}
else if(context.ReturnValue is double d)
{
context.ReturnValue = d + 10.0;
}
}
public override async Task InvokeAsync(AspectContext context, AsyncAspectDelegate next)
{
await next(context);
AddTen(context);
}
}
- 定義聲明式client
[AddTen]
public interface IAddTest
{
int AddTen();
// 對于接口中的默認(rèn)實現(xiàn),并不會被Norns.Urd替代,這樣可以提供某些場景用戶可以自定義實現(xiàn)邏輯
public int NoAdd() => 3;
}
- 注冊client
services.AddTransient<IAddTest>();
services.ConfigureAop();
- 使用
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
IAddTest a;
public WeatherForecastController(IAddTest b)
{
a = b;
}
[HttpGet]
public int GetAddTen() => a.AddTen();
}
InjectAttribute
InjectAttribute 是對 Interface和Abstract Class的默認(rèn)實現(xiàn)的功能補(bǔ)充,
特別是在做聲明式client之類,提供自定義設(shè)置,比如interface 默認(rèn)接口實現(xiàn)時,
用戶可能需要從DI中獲取實例,所以這里提供兩種方式做一些補(bǔ)充。
ParameterInject
方法參數(shù)可以設(shè)置InjectAttribute:
- 當(dāng)參數(shù)為null時,就會從 DI 中嘗試獲取實例
- 當(dāng)參數(shù)不為null時,不會覆蓋傳值,依然時傳參值
示例:
public interface IInjectTest
{
public ParameterInjectTest T([Inject] ParameterInjectTest t = null) => t;
}
PropertyInject
public interface IInjectTest
{
[Inject]
ParameterInjectInterceptorTest PT { get; set; }
}
FieldInject
按照業(yè)界編碼習(xí)慣, field 不推薦沒有賦值就是使用,所以該功能會導(dǎo)致代碼檢查出現(xiàn)需要修復(fù)的問題
public class ParameterInjectTest : IInjectTest
{
[Inject]
ParameterInjectInterceptorTest ft;
}
FallbackAttribute
public class DoFallbackTest
{
[Fallback(typeof(TestFallback))] // just need set Interceptor Type
public virtual int Do(int i)
{
throw new FieldAccessException();
}
[Fallback(typeof(TestFallback))]
public virtual Task<int> DoAsync(int i)
{
throw new FieldAccessException();
}
}
public class TestFallback : AbstractInterceptor
{
public override void Invoke(AspectContext context, AspectDelegate next)
{
context.ReturnValue = (int)context.Parameters[0];
}
public override Task InvokeAsync(AspectContext context, AsyncAspectDelegate next)
{
var t = Task.FromResult((int)context.Parameters[0]);
context.ReturnValue = t;
return t;
}
}
Polly
Polly is .NET resilience and transient-fault-handling library.
這里通過Norns.Urd將Polly的各種功能集成為更加方便使用的功能
如何啟用 Norns.Urd + Polly, 只需使用EnablePolly()
如:
new ServiceCollection()
.AddTransient<DoTimeoutTest>()
.ConfigureAop(i => i.EnablePolly())
TimeoutAttribute
[Timeout(seconds: 1)] // timeout 1 seconds, when timeout will throw TimeoutRejectedException
double Wait(double seconds);
[Timeout(timeSpan: "00:00:00.100")] // timeout 100 milliseconds, only work on async method when no CancellationToken
async Task<double> WaitAsync(double seconds, CancellationToken cancellationToken = default);
[Timeout(timeSpan: "00:00:01")] // timeout 1 seconds, but no work on async method when no CancellationToken
async Task<double> NoCancellationTokenWaitAsync(double seconds);
RetryAttribute
[Retry(retryCount: 2, ExceptionType = typeof(AccessViolationException))] // retry 2 times when if throw Exception
void Do()
CircuitBreakerAttribute
[CircuitBreaker(exceptionsAllowedBeforeBreaking: 3, durationOfBreak: "00:00:01")]
//or
[AdvancedCircuitBreaker(failureThreshold: 0.1, samplingDuration: "00:00:01", minimumThroughput: 3, durationOfBreak: "00:00:01")]
void Do()
BulkheadAttribute
[Bulkhead(maxParallelization: 5, maxQueuingActions: 10)]
void Do()
Norns.Urd 中的一些設(shè)計
Norns.Urd的實現(xiàn)前提
由于Norns.Urd的實現(xiàn)基于以下兩點(diǎn)前提
-
將 sync 和 async 方法同時兼容且如何將實現(xiàn)選擇權(quán)完全交予用戶
- 其實這點(diǎn)還好,工作量變成兩倍多一些就好,sync 和 async 完全拆分成兩套實現(xiàn)。
- 提供給用戶的Interceptor接口要提供 sync 和 async 混合在一套實現(xiàn)代碼的方案,畢竟不能強(qiáng)迫用戶實現(xiàn)兩套代碼,很多場景用戶不需要為sync 和 async 的差異而實現(xiàn)兩套代碼
-
不包含任何內(nèi)置DI,但要整體都為支持DI而作
- 其實如果內(nèi)置DI容器可以讓支持 generic 場景變得非常簡單,畢竟從DI容器中實例化對象時必須有明確的類型,但是呢,現(xiàn)在已經(jīng)有了那么多實現(xiàn)的庫了,我就不想為了一些場景而實現(xiàn)很多功能(我真的懶,否則這個庫也不會寫那么久了)
- 但是DI容器確實解耦非常棒,我自己都常常因此受益而減少了很多代碼修改量,所以做一個aop庫必須要考慮基于DI容器做支持,這樣的話,di 支持的 open generic / 自定義實例化方法都要做支持,并且aop里面還得提供用戶調(diào)用DI的方法,否則還不好用了 (這樣算下來,我真的偷懶了嗎?我是不是在給自己挖坑呀?)
如何設(shè)計解決的?
目前方案不一定完美,暫時算解決了問題而已 (有更好方案請一定要告訴我,我迫切需要學(xué)習(xí))
提供什么樣的攔截器編寫模式給用戶?
以前接觸一些其他aop實現(xiàn)框架,很多都需要將攔截代碼分為 方法前 / 方法后 / 有異常等等,個人覺得這樣的形式還是一定程度上影響攔截器實現(xiàn)的代碼思路,總覺得不夠順滑
但是像 ASP.NET Core Middleware就感覺非常不錯,如下圖和代碼:
app.Run(async context =>
{
await context.Response.WriteAsync("Hello, World!");
});
攔截器也應(yīng)該可以像這樣做,所以攔截器的代碼應(yīng)該可以像這樣:
public class ConsoleInterceptor
{
public async Task InvokeAsync(Context context, Delegate next)
{
Console.WriteLine("Hello, World!");
await next(context);
}
}
sync 和 async 方法如何拆分?又如何能合并在一起呢?用戶有怎么自己選擇實現(xiàn)sync 還是 async 或者兩個都都實現(xiàn)呢?
public delegate Task AsyncAspectDelegate(AspectContext context);
public delegate void AspectDelegate(AspectContext context);
// 拆分:
// 由AspectDelegate 和 AsyncAspectDelegate 建立兩套完全區(qū)分 sync 和 async 的Middleware調(diào)用鏈,具體使用哪個由具體被攔截的方法本身決定
public abstract class AbstractInterceptor : IInterceptor
{
public virtual void Invoke(AspectContext context, AspectDelegate next)
{
InvokeAsync(context, c =>
{
next(c);
return Task.CompletedTask;
}).ConfigureAwait(false)
.GetAwaiter()
.GetResult();
}
// 合并:
// 默認(rèn)實現(xiàn)轉(zhuǎn)換方法內(nèi)容,這樣各種攔截器都可以混在一個Middleware調(diào)用鏈中
public abstract Task InvokeAsync(AspectContext context, AsyncAspectDelegate next);
// 用戶自主性選擇:
// 同時提供sync 和 async 攔截器方法可以重載,用戶就可以自己選擇了
// 所以用戶在 async 中可以調(diào)用專門的未異步優(yōu)化代碼了,也不用說在 sync 中必須 awit 會影響性能了,
// 你認(rèn)為影響性能,你在乎就自己都重載,不在乎那就自己選
}
沒有內(nèi)置DI,如何兼容其他DI框架呢?
DI框架都有注冊類型,我們可以通過 emit 生成代理類,替換原本的注冊,就可以做到兼容。
當(dāng)然每種DI框架都需要定制化的實現(xiàn)一些代碼才能支持(唉,又是工作量呀)
AddTransient<IMTest>(x => new NMTest()), 類似這樣的實例化方法怎么支持呢?
由于這種DI框架的用法,無法通過Func函數(shù)拿到實際會使用的類型,只能根據(jù)IMTest定義通過emit 生成 橋接代理類型,其偽碼類似如下:
interface IMTest
{
int Get(int i);
}
class IMTestProxy : IMTest
{
IMTest instance = (x => new NMTest())();
int Get(int i) => instance.Get(i);
}
.AddTransient(typeof(IGenericTest<,>), typeof(GenericTest<,>)) 類似這樣的 Open generic 怎么支持呢?
其實對于泛型,我們通過 emit 生成泛型類型一點(diǎn)問題都沒有,唯一的難點(diǎn)是不好生成 Get<T>() 這樣的方法調(diào)用, 因為IL需要反射找到的具體方法,比如Get<int>() Get<bool>() 等等,不能是不明確的 Get<T>()。
要解決這個問題就只能將實際的調(diào)用延遲到運(yùn)行時調(diào)用再生成具體的調(diào)用,偽碼大致如下:
interface GenericTest<T,R>
{
T Get<T>(T i) => i;
}
class GenericTestProxy<T,R> : GenericTest<T,R>
{
T Get<T>(T i) => this.GetType().GetMethod("Get<T>").Invoke(i);
}
