從壹開始先後端分離【 .NET Core2.0/3.0 +Vue2.0 】框架之十 || AOP面向切面編程淺解析:簡單日誌記錄 + 服務切面緩存

本文3.0版本文章

 

代碼已上傳Github+Gitee,文末有地址

  上回《從壹開始先後端分離【 .NET Core2.0 Api + Vue 2.0 + AOP + 分佈式】框架之九 || 依賴注入IoC學習 + AOP界面編程初探》我們說到了依賴注入Autofac的使用,不知道你們對IoC的使用是怎樣的感受,我我的表示仍是比較可行的,至少不用本身再關心一個個複雜的實例化服務對象了,直接經過接口就知足需求,固然還有其餘的一些功能,我尚未說到,拋磚引玉嘛,你們若是有好的想法,歡迎留言,也能夠來羣裏,你們一塊兒學習討論。昨天在文末我們說到了AOP面向切面編程的定義和思想,我我的簡單使用了下,感受主要的思路仍是經過攔截器來操做,就像是一箇中間件同樣,今天呢,我給你們說兩個小栗子,固然,你也能夠合併成一個,也能夠自定義擴展,由於咱們是真個系列是基於Autofac框架,因此今天主要說的是基於Autofac的Castle動態代理的方法,靜態注入的方式之後有時間能夠再補充。
  時間真快,轉眼已經十天過去了,感謝你們的鼓勵,批評指正,但願個人文章,對您有一點點兒的幫助,哪怕是有學習新知識的動力也行,至少至少,能夠爲之後跳槽增長新的談資 [哭笑],這些天咱們從面向對象OOP的開發,後又轉向了面向接口開發,到分層解耦,如今到了面向切面編程AOP,往下走將會是,分佈式,微服務等等,技術真是永無止境啊!好啦,立刻開始動筆。

大神反饋:

一、羣裏小夥伴 大齡Giser 根據本文,成功的應用在工做中,點贊,歡迎圍觀:【ABP】面向切面編程(AOP)知識總結html

 

 零、今天完成的深紅色部分

 

 

1、AOP 之 實現日誌記錄(服務層)

首先想想,若是有一個需求(這個只是個人一個想法,真實工做中可能用不上),要記錄整個項目的接口和調用狀況,固然若是隻是控制器的話,仍是挺簡單的,直接用一個過濾器或者一箇中間件,還記得我們開發Swagger攔截權限驗證的中間件麼,那個就很方便的把用戶調用接口的名稱記錄下來,固然也能夠寫成一個切面,可是若是想看下與Service或者Repository層的調用狀況呢,好像目前我們只能在Service層或者Repository層去寫日誌記錄了,那樣的話,不只工程大(固然你能夠用工廠模式),並且耦合性瞬間就高了呀,想象一下,若是日誌要去掉,關閉,修改,須要改多少地方!您說是否是,好不容易前邊的工做把層級的耦合性下降了。別慌,這個時候就用到了AOP和Autofac的Castle結合的完美解決方案了。
  通過這麼多天的開發,幾乎天天都須要引入Nuget包哈,我我的表示也不想再添加了,如今都已經挺大的了(47M固然包括所有dll文件),今天不會辣!其實都是基於昨天的兩個Nuget包中已經自動生成的Castle組件。請看如下步驟:

一、定義服務接口與實現類

首先這裏使用到了 BlogArticle 的實體類(這裏我保留了sqlsugar的特性,沒須要的能夠手動刪除):git

    public class BlogArticle
    {
        /// <summary>
        /// 主鍵
        /// </summary>
        /// 這裏之因此沒用RootEntity,是想保持和以前的數據庫一致,主鍵是bID,不是Id
        [SugarColumn(IsNullable = false, IsPrimaryKey = true, IsIdentity = true)]
        public int bID { get; set; }
        /// <summary>
        /// 建立人
        /// </summary>
        [SugarColumn(Length = 60, IsNullable = true)]
        public string bsubmitter { get; set; }

        /// <summary>
        /// 標題blog
        /// </summary>
        [SugarColumn(Length = 256, IsNullable = true)]
        public string btitle { get; set; }

        /// <summary>
        /// 類別
        /// </summary>
        [SugarColumn(Length = int.MaxValue, IsNullable = true)]
        public string bcategory { get; set; }

        /// <summary>
        /// 內容
        /// </summary>
        [SugarColumn(IsNullable = true, ColumnDataType = "text")]
        public string bcontent { get; set; }

        /// <summary>
        /// 訪問量
        /// </summary>
        public int btraffic { get; set; }

        /// <summary>
        /// 評論數量
        /// </summary>
        public int bcommentNum { get; set; }

        /// <summary> 
        /// 修改時間
        /// </summary>
        public DateTime bUpdateTime { get; set; }

        /// <summary>
        /// 建立時間
        /// </summary>
        public System.DateTime bCreateTime { get; set; }
        /// <summary>
        /// 備註
        /// </summary>
        [SugarColumn(Length = int.MaxValue, IsNullable = true)]
        public string bRemark { get; set; }

        /// <summary>
        /// 邏輯刪除
        /// </summary>
        [SugarColumn(IsNullable = true)]
        public bool? IsDeleted { get; set; }

    }

 

 

在IBlogArticleServices.cs定義一個獲取博客列表接口 ,並在BlogArticleServices實現該接口
   public interface IBlogArticleServices :IBaseServices<BlogArticle>
    {
        Task<List<BlogArticle>> getBlogs();
    }

   public class BlogArticleServices : BaseServices<BlogArticle>, IBlogArticleServices
    {
        IBlogArticleRepository dal;
        public BlogArticleServices(IBlogArticleRepository dal)
        {
            this.dal = dal;
            base.baseDal = dal;
        }
        /// <summary>
        /// 獲取博客列表
        /// </summary>
        /// <param name="id"></param>
        /// <returns></returns>
        public async Task<List<BlogArticle>> getBlogs()
        {
            var bloglist = await dal.Query(a => a.bID > 0, a => a.bID);

            return bloglist;

        }
    }

 

二、在API層中添加對該接口引用

(注意RESTful接口路徑命名規範,我這麼寫只是爲了測試)github

      /// <summary>
        /// 獲取博客列表
        /// </summary>
        /// <returns></returns>
        [HttpGet]
        [Route("GetBlogs")]
        public async Task<List<BlogArticle>> GetBlogs()
        {

            return await blogArticleServices.getBlogs();
        }

 

三、添加AOP攔截器

在Blog.Core新建文件夾AOP,並添加攔截器BlogLogAOP,並設計其中用到的日誌記錄Logger方法或者類sql

 

 

關鍵的一些知識點,註釋中已經說明了,主要是有如下:
一、繼承接口IInterceptor
二、實例化接口IINterceptor的惟一方法Intercept
三、void Proceed();表示執行當前的方法和object ReturnValue { get; set; }執行後調用,object[] Arguments參數對象
四、中間的代碼是新建一個類,仍是單寫,就很隨意了。數據庫

  /// <summary>
    /// 攔截器BlogLogAOP 繼承IInterceptor接口
    /// </summary>
    public class BlogLogAOP : IInterceptor
    {

        /// <summary>
        /// 實例化IInterceptor惟一方法 
        /// </summary>
        /// <param name="invocation">包含被攔截方法的信息</param>
        public void Intercept(IInvocation invocation)
        {
            //記錄被攔截方法信息的日誌信息
            var dataIntercept = $"{DateTime.Now.ToString("yyyyMMddHHmmss")} " +
                $"當前執行方法:{ invocation.Method.Name} " +
                $"參數是: {string.Join(", ", invocation.Arguments.Select(a => (a ?? "").ToString()).ToArray())} \r\n";

            //在被攔截的方法執行完畢後 繼續執行當前方法
            invocation.Proceed();

            dataIntercept += ($"被攔截方法執行完畢,返回結果:{invocation.ReturnValue}");

            #region 輸出到當前項目日誌
            var path = Directory.GetCurrentDirectory() + @"\Log";
            if (!Directory.Exists(path))
            {
                Directory.CreateDirectory(path);
            }

            string fileName = path + $@"\InterceptLog-{DateTime.Now.ToString("yyyyMMddHHmmss")}.log";

            StreamWriter sw = File.AppendText(fileName);
            sw.WriteLine(dataIntercept);
            sw.Close(); 
            #endregion

        }
    }
 

提示:這裏展現瞭如何在項目中使用AOP實現對 service 層進行日誌記錄,若是你想實現異常信息記錄的話,很簡單,編程

注意,這個方法僅僅是針對同步的策略,若是你的service是異步的,這裏獲取不到,正確的寫法,在文章底部的 GitHub 代碼裏,由於和 AOP 思想沒有直接的關係,這裏就不贅述。後端

 
下邊的是完整代碼:
/// <summary>
/// 實例化IInterceptor惟一方法 
/// </summary>
/// <param name="invocation">包含被攔截方法的信息</param>
public void Intercept(IInvocation invocation)
{
    //記錄被攔截方法信息的日誌信息
    var dataIntercept = "" +
        $"【當前執行方法】:{ invocation.Method.Name} \r\n" +
        $"【攜帶的參數有】: {string.Join(", ", invocation.Arguments.Select(a => (a ?? "").ToString()).ToArray())} \r\n";

    try
    {
        MiniProfiler.Current.Step($"執行Service方法:{invocation.Method.Name}() -> ");
        //在被攔截的方法執行完畢後 繼續執行當前方法,注意是被攔截的是異步的
        invocation.Proceed();


        // 異步獲取異常,先執行
        if (IsAsyncMethod(invocation.Method))
        {

            //Wait task execution and modify return value
            if (invocation.Method.ReturnType == typeof(Task))
            {
                invocation.ReturnValue = InternalAsyncHelper.AwaitTaskWithPostActionAndFinally(
                    (Task)invocation.ReturnValue,
                    async () => await TestActionAsync(invocation),
                    ex =>
                    {
                        LogEx(ex, ref dataIntercept);
                    });
            }
            else //Task<TResult>
            {
                invocation.ReturnValue = InternalAsyncHelper.CallAwaitTaskWithPostActionAndFinallyAndGetResult(
                 invocation.Method.ReturnType.GenericTypeArguments[0],
                 invocation.ReturnValue,
                 async () => await TestActionAsync(invocation),
                 ex =>
                 {
                     LogEx(ex, ref dataIntercept);
                 });

            }

        }
        else
        {// 同步1


        }
    }
    catch (Exception ex)// 同步2
    {
        LogEx(ex, ref dataIntercept);

    }

    dataIntercept += ($"【執行完成結果】:{invocation.ReturnValue}");

    Parallel.For(0, 1, e =>
    {
        LogLock.OutSql2Log("AOPLog", new string[] { dataIntercept });
    });

    _hubContext.Clients.All.SendAsync("ReceiveUpdate", LogLock.GetLogData()).Wait();


}

 

 

四、添加到Autofac容器中,實現注入

 
還記得昨天的容器麼,先把攔截器注入,而後對程序集的注入方法中添加攔截器服務便可
 
        builder.RegisterType<BlogLogAOP>();//能夠直接替換其餘攔截器!必定要把攔截器進行註冊

            var assemblysServices = Assembly.Load("Blog.Core.Services");

            //builder.RegisterAssemblyTypes(assemblysServices).AsImplementedInterfaces();//指定已掃描程序集中的類型註冊爲提供全部其實現的接口。

            builder.RegisterAssemblyTypes(assemblysServices)
                      .AsImplementedInterfaces()
                      .InstancePerLifetimeScope()
                      .EnableInterfaceInterceptors()//引用Autofac.Extras.DynamicProxy;
                      .InterceptedBy(typeof(BlogLogAOP));//能夠直接替換攔截器
 
注意其中的兩個方法
.EnableInterfaceInterceptors()//對目標類型啓用接口攔截。攔截器將被肯定,經過在類或接口上截取屬性, 或添加 InterceptedBy ()
.InterceptedBy(typeof(BlogLogAOP));//容許將攔截器服務的列表分配給註冊。
說人話就是,將攔截器添加到要注入容器的接口或者類之上。
 

五、運行項目,查看效果

嗯,你就看到這根目錄下生成了一個Log文件夾,裏邊有日誌記錄,固然記錄很簡陋,裏邊是獲取到的實體類,你們能夠本身根據須要擴展

這裏,面向服務層的日誌記錄就完成了,你們感受是否是很平時的不同?緩存

 

2、AOP 之 實現接口數據的緩存功能

想想,若是咱們要實現緩存功能,通常我們都是將數據獲取到之後,定義緩存,而後在其餘地方使用的時候,在根據key去獲取當前數據,而後再操做等等,平時都是在API接口層獲取數據後進行緩存,今天我們能夠試試,在接口以前就緩存下來。
 

一、定義 Memory 緩存類和接口

老規矩,定義一個緩存類和接口,你會問了,爲何上邊的日誌沒有定義,由於我會在以後講Redis的時候用到這個緩存接口
   /// <summary>
    /// 簡單的緩存接口,只有查詢和添加,之後會進行擴展
    /// </summary>
    public interface ICaching
    {
        object Get(string cacheKey);

        void Set(string cacheKey, object cacheValue);
    }

   /// <summary>
    /// 實例化緩存接口ICaching
    /// </summary>
    public class MemoryCaching : ICaching
    {
        //引用Microsoft.Extensions.Caching.Memory;這個和.net 仍是不同,沒有了Httpruntime了
        private IMemoryCache _cache;
        //仍是經過構造函數的方法,獲取
        public MemoryCaching(IMemoryCache cache)
        {
            _cache = cache;
        }

        public object Get(string cacheKey)
        {
            return _cache.Get(cacheKey);
        }

        public void Set(string cacheKey, object cacheValue)
        {
            _cache.Set(cacheKey, cacheValue, TimeSpan.FromSeconds(7200));
        }
    }

 

 

二、定義一個緩存攔截器

仍是繼承IInterceptor,並實現Intercept
   /// <summary>
    /// 面向切面的緩存使用
    /// </summary>
    public class BlogCacheAOP : IInterceptor
    {
        //經過注入的方式,把緩存操做接口經過構造函數注入
        private ICaching _cache;
        public BlogCacheAOP(ICaching cache)
        {
            _cache = cache;
        }
        //Intercept方法是攔截的關鍵所在,也是IInterceptor接口中的惟必定義
        public void Intercept(IInvocation invocation)
        {
            //獲取自定義緩存鍵
            var cacheKey = CustomCacheKey(invocation);
            //根據key獲取相應的緩存值
            var cacheValue = _cache.Get(cacheKey);
            if (cacheValue != null)
            {
                //將當前獲取到的緩存值,賦值給當前執行方法
                invocation.ReturnValue = cacheValue;
                return;
            }
            //去執行當前的方法
            invocation.Proceed();
            //存入緩存
            if (!string.IsNullOrWhiteSpace(cacheKey))
            {
                _cache.Set(cacheKey, invocation.ReturnValue);
            }
        }

        //自定義緩存鍵
        private string CustomCacheKey(IInvocation invocation)
        {
            var typeName = invocation.TargetType.Name;
            var methodName = invocation.Method.Name;
            var methodArguments = invocation.Arguments.Select(GetArgumentValue).Take(3).ToList();//獲取參數列表,我最多須要三個便可

            string key = $"{typeName}:{methodName}:";
            foreach (var param in methodArguments)
            {
                key += $"{param}:";
            }

            return key.TrimEnd(':');
        }
        //object 轉 string
        private string GetArgumentValue(object arg)
        {
            if (arg is int || arg is long || arg is string)
                return arg.ToString();

            if (arg is DateTime)
                return ((DateTime)arg).ToString("yyyyMMddHHmmss");

            return "";
        }
    }

註釋的很清楚,基本都是狀況服務器

 

三、注入緩存攔截器

ConfigureServices不用動,只須要改下攔截器的名字就行
注意:

//將 TService 中指定的類型的範圍服務添加到實現
services.AddScoped<ICaching, MemoryCaching>();//記得把緩存注入!!!數據結構

 

四、運行,查看效果

你會發現,首次緩存是空的,而後將Repository倉儲中取出來的數據存入緩存,第二次使用就是有值了,其餘全部的地方使用,都不用再寫了,並且也是面向整個程序集合的

 

五、多個AOP執行順序問題 

在我最新的 Github 項目中,我定義了三個 AOP :除了上邊兩個 LogAOP和 CacheAOP 之外,還有一個 RedisCacheAOP,而且經過開關的形式在項目中配置是否啓用:

 

那具體的執行順序是什麼呢,這裏說下,就是從上至下的順序,或者能夠理解成挖金礦的形式,執行完上層的,而後緊接着來下一個AOP,最後想要回家,就再一個一個跳出去,在往上層走的時候,礦確定就執行完了,就不用再操做了,直接出去,就像 break 同樣,能夠參考這個動圖:

 

 

 

六、無接口如何實現AOP

 

上邊咱們討論了不少,可是都是接口框架的,

好比:Service.dll 和與之對應的 IService.dll,Repository.dll和與之對應的 IRepository.dll,咱們能夠直接在對應的層注入的時候,匹配上 AOP 信息,可是若是咱們沒有使用接口怎麼辦?

這裏你們能夠安裝下邊的實驗下:

 

Autofac它只對接口方法 或者 虛virtual方法或者重寫方法override才能起攔截做用。  

 

若是沒有接口

案例是這樣的:

 若是咱們的項目是這樣的,沒有接口,會怎麼辦:

 
    // 服務層類 
   public class StudentService
    {
        StudentRepository _studentRepository;
        public StudentService(StudentRepository studentRepository)
        {
            _studentRepository = studentRepository;
        }


        public string Hello()
        {
            return _studentRepository.Hello();
        }

    }


    // 倉儲層類
     public class StudentRepository
    {
        public StudentRepository()
        {

        }

        public string Hello()
        {
            return "hello world!!!";
        }

    }


    // controller 接口調用
    StudentService _studentService;

    public ValuesController(StudentService studentService)
    {
        _studentService = studentService;
    }
 

 

 

若是是沒有接口的單獨實體類

 
    public class Love
    {
        // 必定要是虛方法
        public virtual string SayLoveU()
        {
            return "I ♥ U";
        }

    }

//---------------------------

//只能注入該類中的虛方法
builder.RegisterAssemblyTypes(Assembly.GetAssembly(typeof(Love)))
    .EnableClassInterceptors()
    .InterceptedBy(typeof(BlogLogAOP));
 

 

 

3、還有其餘的一些問題須要考慮

一、能夠針對某一層的指定類的指定方法進行操做,這裏就不寫了,你們能夠本身實驗
配合Attribute就能夠只攔截相應的方法了。由於攔截器裏面是根據Attribute進行相應判斷的!!
builder.RegisterAssemblyTypes(assembly)
   .Where(type => typeof(IQCaching).IsAssignableFrom(type) && !type.GetTypeInfo().IsAbstract) .AsImplementedInterfaces()
   .InstancePerLifetimeScope()
   .EnableInterfaceInterceptors()
   .InterceptedBy(typeof(QCachingInterceptor));
二、時間問題,阻塞,浪費資源問題等
  定義切面有時候是方便,初次使用會很彆扭,使用多了,可能會對性能有些許的影響,由於會大量動態生成代理類,性能損耗,是特別高的請求併發,好比萬級每秒,仍是不建議生產環節推薦。因此說切面編程要深刻的研究,不可隨意使用,我說的也是九牛一毛,你們繼續加油吧!
 
三、靜態注入

基於Net的IL語言層級進行注入,性能損耗能夠忽略不計,Net使用最多的Aop框架PostSharp(好像收費了;)採用的便是這種方式。

你們能夠參考這個博文:https://www.cnblogs.com/mushroom/p/3932698.html

 

4、結語

  今天的講解就到了這裏了,經過這兩個小栗子,你們應該能對面向切面編程有一些朦朧的感受了吧,感興趣的能夠深刻的研究,也歡迎一塊兒討論,剛剛在緩存中,我說到了緩存接口,就引入了下次的講解內容,Redis的高性能緩存框架,內存存儲的數據結構服務器,可用做數據庫,高速緩存和消息隊列代理。下次再見咯~

 

一、網友好資料

  1. 帶你學習AOP框架之Aspect.Core[1]

 

5、Github && Gitee

相關文章
相關標籤/搜索