写点什么

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

作者:何镇汐
  • 2023-11-11
    四川
  • 本文字数:7119 字

    阅读完需:约 23 分钟

面向切面编程(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 框架有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<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 {}
复制代码


用户头像

何镇汐

关注

15年以上.Net开发经验,擅长代码封装 2023-11-01 加入

15年以上.Net开发经验,擅长代码封装,主要作品为Util应用框架

评论

发布
暂无评论
Util应用框架基础(二)- 面向切面编程(AOP)_开源_何镇汐_InfoQ写作社区