Norns.Urd 正在参加 2020 年度 OSC 中国开源项目评选,请投票支持!
Norns.Urd 在 2020 年度 OSC 中国开源项目评选 中已获得 {{ projectVoteCount }} 票,请投票支持!
投票让它出道
已投票
Norns.Urd 获得 2020 年度 OSC 中国开源项目评选「最佳人气项目」 !
Norns.Urd 获得 2020 年度 OSC 中国开源项目评选「最佳人气项目」「最积极运营项目」 !
Norns.Urd 获得 2020 年度 OSC 中国开源项目评选「最积极运营项目」 !

软件简介

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


展开阅读全文

代码

的 Gitee 指数为
超过 的项目

评论 (0)

加载中
更多评论
暂无内容
发表了博客
2018/11/08 18:41

手把手教你写DI_3_小白徒手支持 `Singleton` 和 `Scoped` 生命周期

手把手教你写DI_3_小白徒手支持 Singleton 和 Scoped 生命周期 在上一节:手把手教你写DI_2_小白徒手撸构造函数注入 浑身绷带的小白同学:我们继续开展我们的工作,大家都知道 Singleton是什么,就是全局只有一个呗,我们就先从它开始,这个多简单,我们找个字典放这些对象就ok啦 public class ServiceProvider : IServiceProvider { ... private readonly ConcurrentDictionary<Type, object> singletonCache = new ...

0
0
发表于软件架构专区
2014/02/21 17:06

[转载]How to set up a clean UTF-8 environment in Lin

出处: http://perlgeek.de/en/article/set-up-a-clean-utf8-environment Many people have problems with handling non-ASCII characters in their programs, or even getting their IRC client or text editor to display them correctly. To efficiently work with text data, your environment has to be set up properly - it is so much easier to debug a problem which has encoding issues if you can trust your term...

0
0
发表了博客
2019/08/11 17:26

python中ocr软件pytesseract使用

首先要看原版的参考 https://github.com/madmaze/pytesseract 直接上代码, import pytesseract from PIL import Image image = Image.open(r'D:\xingjinzi\5.jfif') result =pytesseract.image_to_string(image,config='--psm 8 -c tessedit_char_whitelist=abceefghigklmnopqrstuvwxyz') print(result)   # result =pytesseract.image_to_string (image,lang='eng') #英文 # result =pytesseract.image_to_string (ima...

0
0
2019/10/05 06:30

知识点051-判断linux系统状态命令汇总

系统状态信息查看 top (服务器进程,负载,cpu,内存情况) 参数 解析 load average: 1.15, 1.42, 1.44 1,5,15分钟负载情况,一般参数为cpu内核数的1-1.5倍以上应关注 1 running, 559 sleeping, 0 stopped, 0 zombie 正在运行、睡眠、停止、僵尸进程的数量,关注僵尸进程的数量 %sys 系统占用CPU百分比 id CPU空闲率,越接近100%越好 top -c 完整的进程命令显示 top -bn1 打印当前的top 进程 数字1 显示各个cpu的情况及个数 sh...

0
0
发表了博客
2019/05/05 10:10

计算机网络常用端口

目录 TCP和UDP 端口号的分配 广泛公认的端口列表 常用端口列表 Linux系统的端口列表信息 Linux系统nftables防火墙的端口映射 (20190525添加:Linux系统nftables防火墙的端口映射 章节) 传输层的端口 计算机之间依照互联网传输层TCP/IP协议的协议通信,不同的协议都对应不同的端口。并且,利用数据报文的UDP也不一定和TCP采用相同的端口号码。 TCP和UDP TCP(Transmission Control Protocol,传输控制协议)是面向连接的协议,...

0
0
2016/09/24 22:42

FreeSWITCH调用接口 监听 多方通话 强转 抢接等,FreeSWITCH电话会议(动态库和restful接口)

#更多参考: http://freeswitch.net.cn/86.html 中间件下载地址,含windows,linux 旧版本下载地址: [http://121.40.240.104:8090/](http://121.40.240.104:8090/) 最新版本统一存于: [https://github.com/nwaycn/FSGui](https://github.com/nwaycn/FSGui "https://github.com/nwaycn/FSGui") 新增接口: 1. 查询分机状态 curl -H "Authorization:Nway eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE0NjUxNzMyNzUsImlkIjo...

0
1
没有更多内容
加载失败,请刷新页面
点击加载更多
加载中
下一页
暂无内容
0 评论
1 收藏
分享
OSCHINA
登录后可查看更多优质内容
返回顶部
顶部