上一篇我們簡單介紹了RESTful WebAPI涉及到的一些基礎知識,並初步完善了系統的一些功能;本章我們將介紹下AOP並使用動態代理的方式實現記錄日誌的功能
一、面向切面編程
1、什麼是AOP
AOP是Accept Oriented Programming的縮寫,即面向切面編程。它與IOC控制反轉,OOP面向對象編程等思想一樣,都是一種編程思想,它是通過預編譯方式和運行期間動態代理的方式來實現程序功能統一維護的一種技術。簡單來說就是就是在不影響核心邏輯的情況下,爲程序提供"可拔插"的擴展功能。如下圖,來源爲—韓俊俊,什麼是面向切面編程AOP
2、AOP思想的產生
隨着關注點的不同會導致不同的切面,比如部分方法需要授權才能繼續操作,一些方法中我們需要記錄日誌或是異常信息等,它們與核心邏輯沒有必然的聯繫,它們獨立且分散卻又是程序中必不可少的一部分。C#語言是一種面嚮對象語言,它會基於OOP思想的封裝、繼承、多態三大特性將公共行爲封裝爲一個類,但是當我們需要將獨立的對象引入公共行爲時,會發現它與OOP思想產生了一定的衝突,這時就需要運用AOP思想來解決這類問題。
3、AOP相關術語(瞭解)
- 橫切關注點:用於一個系統的多個部分的片段功能,比如授權驗證,日誌記錄,異常處理等;
- 通知(Advice):執行橫切關注點(獨立功能)的代碼;
- 連接點(JoinPoint):程序執行通知的地方,比如一個類裏面有10個方法,那麼這10個方法在創建方法對象前,創建完成調用方法前以及調用方法後都可以看作是一個連接點;
- 切入點(PointCut):相當於AOP的“where”,它是連接點的”集合“,比如上面10個方法只想在其中幾個連接點使用通知,那麼這幾個連接點就稱爲切入點;
- 切面(Aspect):切面是通知和切入點的結合;
- 引入(Introduction): 允許我們向現有的類添加新的方法或屬性,就是把切面用到目標類中;
- 目標(Target): 引入中所提到的目標類,也就是要被通知的對象,也就是真正的業務邏輯;
- 代理(Porxy): 向目標對象增加通知之後創建的對象,由這個對象來訪問實際的目標對象;
- 織入(Weaving): 將切面應用到目標對象來創建新的代理對象的過程;
4、.NET Core中的實現
在.NET Core中,實現AOP思想的常用對象有中間件(Middleware)、過濾器(Filter)和基於AOP思想的攔截器。其中攔截器又分爲靜態代理、動態代理;靜態代理會在編譯時靜態植入,優點是效率高,缺點是缺乏靈活性;動態代理會爲目標創建代理,通過代理調用實現攔截,優點是靈活性強,缺點是會影響部分效率。
上述三個對象它們對應了不同的應用場景:
- 中間件:處理的是請求管道;通常用於底層服務的通信
- 過濾器:處理的是Action方法和URL;通常用於身份驗證,參數驗證等
- 攔截器:處理的是對象的元數據,包括類、方法名、參數等;通常用於配合處理業務邏輯
二、AOP動態代理
通常情況下,當我們想記錄項目接口的調用情況時,可以使用過濾器或者自定義一箇中間件來實現,但如果想看下與數據層或邏輯層的調用情況,就比較複雜了,在這些層級中進行添加輸出日誌的功能顯然不是一個合理的解決辦法。這裏我們採用動態代理的方式來解決,其核心思想就是將服務的實例交給代理類來控制,代理類可以在其內部方法中控制執行或者是添加自己的處理邏輯,下面我們來看下記錄邏輯層調用信息的具體實現。
1、引入動態代理
其實反射類Reflection中已經封裝了代理方法,但是需要在StartUp中的ConfigureServices方法裏指明代理類與服務實例的映射關係,這就導致沒有較好的方法在控制器中使用。
由於之前我們已經使用Autofac容器替換了系統容器,所以這裏我們可以選擇使用一款封裝好了的且與Autofac配合度較高的第三方插件Castle.Core,在BlogSystem.Core層使用NuGet安裝如下包,它包含了Castle.Core
2、設計攔截器
在BlogSystem.Core層中添加AOP文件夾,並添加一個名爲LogAop的類,繼承自攔截器接口IInterceptor(需要引用Castle.DynamicProxy)並實現其方法,這裏我們先添加invocation.Proceed()方法,如下:
之後我們就可以在該方法內部自定義相關邏輯的,需要注意的是我們的系統內部大多數是異步操作,所以需要判斷是否爲異步方法並進行攔截,否則會攔截失敗。這裏邏輯基本上是參照的老張的哲學的,個人就稍微改了下,具體實現如下:
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 });
});
}
}
}
3、注入服務和分配攔截器
動態代理代理的是服務,從我們的項目結構上看就是BLL層。這裏我們在StartUp類中基於Autofac實現的方法ConfigureContainer內部進行攔截器的註冊和分配操作,原先DALL和BLL寫在一起了,這裏需要拆開,如下:
4、運行實現效果
運行後執行兩個方法,效果如下圖所示。但是這裏存在一個小問題,就是在用戶已登錄的情況下,Swagger執行無需授權的方法時是不傳遞jwt字段的,所以這裏userId爲空,暫時沒有找到解決方案,有了解的朋友可在評論區告知,先在此謝過
本章完~
本人知識點有限,若文中有錯誤的地方請及時指正,方便大家更好的學習和交流。
本文部分內容參考了網絡上的視頻內容和文章,僅爲學習和交流,地址如下:
老張的哲學,系列教程一目錄:.netcore+vue 前後端分離
韓俊俊,什麼是面向切面編程AOP