學習ASP.NET Core(07)-AOP動態代理與日誌

上一篇咱們簡單介紹了RESTful WebAPI涉及到的一些基礎知識,並初步完善了系統的一些功能;本章咱們將介紹下AOP並使用動態代理的方式實現記錄日誌的功能html


1、面向切面編程

一、什麼是AOP

AOP是Accept Oriented Programming的縮寫,即面向切面編程。它與IOC控制反轉,OOP面向對象編程等思想同樣,都是一種編程思想,它是經過預編譯方式和運行期間動態代理的方式來實現程序功能統一維護的一種技術。簡單來講就是就是在不影響核心邏輯的狀況下,爲程序提供"可拔插"的擴展功能。以下圖,來源爲—韓俊俊,什麼是面向切面編程AOPvue

二、AOP思想的產生

隨着關注點的不一樣會致使不一樣的切面,好比部分方法須要受權才能繼續操做,一些方法中咱們須要記錄日誌或是異常信息等,它們與核心邏輯沒有必然的聯繫,它們獨立且分散卻又是程序中必不可少的一部分。C#語言是一種面嚮對象語言,它會基於OOP思想的封裝、繼承、多態三大特性將公共行爲封裝爲一個類,可是當咱們須要將獨立的對象引入公共行爲時,會發現它與OOP思想產生了必定的衝突,這時就須要運用AOP思想來解決這類問題。編程

三、AOP相關術語(瞭解)

  1. 橫切關注點:用於一個系統的多個部分的片斷功能,好比受權驗證,日誌記錄,異常處理等;
  2. 通知(Advice):執行橫切關注點(獨立功能)的代碼;
  3. 鏈接點(JoinPoint):程序執行通知的地方,好比一個類裏面有10個方法,那麼這10個方法在建立方法對象前,建立完成調用方法前以及調用方法後均可以看做是一個鏈接點;
  4. 切入點(PointCut):至關於AOP的「where」,它是鏈接點的」集合「,好比上面10個方法只想在其中幾個鏈接點使用通知,那麼這幾個鏈接點就稱爲切入點;
  5. 切面(Aspect):切面是通知和切入點的結合;
  6. 引入(Introduction): 容許咱們向現有的類添加新的方法或屬性,就是把切面用到目標類中;
  7. 目標(Target): 引入中所提到的目標類,也就是要被通知的對象,也就是真正的業務邏輯;
  8. 代理(Porxy): 向目標對象增長通知以後建立的對象,由這個對象來訪問實際的目標對象;
  9. 織入(Weaving): 將切面應用到目標對象來建立新的代理對象的過程;

四、.NET Core中的實現

在.NET Core中,實現AOP思想的經常使用對象有中間件(Middleware)、過濾器(Filter)和基於AOP思想的攔截器。其中攔截器又分爲靜態代理、動態代理;靜態代理會在編譯時靜態植入,優勢是效率高,缺點是缺少靈活性;動態代理會爲目標建立代理,經過代理調用實現攔截,優勢是靈活性強,缺點是會影響部分效率。c#

上述三個對象它們對應了不一樣的應用場景:後端

  • 中間件:處理的是請求管道;一般用於底層服務的通訊
  • 過濾器:處理的是Action方法和URL;一般用於身份驗證,參數驗證等
  • 攔截器:處理的是對象的元數據,包括類、方法名、參數等;一般用於配合處理業務邏輯

2、AOP動態代理

一般狀況下,當咱們想記錄項目接口的調用狀況時,可使用過濾器或者自定義一箇中間件來實現,但若是想看下與數據層或邏輯層的調用狀況,就比較複雜了,在這些層級中進行添加輸出日誌的功能顯然不是一個合理的解決辦法。這裏咱們採用動態代理的方式來解決,其核心思想就是將服務的實例交給代理類來控制,代理類能夠在其內部方法中控制執行或者是添加本身的處理邏輯,下面咱們來看下記錄邏輯層調用信息的具體實現。網絡

一、引入動態代理

其實反射類Reflection中已經封裝了代理方法,可是須要在StartUp中的ConfigureServices方法裏指明代理類與服務實例的映射關係,這就致使沒有較好的方法在控制器中使用。前後端分離

因爲以前咱們已經使用Autofac容器替換了系統容器,因此這裏咱們能夠選擇使用一款封裝好了的且與Autofac配合度較高的第三方插件Castle.Core,在BlogSystem.Core層使用NuGet安裝以下包,它包含了Castle.Core異步

二、設計攔截器

在BlogSystem.Core層中添加AOP文件夾,並添加一個名爲LogAop的類,繼承自攔截器接口IInterceptor(須要引用Castle.DynamicProxy)並實現其方法,這裏咱們先添加invocation.Proceed()方法,以下:學習

以後咱們就能夠在該方法內部自定義相關邏輯的,須要注意的是咱們的系統內部大多數是異步操做,因此須要判斷是否爲異步方法並進行攔截,不然會攔截失敗。這裏邏輯基本上是參照的老張的哲學的,我的就稍微改了下,具體實現以下:spa

using BlogSystem.Core.Helpers;
using Castle.DynamicProxy;
using Microsoft.AspNetCore.Http;
using Newtonsoft.Json;
using System;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;

namespace BlogSystem.Core.AOP
{
    public class LogAop : IInterceptor
    {
        private readonly IHttpContextAccessor _accessor;

        private static readonly string FileName = "AOPInterceptor-" + DateTime.Now.ToString("yyyyMMddHH") + ".log";

        //支持單個寫線程和多個讀線程的鎖
        private static readonly ReaderWriterLockSlim Lock = new ReaderWriterLockSlim();

        public LogAop(IHttpContextAccessor accessor)
        {
            _accessor = accessor ?? throw new ArgumentNullException(nameof(accessor));
        }

        public void Intercept(IInvocation invocation)
        {
            var userId = JwtHelper.JwtDecrypt(_accessor.HttpContext.Request.Headers["Authorization"]).UserId;

            //記錄被攔截方法執行前的信息
            var logData = $"【執行用戶】:{userId} \r\n" +
                          $"【執行時間】:{DateTime.Now:yyyy/MM/dd HH:mm:ss}  \r\n" +
                          $"【執行方法】: {invocation.Method.Name}  \r\n" +
                          $"【執行參數】:{string.Join(", ", invocation.Arguments.Select(x => (x ?? "").ToString()).ToArray())} \r\n";
            try
            {
                //調用下一個攔截器直到目標方法
                invocation.Proceed();

                //判斷是否爲異步方法
                if (IsAsyncMethod(invocation.Method))
                {
                    var type = invocation.Method.ReturnType;
                    var resultProperty = type.GetProperty("Result");
                    if (resultProperty == null) return;
                    var result = resultProperty.GetValue(invocation.ReturnValue);
                    logData += $"【執行完成】:{JsonConvert.SerializeObject(result)}";
                    Parallel.For(0, 1, e =>
                    {
                        WriteLog(new[] { logData });
                    });
                }
                else//同步方法
                {
                    logData += $"【執行完成】:{invocation.ReturnValue}";
                    Parallel.For(0, 1, e =>
                    {
                        WriteLog(new[] { logData });
                    });
                }

            }
            catch (Exception ex)
            {
                LogException(ex, logData);
            }
        }

        //判斷是否爲異步方法
        private bool IsAsyncMethod(MethodInfo method)
        {
            return method.ReturnType == typeof(Task) ||
                   method.ReturnType.IsGenericType && method.ReturnType.GetGenericTypeDefinition() == typeof(Task<>);
        }

        //日誌寫入方法
        public static void WriteLog(string[] parameters, bool isHeader = true)
        {
            try
            {
                //進入寫模式
                Lock.EnterWriteLock();

                //獲取或建立文件夾
                var path = Path.Combine(Directory.GetCurrentDirectory(), "AOPLog");
                if (!Directory.Exists(path))
                {
                    Directory.CreateDirectory(path);
                }

                //獲取log文件路徑
                var logFilePath = Path.Combine(path, FileName);

                //轉換及拼接字符
                var logContent = string.Join("\r\n", parameters);
                if (isHeader)
                {
                    logContent = "---------------------------------------\r\n"
                                 + DateTime.Now + "\r\n" + logContent + "\r\n";
                }

                //寫入文件
                File.AppendAllText(logFilePath, logContent);
            }
            catch (Exception e)
            {
                Console.WriteLine(e);
            }
            finally
            {
                //退出寫入模式,釋放資源佔用
                Lock.ExitWriteLock();
            }
        }

        //記錄異常信息
        private void LogException(Exception ex, string logData)
        {
            if (ex == null) return;

            logData += $"【出現異常】:{ex.Message + ex.InnerException}\r\n";

            Parallel.For(0, 1, e =>
            {
                WriteLog(new[] { logData });
            });
        }
    }
}

三、注入服務和分配攔截器

動態代理代理的是服務,從咱們的項目結構上看就是BLL層。這裏咱們在StartUp類中基於Autofac實現的方法ConfigureContainer內部進行攔截器的註冊和分配操做,原先DALL和BLL寫在一塊兒了,這裏須要拆開,以下:

四、運行實現效果

運行後執行兩個方法,效果以下圖所示。可是這裏存在一個小問題,就是在用戶已登陸的狀況下,Swagger執行無需受權的方法時是不傳遞jwt字段的,因此這裏userId爲空,暫時沒有找到解決方案,有了解的朋友可在評論區告知,先在此謝過

本章完~


本人知識點有限,若文中有錯誤的地方請及時指正,方便你們更好的學習和交流。

本文部份內容參考了網絡上的視頻內容和文章,僅爲學習和交流,地址以下:

老張的哲學,系列教程一目錄:.netcore+vue 先後端分離

韓俊俊,什麼是面向切面編程AOP

聲明

相關文章
相關標籤/搜索