找回密码
 立即注册
首页 业界区 业界 Util应用框架基础(三) - 面向切面编程(AspectCore AOP) ...

Util应用框架基础(三) - 面向切面编程(AspectCore AOP)

战匈琼 昨天 08:23
本节介绍Util应用框架对AspectCore AOP的使用.
概述

有些问题需要在系统中全局处理,比如记录异常错误日志.
如果在每个出现问题的地方进行处理,不仅费力,还可能产生大量冗余代码,并打断业务逻辑的编写.
这类跨多个业务模块的非功能需求,被称为横切关注点.
我们需要把横切关注点集中管理起来.
Asp.Net Core 提供的过滤器可以处理这类需求.
过滤器有异常过滤器和操作过滤器等类型.
异常过滤器可以全局处理异常.
操作过滤器可以拦截控制器操作,在操作前和操作后执行特定代码.
过滤器很易用,但它必须配合控制器使用,所以只能解决部分问题.
你不能将过滤器特性打在应用服务的方法上,那不会产生作用.
我们需要引入一种类似 Asp.Net Core 过滤器的机制,在控制器范围外处理横切关注点.
AOP框架

AOP 是 Aspect Oriented Programming 的缩写,即面向切面编程.
AOP 框架提供了类似 Asp.Net Core 过滤器的功能,能够拦截方法,在方法执行前后插入自定义代码.
.Net AOP框架有动态代理静态织入两种实现方式.
动态代理 AOP 框架

动态代理 AOP 框架在运行时动态创建代理类,从而为方法提供自定义代码插入点.
动态代理 AOP 框架有一些限制.

  • 要拦截的方法必须在接口中定义,或是虚方法.
  • 代理类过多,特别是启用了参数拦截,会导致启动性能下降.
.Net 动态代理 AOP 框架有Castle 和 AspectCore 等.
Util应用框架使用 AspectCore ,选择 AspectCore 是因为它更加易用.
Util 对 AspectCore 仅简单包装.
静态织入 AOP 框架

静态织入 AOP 框架在编译时修改.Net IL中间代码.
与动态代理AOP相比,静态织入AOP框架有一些优势.

  • 不必是虚方法.
  • 支持静态方法.
  • 更高的启动性能.
但是成熟的 .Net 静态织入 AOP 框架大多是收费的.
Rougamo.Fody 是一个免费的静态织入 AOP 框架,可以关注.
基础用法

引用Nuget包

Nuget包名: Util.Aop.AspectCore
启用Aop

需要明确调用 AddAop 扩展方法启用 AOP 服务.
  1. var builder = WebApplication.CreateBuilder( args );
  2. builder.AsBuild().AddAop();
复制代码
使用要点


  • 定义服务接口
    如果使用抽象基类,应将需要拦截的方法设置为虚方法.
  • 配置服务接口的依赖注入关系
    AspectCore AOP依赖Ioc对象容器,只有在对象容器中注册的服务接口才能创建服务代理.
  • 将方法拦截器放在接口方法上.
    AspectCore AOP拦截器是一种.Net特性 Attribute,遵循 Attribute 使用约定.
    下面的例子将 CacheAttribute 方法拦截器添加到 ITestService 接口的 Test 方法上.
    注意: 应将拦截器放在接口方法上,而不是实现类上.
    按照约定, CacheAttribute 需要去掉 Attribute 后缀,并放到 [] 中.
    1. public interface ITestService : ISingletonDependency {        
    2.     [Cache]
    3.     List<string> Test( string value );
    4. }
    复制代码
  • 将参数拦截器放在接口方法参数上.
    AspectCore AOP 支持拦截特定参数.
    下面的例子在参数 value 上施加了 NotNullAttribute 参数拦截器.
    1.   public interface ITestService : ISingletonDependency {
    2.       void Test( [NotNull] string value );
    3.   }
    复制代码
Util内置拦截器

Util应用框架使用 Asp.Net Core 过滤器处理全局异常,全局错误日志,授权等需求,仅定义少量 AOP 拦截器.
Util应用框架定义了几个参数拦截器,用于验证.

  • NotNullAttribute

    • 验证是否为 null,如果为 null 抛出 ArgumentNullException 异常.
    • 使用范例:
    1.   public interface ITestService : ISingletonDependency {
    2.       void Test( [NotNull] string value );
    3.   }
    复制代码
  • NotEmptyAttribute

    • 使用 string.IsNullOrWhiteSpace 验证是否为空字符串,如果为空则抛出 ArgumentNullException 异常.
    • 使用范例:
    1.   public interface ITestService : ISingletonDependency {
    2.       void Test( [NotEmpty] string value );
    3.   }
    复制代码
  • ValidAttribute

    • 如果对象实现了 IValidation 验证接口,则自动调用对象的 Validate 方法进行验证.
      Util应用框架实体,值对象,DTO等基础对象均已实现 IValidation 接口.
    • 使用范例:
      验证单个对象.
    1.   public interface ITestService : ISingletonDependency {
    2.       void Test( [Valid] CustomerDto dto );
    3.   }
    复制代码
    验证对象集合.
    1.   public interface ITestService : ISingletonDependency {
    2.       void Test( [Valid] List<CustomerDto> dto );
    3.   }
    复制代码
Util应用框架为缓存定义了方法拦截器.

  • CacheAttribute

    • 使用范例:
    1.   public interface ITestService : ISingletonDependency {
    2.       [Cache]
    3.       List<string> Test( string value );
    4.   }
    复制代码
禁止创建服务代理

有些时候,你不希望为某些接口创建代理类.
使用 Util.Aop.IgnoreAttribute 特性标记接口即可.
下面演示了从 AspectCore AOP 排除工作单元接口.
  1. [Util.Aop.Ignore]
  2. public interface IUnitOfWork {
  3.     Task<int> CommitAsync();
  4. }
复制代码
创建自定义拦截器

除了内置的拦截器外,你可以根据需要创建自定义拦截器.
创建方法拦截器

继承 Util.Aop.InterceptorBase 基类,重写 Invoke 方法.
下面以缓存拦截器为例讲解创建方法拦截器的要点.

  • 缓存拦截器获取 ICache 依赖服务并创建缓存键.
  • 通过缓存键和返回类型查找缓存是否存在.
  • 如果缓存已经存在,则设置返回值,不需要执行拦截的方法.
  • 如果缓存不存在,执行方法获取返回值并设置缓存.
Invoke 方法有两个参数 AspectContextAspectDelegate.

  • AspectContext上下文提供了方法元数据信息和服务提供程序.

    • 使用 AspectContext 上下文获取方法元数据.
      AspectContext 上下文提供了拦截方法相关的大量元数据信息.
      本例使用 context.ServiceMethod.ReturnType 获取返回类型.
    • 使用 AspectContext 上下文获取依赖的服务.
      AspectContext上下文提供了 ServiceProvider 服务提供器,可以使用它获取依赖服务.
      本例需要获取缓存操作接口 ICache ,使用 context.ServiceProvider.GetService() 获取依赖.

  • AspectDelegate表示拦截的方法.
    await next( context ); 执行拦截方法.
    如果需要在方法执行前插入自定义代码,只需将代码放在 await next( context ); 之前即可.
  1. /// <summary>
  2. /// 缓存拦截器
  3. /// </summary>
  4. public class CacheAttribute : InterceptorBase {
  5.     /// <summary>
  6.     /// 缓存键前缀
  7.     /// </summary>
  8.     public string CacheKeyPrefix { get; set; }
  9.     /// <summary>
  10.     /// 缓存过期间隔,单位:秒,默认值:36000
  11.     /// </summary>
  12.     public int Expiration { get; set; } = 36000;
  13.     /// <summary>
  14.     /// 执行
  15.     /// </summary>
  16.     public override async Task Invoke( AspectContext context, AspectDelegate next ) {
  17.         var cache = GetCache( context );
  18.         var returnType = GetReturnType( context );
  19.         var key = CreateCacheKey( context );
  20.         var value = await GetCacheValue( cache, returnType, key );
  21.         if ( value != null ) {
  22.             SetReturnValue( context, returnType, value );
  23.             return;
  24.         }
  25.         await next( context );
  26.         await SetCache( context, cache, key );
  27.     }
  28.     /// <summary>
  29.     /// 获取缓存服务
  30.     /// </summary>
  31.     protected virtual ICache GetCache( AspectContext context ) {
  32.         return context.ServiceProvider.GetService<ICache>();
  33.     }
  34.     /// <summary>
  35.     /// 获取返回类型
  36.     /// </summary>
  37.     private Type GetReturnType( AspectContext context ) {
  38.         return context.IsAsync() ? context.ServiceMethod.ReturnType.GetGenericArguments().First() : context.ServiceMethod.ReturnType;
  39.     }
  40.     /// <summary>
  41.     /// 创建缓存键
  42.     /// </summary>
  43.     private string CreateCacheKey( AspectContext context ) {
  44.         var keyGenerator = context.ServiceProvider.GetService<ICacheKeyGenerator>();
  45.         return keyGenerator.CreateCacheKey( context.ServiceMethod, context.Parameters, CacheKeyPrefix );
  46.     }
  47.     /// <summary>
  48.     /// 获取缓存值
  49.     /// </summary>
  50.     private async Task<object> GetCacheValue( ICache cache, Type returnType, string key ) {
  51.         return await cache.GetAsync( key, returnType );
  52.     }
  53.     /// <summary>
  54.     /// 设置返回值
  55.     /// </summary>
  56.     private void SetReturnValue( AspectContext context, Type returnType, object value ) {
  57.         if ( context.IsAsync() ) {
  58.             context.ReturnValue = typeof( Task ).GetMethods()
  59.                 .First( p => p.Name == "FromResult" && p.ContainsGenericParameters )
  60.                 .MakeGenericMethod( returnType ).Invoke( null, new[] { value } );
  61.             return;
  62.         }
  63.         context.ReturnValue = value;
  64.     }
  65.     /// <summary>
  66.     /// 设置缓存
  67.     /// </summary>
  68.     private async Task SetCache( AspectContext context, ICache cache, string key ) {
  69.         var options = new CacheOptions { Expiration = TimeSpan.FromSeconds( Expiration ) };
  70.         var returnValue = context.IsAsync() ? await context.UnwrapAsyncReturnValue() : context.ReturnValue;
  71.         await cache.SetAsync( key, returnValue, options );
  72.     }
  73. }
复制代码
创建参数拦截器

继承 Util.Aop.ParameterInterceptorBase 基类,重写 Invoke 方法.
与方法拦截器类似, Invoke 也提供了两个参数 ParameterAspectContext 和 ParameterAspectDelegate.
ParameterAspectContext 上下文提供方法元数据.
ParameterAspectDelegate 表示拦截的方法.
下面演示了 [NotNull] 参数拦截器.
在方法执行前判断参数是否为 null,如果为 null 抛出异常,不会执行拦截方法.
  1. /// <summary>
  2. /// 验证参数不能为null
  3. /// </summary>
  4. public class NotNullAttribute : ParameterInterceptorBase {
  5.     /// <summary>
  6.     /// 执行
  7.     /// </summary>
  8.     public override Task Invoke( ParameterAspectContext context, ParameterAspectDelegate next ) {
  9.         if( context.Parameter.Value == null )
  10.             throw new ArgumentNullException( context.Parameter.Name );
  11.         return next( context );
  12.     }
  13. }
复制代码
性能优化

AddAop 配置方法默认不带参数,所有添加到 Ioc 容器的服务都会创建代理类,并启用参数拦截器.
AspectCore AOP 参数拦截器对启动性能有很大的影响.
默认配置适合规模较小的项目.
当你在Ioc容器注册了上千个甚至更多的服务时,启动时间将显著增长,因为启动时需要创建大量的代理类.
有几个方法可以优化 AspectCore AOP 启动性能.

  • 拆分项目
    对于微服务架构,单个项目包含的接口应该不会特别多.
    如果发现由于创建代理类导致启动时间过长,可以拆分项目.
    但对于单体架构,不能通过拆分项目的方式解决.
  • 减少创建的代理类.
    Util定义了一个AOP标记接口 IAopProxy ,只有继承了 IAopProxy 的接口才会创建代理类.
    要启用 IAopProxy 标记接口,只需向 AddAop 传递 true .
    1.   var builder = WebApplication.CreateBuilder( args );
    2.   builder.AsBuild().AddAop( true );
    复制代码
    现在只有明确继承自 IAopProxy 的接口才会创建代理类,代理类的数量将大幅减少.
    应用服务和领域服务接口默认继承了 IAopProxy.
    如果你在其它构造块使用了拦截器,比如仓储,需要让你的仓储接口继承 IAopProxy.
  • 禁用参数拦截器.
    如果启用了 IAopProxy 标记接口,启动性能依然未达到你的要求,可以禁用参数拦截器.
    AddAop 扩展方法支持传入 Action 参数,可以覆盖默认设置.
    下面的例子禁用了参数拦截器,并为所有继承了 IAopProxy 的接口创建代理.
    1.   var builder = WebApplication.CreateBuilder( args );
    2.   builder.AsBuild().AddAop( options => options.NonAspectPredicates.Add( t => !IsProxy( t.DeclaringType ) ) );
    3.   /// <summary>
    4.   /// 是否创建代理
    5.   /// </summary>
    6.   private static bool IsProxy( Type type ) {
    7.       if ( type == null )
    8.           return false;
    9.       var interfaces = type.GetInterfaces();
    10.       if ( interfaces == null || interfaces.Length == 0 )
    11.           return false;
    12.       foreach ( var item in interfaces ) {
    13.           if ( item == typeof( IAopProxy ) )
    14.               return true;
    15.       }
    16.       return false;
    17.   }
    复制代码
源码解析

AppBuilderExtensions

扩展了 AddAop 配置方法.
isEnableIAopProxy 参数用于启用 IAopProxy 标记接口.
Action 参数用于覆盖默认配置.
  1. /// <summary>
  2. /// Aop配置扩展
  3. /// </summary>
  4. public static class AppBuilderExtensions {
  5.     /// <summary>
  6.     /// 启用AspectCore拦截器
  7.     /// </summary>
  8.     /// <param name="builder">应用生成器</param>
  9.     public static IAppBuilder AddAop( this IAppBuilder builder ) {
  10.         return builder.AddAop( false );
  11.     }
  12.     /// <summary>
  13.     /// 启用AspectCore拦截器
  14.     /// </summary>
  15.     /// <param name="builder">应用生成器</param>
  16.     /// <param name="isEnableIAopProxy">是否启用IAopProxy接口标记</param>
  17.     public static IAppBuilder AddAop( this IAppBuilder builder,bool isEnableIAopProxy ) {
  18.         return builder.AddAop( null, isEnableIAopProxy );
  19.     }
  20.     /// <summary>
  21.     /// 启用AspectCore拦截器
  22.     /// </summary>
  23.     /// <param name="builder">应用生成器</param>
  24.     /// <param name="setupAction">AspectCore拦截器配置操作</param>
  25.     public static IAppBuilder AddAop( this IAppBuilder builder, Action<IAspectConfiguration> setupAction ) {
  26.         return builder.AddAop( setupAction, false );
  27.     }
  28.     /// <summary>
  29.     /// 启用AspectCore拦截器
  30.     /// </summary>
  31.     /// <param name="builder">应用生成器</param>
  32.     /// <param name="setupAction">AspectCore拦截器配置操作</param>
  33.     /// <param name="isEnableIAopProxy">是否启用IAopProxy接口标记</param>
  34.     private static IAppBuilder AddAop( this IAppBuilder builder, Action<IAspectConfiguration> setupAction, bool isEnableIAopProxy ) {
  35.         builder.CheckNull( nameof( builder ) );
  36.         builder.Host.UseServiceProviderFactory( new DynamicProxyServiceProviderFactory() );
  37.         builder.Host.ConfigureServices( ( context, services ) => {
  38.             ConfigureDynamicProxy( services, setupAction, isEnableIAopProxy );
  39.             RegisterAspectScoped( services );
  40.         } );
  41.         return builder;
  42.     }
  43.     /// <summary>
  44.     /// 配置拦截器
  45.     /// </summary>
  46.     private static void ConfigureDynamicProxy( IServiceCollection services, Action<IAspectConfiguration> setupAction, bool isEnableIAopProxy ) {
  47.         services.ConfigureDynamicProxy( config => {
  48.             if ( setupAction == null ) {
  49.                 config.NonAspectPredicates.Add( t => !IsProxy( t.DeclaringType, isEnableIAopProxy ) );
  50.                 config.EnableParameterAspect();
  51.                 return;
  52.             }
  53.             setupAction.Invoke( config );
  54.         } );
  55.     }
  56.     /// <summary>
  57.     /// 是否创建代理
  58.     /// </summary>
  59.     private static bool IsProxy( Type type, bool isEnableIAopProxy ) {
  60.         if ( type == null )
  61.             return false;
  62.         if ( isEnableIAopProxy == false ) {
  63.             if ( type.SafeString().Contains( "Xunit.DependencyInjection.ITestOutputHelperAccessor" ) )
  64.                 return false;
  65.             return true;
  66.         }
  67.         var interfaces = type.GetInterfaces();
  68.         if ( interfaces == null || interfaces.Length == 0 )
  69.             return false;
  70.         foreach ( var item in interfaces ) {
  71.             if ( item == typeof( IAopProxy ) )
  72.                 return true;
  73.         }
  74.         return false;
  75.     }
  76.     /// <summary>
  77.     /// 注册拦截器服务
  78.     /// </summary>
  79.     private static void RegisterAspectScoped( IServiceCollection services ) {
  80.         services.AddScoped<IAspectScheduler, ScopeAspectScheduler>();
  81.         services.AddScoped<IAspectBuilderFactory, ScopeAspectBuilderFactory>();
  82.         services.AddScoped<IAspectContextFactory, ScopeAspectContextFactory>();
  83.     }
  84. }
复制代码
Util.Aop.IAopProxy

IAopProxy 是一个标记接口,继承了它的接口才会创建代理类.
  1. /// <summary>
  2. /// Aop代理标记
  3. /// </summary>
  4. public interface IAopProxy {
  5. }
复制代码
Util.Aop.InterceptorBase

InterceptorBase 是方法拦截器基类.
它是一个简单抽象层, 未来可能提供一些共享方法.
  1. /// <summary>
  2. /// 拦截器基类
  3. /// </summary>
  4. public abstract class InterceptorBase : AbstractInterceptorAttribute {
  5. }
复制代码
Util.Aop.ParameterInterceptorBase

ParameterInterceptorBase 是参数拦截器基类.
  1. /// <summary>
  2. /// 参数拦截器基类
  3. /// </summary>
  4. public abstract class ParameterInterceptorBase : ParameterInterceptorAttribute {
  5. }
复制代码
Util.Aop.IgnoreAttribute

[Util.Aop.Ignore] 用于禁止创建代理类.
  1. /// <summary>
  2. /// 忽略拦截
  3. /// </summary>
  4. public class IgnoreAttribute : NonAspectAttribute {
  5. }
复制代码
欢迎转载何镇汐的技术博客微信扫描二维码支持Util
1.jpeg

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
您需要登录后才可以回帖 登录 | 立即注册