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
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...