Util應用框架基礎(三) - 面向切面程式設計(AspectCore AOP)

何鎮汐發表於2023-11-05

本節介紹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<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 {
}

相關文章