Util應用框架基礎(三) - 面向切麵編程(AspectCore AOP)

来源:https://www.cnblogs.com/xiadao521/archive/2023/11/05/17810769.html
-Advertisement-
Play Games

本節介紹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<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 {
}
Util應用框架交流群: 24791014 歡迎轉載 何鎮汐的技術博客 微信掃描二維碼支持Util
您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 一、創建數組對象的方式 var arrOb=new Array(值,........) var arrOb=Array(值,.......) var arrOb=[值,.........] var arrOb=new Array(n); arrOb[0]=值1; arrOb[1]=值2; 二、數組的 ...
  • 一、函數及函數的構造 函數是一個可重用的代碼塊,用來完成某個特定功能。每當需要反覆執行一段代碼時,可以利用函數來避免重覆書寫相同代碼。 函數包含著的代碼只能在函數被調用時才會執行,就可以避免頁面載入時執行該腳本 簡單來說就是一個封裝,封裝的是一個特定的功能,重覆使用 函數的三種定義方法: Funct ...
  • 在WPF開發中,依賴註入(Dependency Injection)和控制反轉(Inversion of Control)是程式解耦的關鍵,在當今軟體工程中占有舉足輕重的地位,兩者之間有著密不可分的聯繫。今天就以一個簡單的小例子,簡述如何在WPF中實現依賴註入和控制反轉,僅供學習分享使用,如有不足之... ...
  • 寫一個特性類,用來做標記 [AttributeUsage(AttributeTargets.Method)] //只對方法有效 public class ResourceFilterAttribute : Attribute { } 我這裡使用了MemoryCache來做緩存,也可以使用字典來做,但 ...
  • 從ASP.NET Core 3.0版本開始,SignalR的Hub已經集成到了ASP.NET Core框架中。因此,在更高版本的ASP.NET Core中,不再需要單獨引用Microsoft.AspNetCore.SignalR包來使用Hub。 在項目創建一個類繼承Hub, 首先是寫一個Create ...
  • 親測可行,Android Studio 查看源碼出現 Source for ‘Android API xxx Platform’ not found 的解決方法 如標題中的問題,產生的原因就是 SDK 源碼目錄下找不到對應版本的源碼文件。解決方案一般就是下載對應版本的源碼文件即可。 這裡主要是另一種 ...
  • 痞子衡嵌入式半月刊: 第 84 期 這裡分享嵌入式領域有用有趣的項目/工具以及一些熱點新聞,農曆年分二十四節氣,希望在每個交節之日準時發佈一期。 本期刊是開源項目(GitHub: JayHeng/pzh-mcu-bi-weekly),歡迎提交 issue,投稿或推薦你知道的嵌入式那些事兒。 上期回顧 ...
  • 這裡簡單介紹一下如何處理解決Linux平臺下Oracle 19c啟動時,告警日誌出現ORA-00800錯誤的問題,詳情介紹請見下麵內容: 環境描述: 操作系統:Red Hat Enterprise Linux release 8.8 (Ootpa) 資料庫 :19.16.0.0.0 企業版 問題描述 ...
一周排行
    -Advertisement-
    Play Games
  • 示例項目結構 在 Visual Studio 中創建一個 WinForms 應用程式後,項目結構如下所示: MyWinFormsApp/ │ ├───Properties/ │ └───Settings.settings │ ├───bin/ │ ├───Debug/ │ └───Release/ ...
  • [STAThread] 特性用於需要與 COM 組件交互的應用程式,尤其是依賴單線程模型(如 Windows Forms 應用程式)的組件。在 STA 模式下,線程擁有自己的消息迴圈,這對於處理用戶界面和某些 COM 組件是必要的。 [STAThread] static void Main(stri ...
  • 在WinForm中使用全局異常捕獲處理 在WinForm應用程式中,全局異常捕獲是確保程式穩定性的關鍵。通過在Program類的Main方法中設置全局異常處理,可以有效地捕獲並處理未預見的異常,從而避免程式崩潰。 註冊全局異常事件 [STAThread] static void Main() { / ...
  • 前言 給大家推薦一款開源的 Winform 控制項庫,可以幫助我們開發更加美觀、漂亮的 WinForm 界面。 項目介紹 SunnyUI.NET 是一個基於 .NET Framework 4.0+、.NET 6、.NET 7 和 .NET 8 的 WinForm 開源控制項庫,同時也提供了工具類庫、擴展 ...
  • 說明 該文章是屬於OverallAuth2.0系列文章,每周更新一篇該系列文章(從0到1完成系統開發)。 該系統文章,我會儘量說的非常詳細,做到不管新手、老手都能看懂。 說明:OverallAuth2.0 是一個簡單、易懂、功能強大的許可權+可視化流程管理系統。 有興趣的朋友,請關註我吧(*^▽^*) ...
  • 一、下載安裝 1.下載git 必須先下載並安裝git,再TortoiseGit下載安裝 git安裝參考教程:https://blog.csdn.net/mukes/article/details/115693833 2.TortoiseGit下載與安裝 TortoiseGit,Git客戶端,32/6 ...
  • 前言 在項目開發過程中,理解數據結構和演算法如同掌握蓋房子的秘訣。演算法不僅能幫助我們編寫高效、優質的代碼,還能解決項目中遇到的各種難題。 給大家推薦一個支持C#的開源免費、新手友好的數據結構與演算法入門教程:Hello演算法。 項目介紹 《Hello Algo》是一本開源免費、新手友好的數據結構與演算法入門 ...
  • 1.生成單個Proto.bat內容 @rem Copyright 2016, Google Inc. @rem All rights reserved. @rem @rem Redistribution and use in source and binary forms, with or with ...
  • 一:背景 1. 講故事 前段時間有位朋友找到我,說他的窗體程式在客戶這邊出現了卡死,讓我幫忙看下怎麼回事?dump也生成了,既然有dump了那就上 windbg 分析吧。 二:WinDbg 分析 1. 為什麼會卡死 窗體程式的卡死,入口門檻很低,後續往下分析就不一定了,不管怎麼說先用 !clrsta ...
  • 前言 人工智慧時代,人臉識別技術已成為安全驗證、身份識別和用戶交互的關鍵工具。 給大家推薦一款.NET 開源提供了強大的人臉識別 API,工具不僅易於集成,還具備高效處理能力。 本文將介紹一款如何利用這些API,為我們的項目添加智能識別的亮點。 項目介紹 GitHub 上擁有 1.2k 星標的 C# ...