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


    本节介绍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 框架有CastleAspectCore 等.

    Util应用框架使用 AspectCore ,选择 AspectCore 是因为它更加易用.

    Util 对 AspectCore 仅简单包装.

    静态织入 AOP 框架

    静态织入 AOP 框架在编译时修改.Net IL中间代码.

    与动态代理AOP相比,静态织入AOP框架有一些优势.

    • 不必是虚方法.

    • 支持静态方法.

    • 更高的启动性能.

    但是成熟的 .Net 静态织入 AOP 框架大多是收费的.

    Rougamo.Fody 是一个免费的静态织入 AOP 框架,可以关注.

    基础用法

    引用Nuget包

    Nuget包名: Util.Aop.AspectCore

    启用Aop

    需要明确调用 AddAop 扩展方法启用 AOP 服务.

    var builder = WebApplication.CreateBuilder( args );
    builder.AsBuild().AddAop();
    

    使用要点

    • 定义服务接口

      如果使用抽象基类,应将需要拦截的方法设置为虚方法.

    • 配置服务接口的依赖注入关系

      AspectCore AOP依赖Ioc对象容器,只有在对象容器中注册的服务接口才能创建服务代理.

    • 将方法拦截器放在接口方法上.

      AspectCore AOP拦截器是一种.Net特性 Attribute,遵循 Attribute 使用约定.

      下面的例子将 CacheAttribute 方法拦截器添加到 ITestService 接口的 Test 方法上.

      注意: 应将拦截器放在接口方法上,而不是实现类上.

      按照约定, CacheAttribute 需要去掉 Attribute 后缀,并放到 [] 中.

      public interface ITestService : ISingletonDependency {        
          [Cache]
          List<string> Test( string value );
      }
      
    • 将参数拦截器放在接口方法参数上.

      AspectCore AOP 支持拦截特定参数.

      下面的例子在参数 value 上施加了 NotNullAttribute 参数拦截器.

        public interface ITestService : ISingletonDependency {
            void Test( [NotNull] string value );
        }
      

    Util内置拦截器

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

    Util应用框架定义了几个参数拦截器,用于验证.

    • NotNullAttribute

      • 验证是否为 null,如果为 null 抛出 ArgumentNullException 异常.

      • 使用范例:

        public interface ITestService : ISingletonDependency {
            void Test( [NotNull] string value );
        }
      
    • NotEmptyAttribute

      • 使用 string.IsNullOrWhiteSpace 验证是否为空字符串,如果为空则抛出 ArgumentNullException 异常.

      • 使用范例:

        public interface ITestService : ISingletonDependency {
            void Test( [NotEmpty] string value );
        }
      
    • ValidAttribute

      • 如果对象实现了 IValidation 验证接口,则自动调用对象的 Validate 方法进行验证.

        Util应用框架实体,值对象,DTO等基础对象均已实现 IValidation 接口.

      • 使用范例:

        验证单个对象.

        public interface ITestService : ISingletonDependency {
            void Test( [Valid] CustomerDto dto );
        }
      

      验证对象集合.

        public interface ITestService : ISingletonDependency {
            void Test( [Valid] List dto );
        }
      

    Util应用框架为缓存定义了方法拦截器.

    • CacheAttribute

      • 使用范例:
        public interface ITestService : ISingletonDependency {
            [Cache]
            List<string> Test( string value );
        }
      

    禁止创建服务代理

    有些时候,你不希望为某些接口创建代理类.

    使用 Util.Aop.IgnoreAttribute 特性标记接口即可.

    下面演示了从 AspectCore AOP 排除工作单元接口.

    [Util.Aop.Ignore]
    public interface IUnitOfWork {
        Task<int> CommitAsync();
    }
    

    创建自定义拦截器

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

    创建方法拦截器

    继承 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 ); 之前即可.

    /// 
    /// 缓存拦截器
    /// 
    public class CacheAttribute : InterceptorBase {
        /// 
        /// 缓存键前缀
        /// 
        public string CacheKeyPrefix { get; set; }
        /// 
        /// 缓存过期间隔,单位:秒,默认值:36000
        /// 
        public int Expiration { get; set; } = 36000;
    
        /// 
        /// 执行
        /// 
        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 );
        }
    
        /// 
        /// 获取缓存服务
        /// 
        protected virtual ICache GetCache( AspectContext context ) {
            return context.ServiceProvider.GetService();
        }
    
        /// 
        /// 获取返回类型
        /// 
        private Type GetReturnType( AspectContext context ) {
            return context.IsAsync() ? context.ServiceMethod.ReturnType.GetGenericArguments().First() : context.ServiceMethod.ReturnType;
        }
    
        /// 
        /// 创建缓存键
        /// 
        private string CreateCacheKey( AspectContext context ) {
            var keyGenerator = context.ServiceProvider.GetService();
            return keyGenerator.CreateCacheKey( context.ServiceMethod, context.Parameters, CacheKeyPrefix );
        }
    
        /// 
        /// 获取缓存值
        /// 
        private async Task GetCacheValue( ICache cache, Type returnType, string key ) {
            return await cache.GetAsync( key, returnType );
        }
    
        /// 
        /// 设置返回值
        /// 
        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;
        }
    
        /// 
        /// 设置缓存
        /// 
        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 );
        }
    }
    
    

    创建参数拦截器

    继承 Util.Aop.ParameterInterceptorBase 基类,重写 Invoke 方法.

    与方法拦截器类似, Invoke 也提供了两个参数 ParameterAspectContext 和 ParameterAspectDelegate.

    ParameterAspectContext 上下文提供方法元数据.

    ParameterAspectDelegate 表示拦截的方法.

    下面演示了 [NotNull] 参数拦截器.

    在方法执行前判断参数是否为 null,如果为 null 抛出异常,不会执行拦截方法.

    /// 
    /// 验证参数不能为null
    /// 
    public class NotNullAttribute : ParameterInterceptorBase {
        /// 
        /// 执行
        /// 
        public override Task Invoke( ParameterAspectContext context, ParameterAspectDelegate next ) {
            if( context.Parameter.Value == null )
                throw new ArgumentNullException( context.Parameter.Name );
            return next( context );
        }
    }
    

    性能优化

    AddAop 配置方法默认不带参数,所有添加到 Ioc 容器的服务都会创建代理类,并启用参数拦截器.

    AspectCore AOP 参数拦截器对启动性能有很大的影响.

    默认配置适合规模较小的项目.

    当你在Ioc容器注册了上千个甚至更多的服务时,启动时间将显著增长,因为启动时需要创建大量的代理类.

    有几个方法可以优化 AspectCore AOP 启动性能.

    • 拆分项目

      对于微服务架构,单个项目包含的接口应该不会特别多.

      如果发现由于创建代理类导致启动时间过长,可以拆分项目.

      但对于单体架构,不能通过拆分项目的方式解决.

    • 减少创建的代理类.

      Util定义了一个AOP标记接口 IAopProxy ,只有继承了 IAopProxy 的接口才会创建代理类.

      要启用 IAopProxy 标记接口,只需向 AddAop 传递 true .

        var builder = WebApplication.CreateBuilder( args );
        builder.AsBuild().AddAop( true );
      

      现在只有明确继承自 IAopProxy 的接口才会创建代理类,代理类的数量将大幅减少.

      应用服务和领域服务接口默认继承了 IAopProxy.

      如果你在其它构造块使用了拦截器,比如仓储,需要让你的仓储接口继承 IAopProxy.

    • 禁用参数拦截器.

      如果启用了 IAopProxy 标记接口,启动性能依然未达到你的要求,可以禁用参数拦截器.

      AddAop 扩展方法支持传入 Action 参数,可以覆盖默认设置.

      下面的例子禁用了参数拦截器,并为所有继承了 IAopProxy 的接口创建代理.

        var builder = WebApplication.CreateBuilder( args );
        builder.AsBuild().AddAop( options => options.NonAspectPredicates.Add( t => !IsProxy( t.DeclaringType ) ) );
      
        /// 
        /// 是否创建代理
        /// 
        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

    扩展了 AddAop 配置方法.

    isEnableIAopProxy 参数用于启用 IAopProxy 标记接口.

    Action 参数用于覆盖默认配置.

    /// 
    /// Aop配置扩展
    /// 
    public static class AppBuilderExtensions {
        /// 
        /// 启用AspectCore拦截器
        /// 
        /// 应用生成器
        public static IAppBuilder AddAop( this IAppBuilder builder ) {
            return builder.AddAop( false );
        }
    
        /// 
        /// 启用AspectCore拦截器
        /// 
        /// 应用生成器
        /// 是否启用IAopProxy接口标记
        public static IAppBuilder AddAop( this IAppBuilder builder,bool isEnableIAopProxy ) {
            return builder.AddAop( null, isEnableIAopProxy );
        }
    
        /// 
        /// 启用AspectCore拦截器
        /// 
        /// 应用生成器
        /// AspectCore拦截器配置操作
        public static IAppBuilder AddAop( this IAppBuilder builder, Action setupAction ) {
            return builder.AddAop( setupAction, false );
        }
    
        /// 
        /// 启用AspectCore拦截器
        /// 
        /// 应用生成器
        /// AspectCore拦截器配置操作
        /// 是否启用IAopProxy接口标记
        private static IAppBuilder AddAop( this IAppBuilder builder, Action 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;
        }
    
        /// 
        /// 配置拦截器
        /// 
        private static void ConfigureDynamicProxy( IServiceCollection services, Action setupAction, bool isEnableIAopProxy ) {
            services.ConfigureDynamicProxy( config => {
                if ( setupAction == null ) {
                    config.NonAspectPredicates.Add( t => !IsProxy( t.DeclaringType, isEnableIAopProxy ) );
                    config.EnableParameterAspect();
                    return;
                }
                setupAction.Invoke( config );
            } );
        }
    
        /// 
        /// 是否创建代理
        /// 
        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;
        }
    
        /// 
        /// 注册拦截器服务
        /// 
        private static void RegisterAspectScoped( IServiceCollection services ) {
            services.AddScoped();
            services.AddScoped();
            services.AddScoped();
        }
    }
    

    Util.Aop.IAopProxy

    IAopProxy 是一个标记接口,继承了它的接口才会创建代理类.

    /// 
    /// Aop代理标记
    /// 
    public interface IAopProxy {
    }
    

    Util.Aop.InterceptorBase

    InterceptorBase 是方法拦截器基类.

    它是一个简单抽象层, 未来可能提供一些共享方法.

    /// 
    /// 拦截器基类
    /// 
    public abstract class InterceptorBase : AbstractInterceptorAttribute {
    }
    

    Util.Aop.ParameterInterceptorBase

    ParameterInterceptorBase 是参数拦截器基类.

    /// 
    /// 参数拦截器基类
    /// 
    public abstract class ParameterInterceptorBase : ParameterInterceptorAttribute {
    }
    

    Util.Aop.IgnoreAttribute

    [Util.Aop.Ignore] 用于禁止创建代理类.

    /// 
    /// 忽略拦截
    /// 
    public class IgnoreAttribute : NonAspectAttribute {
    }
    
  • 相关阅读:
    UML笔记
    2023年第四届MathorCup高校数学建模挑战赛——大数据竞赛B题
    HTML静态网页作业——电影介绍-你的名字 5页 无js 带音乐
    alist网盘自动同步
    Leetcode(524)——通过删除字母匹配到字典里最长单词
    关于智慧城市项目的小总结:
    坤坤音效键盘(Python实现)
    RK3399平台开发系列讲解(USB篇)URB通信过程详解
    NFT 交易市场的后起之秀要如何超越 OpenSea?
    抽象语法树AST(Abstract Syntax Tree)
  • 原文地址:https://www.cnblogs.com/xiadao521/p/17810769.html