Norns.Urd轻量级 AOP 框架

联合创作 · 2023-09-30 06:54

Norns.Urd 是一个基于 emit 实现动态代理的轻量级 AOP 框架。

版本基于 netstandard2.0. 所以哪些.net 版本能用你懂的。

完成这个框架的目的主要出自于个人以下意愿:

  • 静态AOP和动态AOP都实现一次
  • 如果不实现DI,怎么将AOP框架实现与其他现有DI框架集成
  • 一个AOP 如何将 sync 和 async 方法同时兼容且如何将实现选择权完全交予用户

希望该库能对大家有些小小的作用

对了,如果不了解AOP的同学,可以看看这些文章:

面向切面的程序设计

什么是面向切面编程AOP?

AOP 有几种实现方式?

Simple Benchmark

只是一个简单性能测试,不代表全部场景,也没有故意对比,

Castle 和 AspectCore 都是非常优秀的库,

Norns.Urd 很多实现都是参考了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拦截的简单示例,具体详细示例代码可以参阅Examples.WebApi

  1. 创建 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);
             }
         }
     }
    
  2. 设置 WeatherForecastController 的方法为 virtual

     [ApiController]
     [Route("[controller]")]
     public class WeatherForecastController : ControllerBase
     {
         [HttpGet]
         public virtual IEnumerable<WeatherForecast> Get() => test.Get();
     }
    
  3. 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();
     }
    
  4. 设置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()));
     }
    
  5. 运行程序

    你会在控制台看见如下输出

     Norns.Urd.DynamicProxy.Generated.WeatherForecastController_Proxy_Inherit.IEnumerable<WeatherForecast> Get()
    

功能说明

Interceptor 拦截器

在Norns.Urd中,Interceptor 拦截器是用户可以在方法插入自己的逻辑的核心。

拦截器结构定义

拦截器定义了标准结构为IInterceptor

public interface IInterceptor
{
    // 用户可以通过Order自定义拦截器顺序,排序方式为ASC,全局拦截器和显示拦截器都会列入排序中
    int Order { get; }

    // 同步拦截方法
    void Invoke(AspectContext context, AspectDelegate next);

    // 异步拦截方法
    Task InvokeAsync(AspectContext context, AsyncAspectDelegate next);

    // 可以设置拦截器如何选择过滤是否拦截方法,除了这里还有NonAspectAttribute 和全局的NonPredicates可以影响过滤
    bool CanAspect(MethodInfo method);
}

拦截器结类型

拦截器实际从设计上只有IInterceptor这一个统一的定义,不过由于csharp的单继承和Attribute的语言限制,所以有AbstractInterceptorAttribute 和 AbstractInterceptor两个类。

AbstractInterceptorAttribute (显示拦截器)

public abstract class AbstractInterceptorAttribute : Attribute, IInterceptor
{
    public virtual int Order { get; set; }

    public virtual bool CanAspect(MethodInfo method) => true;

    // 默认提供在同步拦截器方法中转换异步方法为同步方式调用,存在一些性能损失,如果用户想要减少这方面的损耗,可以选择重载实现。
    public virtual void Invoke(AspectContext context, AspectDelegate next)
    {
        InvokeAsync(context, c =>
        {
            next(c);
            return Task.CompletedTask;
        }).ConfigureAwait(false)
                    .GetAwaiter()
                    .GetResult();
    }

    // 默认只需要实现异步拦截器方法
    public abstract Task InvokeAsync(AspectContext context, AsyncAspectDelegate next);
}

一个拦截器实现举例:

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 可以设置 Attribute,如
[AddTenInterceptor]
public interface IGenericTest<T, R> : IDisposable
{
    // or
    //[AddTenInterceptor]
    T GetT();
}
  • 全局拦截器中也可以设置
public void ConfigureServices(IServiceCollection services)
{
    services.ConfigureAop(i => i.GlobalInterceptors.Add(new AddTenInterceptorAttribute()));
}

AbstractInterceptor

和 AbstractInterceptorAttribute 几乎一模一样,不过不是Attribute,不能用于对应场景,只能在全局拦截器中使用。其实本身就是提供给用户用于不想Attribute场景简化Interceptor创建。

Interceptor拦截器使用方式

只能在全局拦截器中设置

public void ConfigureServices(IServiceCollection services)
{
    services.ConfigureAop(i => i.GlobalInterceptors.Add(new AddSixInterceptor()));
}

全局拦截器 vs 显示拦截器

  • 全局拦截器,是针对所有可以代理的方法都会做拦截,只需一次声明,全局有效
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限制

  • 当 service type 为 class 时, 只有 virtual 且 子类能有访问的 方法才能代理拦截
  • 有方法参数为 in readonly struct 的类型无法代理

Interface和Abstract Class的默认实现

如果你向DI框架注册没有真正有具体实现的 InterfaceAbstract Class, Norns.Urd 会实现默认的子类型。

为什么提供这样的功能呢?

这是为声明式编码思想提供一些底层实现支持,这样有更多的同学可以自定义自己的一些声明式库,简化代码,比如实现一个 声明式HttpClient

默认实现限制

  • 不支持属性注入
  • Norns.Urd 生成的默认实现皆为返回类型的默认值

demo

后面会完成一个简单的httpclient作为示例,这里先做个简单demo

  1. 假如要加 10 就是我们类似http调用的逻辑,我们就可以讲全部的加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);
    }
}
  1. 定义声明式client
[AddTen]
public interface IAddTest
{
    int AddTen();

    // 对于接口中的默认实现,并不会被Norns.Urd替代,这样可以提供某些场景用户可以自定义实现逻辑
    public int NoAdd() => 3;
}
  1. 注册client
services.AddTransient<IAddTest>();
services.ConfigureAop();
  1. 使用
[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的默认实现的功能补充,

特别是在做声明式client之类,提供自定义设置,比如interface 默认接口实现时,

用户可能需要从DI中获取实例,所以这里提供两种方式做一些补充。

ParameterInject

方法参数可以设置InjectAttribute

  • 当参数为null时,就会从 DI 中尝试获取实例
  • 当参数不为null时,不会覆盖传值,依然时传参值

示例:

public interface IInjectTest
{
    public ParameterInjectTest T([Inject] ParameterInjectTest t = null) => t;
}

PropertyInject

public interface IInjectTest
{
    [Inject]
    ParameterInjectInterceptorTest PT { get; set; }
}

FieldInject

按照业界编码习惯, field 不推荐没有赋值就是使用,所以该功能会导致代码检查出现需要修复的问题

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 中的一些设计

Norns.Urd的实现前提

由于Norns.Urd的实现基于以下两点前提

  1. 将 sync 和 async 方法同时兼容且如何将实现选择权完全交予用户

    • 其实这点还好,工作量变成两倍多一些就好,sync 和 async 完全拆分成两套实现。
    • 提供给用户的Interceptor接口要提供 sync 和 async 混合在一套实现代码的方案,毕竟不能强迫用户实现两套代码,很多场景用户不需要为sync 和 async 的差异而实现两套代码
  2. 不包含任何内置DI,但要整体都为支持DI而作

    • 其实如果内置DI容器可以让支持 generic 场景变得非常简单,毕竟从DI容器中实例化对象时必须有明确的类型,但是呢,现在已经有了那么多实现的库了,我就不想为了一些场景而实现很多功能(我真的懒,否则这个库也不会写那么久了)
    • 但是DI容器确实解耦非常棒,我自己都常常因此受益而减少了很多代码修改量,所以做一个aop库必须要考虑基于DI容器做支持,这样的话,di 支持的 open generic / 自定义实例化方法都要做支持,并且aop里面还得提供用户调用DI的方法,否则还不好用了 (这样算下来,我真的偷懒了吗?我是不是在给自己挖坑呀?)

如何设计解决的?

目前方案不一定完美,暂时算解决了问题而已 (有更好方案请一定要告诉我,我迫切需要学习)

提供什么样的拦截器编写模式给用户?

以前接触一些其他aop实现框架,很多都需要将拦截代码分为 方法前 / 方法后 / 有异常等等,个人觉得这样的形式还是一定程度上影响拦截器实现的代码思路,总觉得不够顺滑

但是像 ASP.NET Core Middleware就感觉非常不错,如下图和代码:

https://docs.microsoft.com/en-us/aspnet/core/fundamentals/middleware/index/_static/request-delegate-pipeline.png?view=aspnetcore-5.0

app.Run(async context =>
{
    await context.Response.WriteAsync("Hello, World!");
});

拦截器也应该可以像这样做,所以拦截器的代码应该可以像这样:

public class ConsoleInterceptor 
{
    public async Task InvokeAsync(Context context, Delegate next)
    {
        Console.WriteLine("Hello, World!");
        await next(context);
    }
}

sync 和 async 方法如何拆分?又如何能合并在一起呢?用户有怎么自己选择实现sync 还是 async 或者两个都都实现呢?


public delegate Task AsyncAspectDelegate(AspectContext context);

public delegate void AspectDelegate(AspectContext context);

// 拆分: 
// 由AspectDelegate 和 AsyncAspectDelegate 建立两套完全区分 sync 和 async 的Middleware调用链,具体使用哪个由具体被拦截的方法本身决定

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();
    }

// 合并:
// 默认实现转换方法内容,这样各种拦截器都可以混在一个Middleware调用链中

    public abstract Task InvokeAsync(AspectContext context, AsyncAspectDelegate next);

// 用户自主性选择:
// 同时提供sync 和 async 拦截器方法可以重载,用户就可以自己选择了
// 所以用户在 async 中可以调用专门的未异步优化代码了,也不用说在 sync 中必须 awit 会影响性能了,
// 你认为影响性能,你在乎就自己都重载,不在乎那就自己选
}

没有内置DI,如何兼容其他DI框架呢?

DI框架都有注册类型,我们可以通过 emit 生成代理类,替换原本的注册,就可以做到兼容。

当然每种DI框架都需要定制化的实现一些代码才能支持(唉,又是工作量呀)

AddTransient<IMTest>(x => new NMTest()), 类似这样的实例化方法怎么支持呢?

由于这种DI框架的用法,无法通过Func函数拿到实际会使用的类型,只能根据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 生成泛型类型一点问题都没有,唯一的难点是不好生成 Get<T>() 这样的方法调用, 因为IL需要反射找到的具体方法,比如Get<int>() Get<bool>() 等等,不能是不明确的 Get<T>()

要解决这个问题就只能将实际的调用延迟到运行时调用再生成具体的调用,伪码大致如下:


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);
}


浏览 20
点赞
评论
收藏
分享

手机扫一扫分享

编辑 分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

编辑 分享
举报