百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术资源 > 正文

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

moboyou 2025-08-11 01:28 9 浏览

面向切面编程(AOP)

Util应用框架横切关注点处理

本节介绍Util应用框架对横切关注点的处理.

文章分为多个小节,如果对设计原理不感兴趣,只需阅读基础用法部分即可.

概述

有些问题需要在系统中全局处理,比如记录异常错误日志.

如果在每个出现问题的地方进行处理,不仅费力,还可能产生大量冗余代码,并打断业务逻辑的编写.

这类跨多个业务模块的非功能需求,被称为横切关注点.

我们需要把横切关注点集中管理起来.

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 服务.

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<CustomerDto> 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<ICache>() 获取依赖.

  • AspectDelegate表示拦截的方法.

await next( context ); 执行拦截方法.

如果需要在方法执行前插入自定义代码,只需将代码放在 await next( context ); 之前即可.

/// <summary>
/// 缓存拦截器
/// </summary>
public class CacheAttribute : InterceptorBase {
    /// <summary>
    /// 缓存键前缀
    /// </summary>
    public string CacheKeyPrefix { get; set; }
    /// <summary>
    /// 缓存过期间隔,单位:秒,默认值:36000
    /// </summary>
    public int Expiration { get; set; } = 36000;

    /// <summary>
    /// 执行
    /// </summary>
    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 );
    }

    /// <summary>
    /// 获取缓存服务
    /// </summary>
    protected virtual ICache GetCache( AspectContext context ) {
        return context.ServiceProvider.GetService<ICache>();
    }

    /// <summary>
    /// 获取返回类型
    /// </summary>
    private Type GetReturnType( AspectContext context ) {
        return context.IsAsync() ? context.ServiceMethod.ReturnType.GetGenericArguments().First() : context.ServiceMethod.ReturnType;
    }

    /// <summary>
    /// 创建缓存键
    /// </summary>
    private string CreateCacheKey( AspectContext context ) {
        var keyGenerator = context.ServiceProvider.GetService<ICacheKeyGenerator>();
        return keyGenerator.CreateCacheKey( context.ServiceMethod, context.Parameters, CacheKeyPrefix );
    }

    /// <summary>
    /// 获取缓存值
    /// </summary>
    private async Task<object> GetCacheValue( ICache cache, Type returnType, string key ) {
        return await cache.GetAsync( key, returnType );
    }

    /// <summary>
    /// 设置返回值
    /// </summary>
    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;
    }

    /// <summary>
    /// 设置缓存
    /// </summary>
    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 抛出异常,不会执行拦截方法.

/// <summary>
/// 验证参数不能为null
/// </summary>
public class NotNullAttribute : ParameterInterceptorBase {
    /// <summary>
    /// 执行
    /// </summary>
    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<IAspectConfiguration> 参数,可以覆盖默认设置.

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

var builder = WebApplication.CreateBuilder( args ); 
builder.AsBuild().AddAop( options => options.NonAspectPredicates.Add( t => !IsProxy( t.DeclaringType ) ) ); 
/// <summary>
/// 是否创建代理 
/// </summary>
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<IAspectConfiguration> 参数用于覆盖默认配置.

/// <summary>
/// Aop配置扩展
/// </summary>
public static class AppBuilderExtensions {
    /// <summary>
    /// 启用AspectCore拦截器
    /// </summary>
    /// <param name="builder">应用生成器</param>
    public static IAppBuilder AddAop( this IAppBuilder builder ) {
        return builder.AddAop( false );
    }

    /// <summary>
    /// 启用AspectCore拦截器
    /// </summary>
    /// <param name="builder">应用生成器</param>
    /// <param name="isEnableIAopProxy">是否启用IAopProxy接口标记</param>
    public static IAppBuilder AddAop( this IAppBuilder builder,bool isEnableIAopProxy ) {
        return builder.AddAop( null, isEnableIAopProxy );
    }

    /// <summary>
    /// 启用AspectCore拦截器
    /// </summary>
    /// <param name="builder">应用生成器</param>
    /// <param name="setupAction">AspectCore拦截器配置操作</param>
    public static IAppBuilder AddAop( this IAppBuilder builder, Action<IAspectConfiguration> setupAction ) {
        return builder.AddAop( setupAction, false );
    }

    /// <summary>
    /// 启用AspectCore拦截器
    /// </summary>
    /// <param name="builder">应用生成器</param>
    /// <param name="setupAction">AspectCore拦截器配置操作</param>
    /// <param name="isEnableIAopProxy">是否启用IAopProxy接口标记</param>
    private static IAppBuilder AddAop( this IAppBuilder builder, Action<IAspectConfiguration> 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;
    }

    /// <summary>
    /// 配置拦截器
    /// </summary>
    private static void ConfigureDynamicProxy( IServiceCollection services, Action<IAspectConfiguration> setupAction, bool isEnableIAopProxy ) {
        services.ConfigureDynamicProxy( config => {
            if ( setupAction == null ) {
                config.NonAspectPredicates.Add( t => !IsProxy( t.DeclaringType, isEnableIAopProxy ) );
                config.EnableParameterAspect();
                return;
            }
            setupAction.Invoke( config );
        } );
    }

    /// <summary>
    /// 是否创建代理
    /// </summary>
    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;
    }

    /// <summary>
    /// 注册拦截器服务
    /// </summary>
    private static void RegisterAspectScoped( IServiceCollection services ) {
        services.AddScoped<IAspectScheduler, ScopeAspectScheduler>();
        services.AddScoped<IAspectBuilderFactory, ScopeAspectBuilderFactory>();
        services.AddScoped<IAspectContextFactory, ScopeAspectContextFactory>();
    }
}

Util.Aop.IAopProxy

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

/// <summary>
/// Aop代理标记
/// </summary>
public interface IAopProxy {
}

Util.Aop.InterceptorBase

InterceptorBase 是方法拦截器基类.

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

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

Util.Aop.ParameterInterceptorBase

ParameterInterceptorBase 是参数拦截器基类.

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

Util.Aop.IgnoreAttribute

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

/// <summary>
/// 忽略拦截
/// </summary>
public class IgnoreAttribute : NonAspectAttribute {
}

相关推荐

Excel技巧:SHEETSNA函数一键提取所有工作表名称批量生产目录

首先介绍一下此函数:SHEETSNAME函数用于获取工作表的名称,有三个可选参数。语法:=SHEETSNAME([参照区域],[结果方向],[工作表范围])(参照区域,可选。给出参照,只返回参照单元格...

Excel HOUR函数:“小时”提取器_excel+hour函数提取器怎么用

一、函数概述HOUR函数是Excel中用于提取时间值小时部分的日期时间函数,返回0(12:00AM)到23(11:00PM)之间的整数。该函数在时间数据分析、考勤统计、日程安排等场景中应用广泛。语...

Filter+Search信息管理不再难|多条件|模糊查找|Excel函数应用

原创版权所有介绍一个信息管理系统,要求可以实现:多条件、模糊查找,手动输入的内容能去空格。先看效果,如下图动画演示这样的一个效果要怎样实现呢?本文所用函数有Filter和Search。先用filter...

FILTER函数介绍及经典用法12:FILTER+切片器的应用

EXCEL函数技巧:FILTER经典用法12。FILTER+切片器制作筛选按钮。FILTER的函数的经典用法12是用FILTER的函数和切片器制作一个筛选按钮。像左边的原始数据,右边想要制作一...

office办公应用网站推荐_office办公软件大全

以下是针对Office办公应用(Word/Excel/PPT等)的免费学习网站推荐,涵盖官方教程、综合平台及垂直领域资源,适合不同学习需求:一、官方权威资源1.微软Office官方培训...

WPS/Excel职场办公最常用的60个函数大全(含卡片),效率翻倍!

办公最常用的60个函数大全:从入门到精通,效率翻倍!在职场中,WPS/Excel几乎是每个人都离不开的工具,而函数则是其灵魂。掌握常用的函数,不仅能大幅提升工作效率,还能让你在数据处理、报表分析、自动...

收藏|查找神器Xlookup全集|一篇就够|Excel函数|图解教程

原创版权所有全程图解,方便阅读,内容比较多,请先收藏!Xlookup是Vlookup的升级函数,解决了Vlookup的所有缺点,可以完全取代Vlookup,学完本文后你将可以应对所有的查找难题,内容...

批量查询快递总耗时?用Excel这个公式,自动计算揽收到签收天数

批量查询快递总耗时?用Excel这个公式,自动计算揽收到签收天数在电商运营、物流对账等工作中,经常需要统计快递“揽收到签收”的耗时——比如判断某快递公司是否符合“3天内送达”的服务承...

Excel函数公式教程(490个实例详解)

Excel函数公式教程(490个实例详解)管理层的财务人员为什么那么厉害?就是因为他们精通excel技能!财务人员在日常工作中,经常会用到Excel财务函数公式,比如财务报表分析、工资核算、库存管理等...

Excel(WPS表格)Tocol函数应用技巧案例解读,建议收藏备用!

工作中,经常需要从多个单元格区域中提取唯一值,如体育赛事报名信息中提取唯一的参赛者信息等,此时如果复制粘贴然后去重,效率就会很低。如果能合理利用Tocol函数,将会极大地提高工作效率。一、功能及语法结...

Excel中的SCAN函数公式,把计算过程理清,你就会了

Excel新版本里面,除了出现非常好用的xlookup,Filter公式之外,还更新一批自定义函数,可以像写代码一样写公式其中SCAN函数公式,也非常强大,它是一个循环函数,今天来了解这个函数公式的计...

Excel(WPS表格)中多列去重就用Tocol+Unique组合函数,简单高效

在数据的分析和处理中,“去重”一直是绕不开的话题,如果单列去重,可以使用Unique函数完成,如果多列去重,如下图:从数据信息中可以看到,每位参赛者参加了多项运动,如果想知道去重后的参赛者有多少人,该...

Excel(WPS表格)函数Groupby,聚合统计,快速提高效率!

在前期的内容中,我们讲了很多的统计函数,如Sum系列、Average系列、Count系列、Rank系列等等……但如果用一个函数实现类似数据透视表的功能,就必须用Groupby函数,按指定字段进行聚合汇...

Excel新版本,IFS函数公式,太强大了!

我们举一个工作实例,现在需要计算业务员的奖励数据,右边是公司的奖励标准:在新版本的函数公式出来之前,我们需要使用IF函数公式来解决1、IF函数公式IF函数公式由三个参数组成,IF(判断条件,对的时候返...

Excel不用函数公式数据透视表,1秒完成多列项目汇总统计

如何将这里的多组数据进行汇总统计?每组数据当中一列是不同菜品,另一列就是该菜品的销售数量。如何进行汇总统计得到所有的菜品销售数量的求和、技术、平均、最大、最小值等数据?不用函数公式和数据透视表,一秒就...