切面是异步还是同步操作‘_【 .NET Core 3.0 】框架之十 || AOP 切面思想
本文有配套視頻:
https://www.bilibili.com/video/av58096866/?p=6
?前言
上回《【 .NET Core3.0 】框架之九 || 依賴注入IoC學習 + AOP界面編程初探》咱們說到了依賴注入Autofac的使用,不知道大家對IoC的使用是怎樣的感覺,我個人表示還是比較可行的,至少不用自己再關心一個個復雜的實例化服務對象了,直接通過接口就滿足需求,當然還有其他的一些功能,我還沒有說到,拋磚引玉嘛,大家如果有好的想法,歡迎留言,也可以來群里,大家一起學習討論。昨天在文末咱們說到了AOP面向切面編程的定義和思想,我個人簡單使用了下,感覺主要的思路還是通過攔截器來操作,就像是一個中間件一樣,今天呢,我給大家說兩個小栗子,當然,你也可以合并成一個,也可以自定義擴展,因為我們是真個系列是基于Autofac框架,所以今天主要說的是基于Autofac的Castle動態代理的方法,靜態注入的方式以后有時間可以再補充。
時間真快,轉眼已經十天過去了,感謝大家的鼓勵,批評指正,希望我的文章,對您有一點點兒的幫助,哪怕是有學習新知識的動力也行,至少至少,可以為以后跳槽增加新的談資 [哭笑],這些天我們從面向對象OOP的開發,后又轉向了面向接口開發,到分層解耦,現在到了面向切面編程AOP,往下走將會是,分布式,微服務等等,技術真是永無止境啊!好啦,馬上開始動筆。
? 一、什么是 AOP 切面編程思想
什么是AOP?引用百度百科:AOP為Aspect Oriented Programming的縮寫,意為:面向切面編程,通過預編譯方式和運行期動態代理實現程序功能的統一維護的一種技術。實現AOP主要由兩種方式,
一種是編譯時靜態織入,優點是效率高,缺點是缺乏靈活性,.net下postsharp為代表者(好像是付費了。。)。
另一種方式是動態代理,優點是靈活性強,但是會影響部分效率,動態為目標類型創建代理,通過代理調用實現攔截。
AOP能做什么,常見的用例是事務處理、日志記錄等等。
常見的AOP都是配合在Ioc的基礎上進行操作,上邊咱們講了Autofac這個簡單強大的Ioc框架,下面就講講Autofac怎么實現AOP。Autofac的AOP是通過Castle(也是一個容器)項目的核心部分實現的,名為Autofac.Extras.DynamicProxy,顧名思義,其實現方式為動態代理。當然AOP并不一定要和依賴注入在一起使用,自身也可以單獨使用。
是不是很拗口,沒關系,網上有一個博友的圖片,大概講了AOP切面編程:
?
說的很通俗易懂的話就是,我們在 service 方法的前邊和后邊,各自動態增加了一個方法,這樣就包裹了每一個服務方法,從而實現業務邏輯的解耦。
AOP,我們并不陌生。可能大家感覺這個切面編程思想之前沒有用到過,很新鮮的一個東西,其實不是的,之前我們開發的時候也一直在使用這種思想,那就是過濾器,我們可以想想,我們之前在開發 MVC 的時候,是不是經常要對action進行控制過濾,最常見的就是全局異常處理過濾器,只要有錯誤,就跳出去,記錄日志,然后去一個自定義的異常頁面,這個其實就是一個 AOP 的思想,但是這里請注意,這個思想是廣義的 AOP 編程思想,今天要說的,是真正意義上的切面編程思想,是基于動態代理的基于服務層的編程思想,也是在以后的開發中使用很多的一種編程思想。
?二、AOP 之實現日志記錄
首先想一想,如果有這么一個需求,要記錄整個項目的接口和調用情況,當然如果只是控制器的話,還是挺簡單的,直接用一個過濾器或者一個中間件,還記得咱們開發Swagger攔截權限驗證的中間件么,那個就很方便的把用戶調用接口的名稱記錄下來,當然也可以寫成一個切面,但是如果想看下與Service或者Repository層的調用情況呢,好像目前咱們只能在Service層或者Repository層去寫日志記錄了,那樣的話,不僅工程大(當然你可以用工廠模式),而且耦合性瞬間就高了呀,想象一下,如果日志要去掉,關閉,修改,需要改多少地方!您說是不是,好不容易前邊的工作把層級的耦合性降低了。別慌,這個時候就用到了AOP和Autofac的Castle結合的完美解決方案了。
經過這么多天的開發,幾乎每天都需要引入Nuget包哈,我個人表示也不想再添加了,現在都已經挺大的了(47M當然包括全部dll文件),今天不會啦!其實都是基于昨天的兩個Nuget包中已經自動生成的Castle組件。請看以下步驟:
1、定義服務接口與實現類
在上一篇文章中,我們說到了使用
AdvertisementServices.cs 和 IAdvertisementServices.cs這個服務,我們新建兩個層,分別包含這兩個 cs 文件:
然后我們模擬下數據,再新建一個 Model 層,添加 AdvertisementEntity 實體類
namespace Blog.Core.Model{ public class AdvertisementEntity { public int id { get; set; } public string name { get; set; } }}然后在上邊的 service 方法中,返回一個List數據:
// 接口 public interface IAdvertisementServices { int Test(); ListTestAOP(); } // 實現類 public class AdvertisementServices : IAdvertisementServices { public int Test() { return 1; } public ListTestAOP() => new List() { new AdvertisementEntity() { id = 1, name = "laozhang" } }; }2、在API層中添加對該接口引用
還是在默認的控制器——weatherForecastController.cs 里,添加調用方法:
/// /// 測試AOP /// /// [HttpGet] public ListTestAdsFromAOP() { return _advertisementServices.TestAOP(); }這里采用的是依賴注入的方法,把 _advertisementServices 注入到控制器的,如果還不會,請看我上一篇文章。
3、添加AOP攔截器
在api層新建文件夾AOP,添加攔截器BlogLogAOP,并設計其中用到的日志記錄Logger方法或者類
關鍵的一些知識點,注釋中已經說明了,主要是有以下:
1、繼承接口IInterceptor2、實例化接口IINterceptor的唯一方法Intercept3、void Proceed();表示執行當前的方法4、執行后,輸出到日志文件。
namespace blog.core.test3._0.AOP{ /// /// 攔截器BlogLogAOP 繼承IInterceptor接口 /// public class BlogLogAOP : IInterceptor { /// /// 實例化IInterceptor唯一方法 /// /// 包含被攔截方法的信息 public void Intercept(IInvocation invocation) { // 事前處理: 在服務方法執行之前,做相應的邏輯處理 var dataIntercept = "" + $"【當前執行方法】:{ invocation.Method.Name} \r\n" + $"【攜帶的參數有】: {string.Join(", ", invocation.Arguments.Select(a => (a ?? "").ToString()).ToArray())} \r\n"; // 執行當前訪問的服務方法,(注意:如果下邊還有其他的AOP攔截器的話,會跳轉到其他的AOP里) invocation.Proceed(); // 事后處理: 在service被執行了以后,做相應的處理,這里是輸出到日志文件 dataIntercept += ($"【執行完成結果】:{invocation.ReturnValue}"); // 輸出到日志文件 Parallel.For(0, 1, e => { LogLock.OutSql2Log("AOPLog", new string[] { dataIntercept }); }); } }}提示:這里展示了如何在項目中使用AOP實現對 service 層進行日志記錄,如果你想實現異常信息記錄的話,很簡單,
注意,這個方法僅僅是針對同步的策略,如果你的service是異步的,這里獲取不到,正確的寫法,在文章底部的 GitHub 代碼里,因為和 AOP 思想沒有直接的關系,這里就不贅述。
4、將攔截器注入容器,代理服務
還記得昨天的Autofac容器 ConfigureContainer 么,我們繼續對它進行處理:
1、先把攔截器注入容器;
2、然后對程序集的注入方法中匹配攔截器服務;
public void ConfigureContainer(ContainerBuilder builder) { var basePath = Microsoft.DotNet.PlatformAbstractions.ApplicationEnvironment.ApplicationBasePath; //直接注冊某一個類和接口 //左邊的是實現類,右邊的As是接口 builder.RegisterType().As(); builder.RegisterType();//可以直接替換其他攔截器!一定要把攔截器進行注冊 //注冊要通過反射創建的組件 var servicesDllFile = Path.Combine(basePath, "Blog.Core.Services.dll"); var assemblysServices = Assembly.LoadFrom(servicesDllFile); builder.RegisterAssemblyTypes(assemblysServices) .AsImplementedInterfaces() .InstancePerLifetimeScope() .EnableInterfaceInterceptors() .InterceptedBy(typeof(BlogLogAOP));//可以放一個AOP攔截器集合 }注意其中的兩個方法
.EnableInterfaceInterceptors()//對目標類型啟用接口攔截。攔截器將被確定,通過在類或接口上截取屬性, 或添加 InterceptedBy ()
.InterceptedBy(typeof(BlogLogAOP));//允許將攔截器服務的列表分配給注冊。
說人話就是,將攔截器添加到要注入容器的接口或者類之上。
5、運行項目,查看效果
這個時候,我們運行項目,然后訪問api 的 TestAdsFromAOP() 接口,你就看到這根目錄下生成了一個Log文件夾,里邊有日志記錄,當然記錄很簡陋,里邊是獲取到的實體類,大家可以自己根據需要擴展。
這里,面向服務層的日志記錄就完成了,大家感覺是不是很平時的不一樣?我們幾乎什么都沒做,只是增加了一個AOP的攔截器,就可以控制 service 層的任意一個方法,這就是AOP思想的精髓——業務的解耦。
那AOP僅僅是做日志記錄么,還有沒有其他的用途,這里我隨便舉一個例子——緩存。
?三、AOP 實現數據緩存功能
想一想,如果我們要實現緩存功能,一般咱們都是將數據獲取到以后,定義緩存,然后在其他地方使用的時候,在根據key去獲取當前數據,然后再操作等等,平時都是在API接口層獲取數據后進行緩存,今天咱們可以試試,在接口之前就緩存下來 —— 基于service層的緩存策略。
1、定義 Memory 緩存類和接口
這里既然要用到緩存,那我們就定義一個緩存類和接口,在 Helper 文件夾下,新建兩個類文件,ICaching.cs 和 MemoryCaching.cs
你會問了,為什么上邊的日志沒有定義,因為我會在之后講Redis的時候用到這個緩存接口。
/// /// 簡單的緩存接口,只有查詢和添加,以后會進行擴展 /// public interface ICaching {????????object?Get(string?cacheKey); void Set(string cacheKey, object cacheValue); } /// /// 實例化緩存接口ICaching /// 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)); } }2、定義一個緩存攔截器
還是繼承IInterceptor,并實現Intercept,這個過程和上邊 日志AOP 是一樣,不多說,大家也正好可以自己動手練習一下。
新建緩存AOP:BlogCacheAOP.cs
/// /// 面向切面的緩存使用 /// ?public?class?BlogCacheAOP?:?AOPbase { //通過注入的方式,把緩存操作接口通過構造函數注入 private readonly ICaching _cache; public BlogCacheAOP(ICaching cache) { _cache = cache; } //Intercept方法是攔截的關鍵所在,也是IInterceptor接口中的唯一定義 public override 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); } } }代碼中注釋的很清楚,需要注意是兩點:
1、采用依賴注入,把緩存注入到當前攔截器里;
2、繼承了一個 AOPBase 抽象類,里邊有如何定義緩存 key 等內容;
namespace blog.core.test3._0.AOP{ public abstract class AOPbase : IInterceptor { /// /// AOP的攔截方法 /// /// public abstract void Intercept(IInvocation invocation); /// /// 自定義緩存的key /// /// /// protected 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 = $"{key}{param}:"; } return key.TrimEnd(':'); } /// /// object 轉 string /// /// /// protected static string GetArgumentValue(object arg) { if (arg is DateTime || arg is DateTime?) return ((DateTime)arg).ToString("yyyyMMddHHmmss"); if (arg is string || arg is ValueType || arg is Nullable) return arg.ToString(); if (arg != null) { if (arg.GetType().IsClass) { return MD5Encrypt16(Newtonsoft.Json.JsonConvert.SerializeObject(arg)); } } return string.Empty; } /// /// 16位MD5加密 /// /// /// public static string MD5Encrypt16(string password) { var md5 = new MD5CryptoServiceProvider(); string t2 = BitConverter.ToString(md5.ComputeHash(Encoding.Default.GetBytes(password)), 4, 8); t2 = t2.Replace("-", string.Empty); return t2; } }}3、注入攔截器到服務
具體的操作方法,上邊我們都已經說到了,大家依然可以自己練習一下,這里直接把最終的代碼展示一下:
注意://將 TService 中指定的類型的范圍服務添加到實現?services.AddScoped();//記得把緩存注入!!!
4、運行,查看效果
你會發現,首次緩存是空的,然后將serv中取出來的數據存入緩存,第二次使用就是有值了,其他所有的地方使用,都不用再寫了,而且也是面向整個程序集合的
5、多個AOP執行順序問題?
在我最新的 Github 項目中,我定義了四個 AOP :除了上邊兩個 LogAOP和 CacheAOP 以外,還有一個 RedisCacheAOP 和 事務BlogTranAOP,并且通過開關的形式在項目中配置是否啟用:
那具體的執行順序是什么呢,這里說下,就是從上至下的順序,或者可以理解成挖金礦的形式,執行完上層的,然后緊接著來下一個AOP,最后想要回家,就再一個一個跳出去,在往上層走的時候,礦肯定就執行完了,就不用再操作了,直接出去,就像 break 一樣。
6、無接口如何實現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));到了這里,我們已經明白了什么是AOP切面編程,也通過兩個業務邏輯學會了如何去使用AOP編程,那這里有一個小問題,如果我某些service類和方法并不想做相應的AOP處理,該如何篩選呢?請繼續看。
?四、給緩存增加驗證篩選
1、自定義緩存特性
在解決方案中添加新項目Blog.Core.Common,然后在該Common類庫中添加 特性文件夾 和 特性實體類,以后特性就在這里
/// /// 這個Attribute就是使用時候的驗證,把它添加到要緩存數據的方法中,即可完成緩存的操作。注意是對Method驗證有效 /// [AttributeUsage(AttributeTargets.Method, Inherited = true)] public class CachingAttribute : Attribute { //緩存絕對過期時間 public int AbsoluteExpiration { get; set; } = 30; }2、在AOP攔截器中進行過濾
添加Common程序集引用,然后修改緩存AOP類方法 BlogCacheAOP=》Intercept,簡單對方法的方法進行判斷
/// /// 面向切面的緩存使用/// public class BlogCacheAOP : AOPbase{ //通過注入的方式,把緩存操作接口通過構造函數注入 private readonly ICaching _cache; public BlogCacheAOP(ICaching cache) { _cache = cache; } //Intercept方法是攔截的關鍵所在,也是IInterceptor接口中的唯一定義 public override void Intercept(IInvocation invocation) { var method = invocation.MethodInvocationTarget ?? invocation.Method; //對當前方法的特性驗證 var qCachingAttribute = method.GetCustomAttributes(true).FirstOrDefault(x => x.GetType() == typeof(CachingAttribute)) as CachingAttribute; //只有那些指定的才可以被緩存,需要驗證 if (qCachingAttribute != null)????????{ //獲取自定義緩存鍵 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); } } }}我們增加了一個 if 判斷,只有那些帶有緩存特性的類和方法才會被執行這個 AOP 攔截。
3、在service層中增加緩存特性
在指定的Service層中的某些類的某些方法上增加特性(一定是方法,不懂的可以看定義特性的時候AttributeTargets.Method)
4、特定緩存效果展示
運行項目,打斷點,就可以看到,普通的Query或者CURD等都不繼續緩存了,只有咱們特定的 getBlogs()方法,帶有緩存特性的才可以
當然,這里還有一個小問題,就是所有的方法還是走的切面,只是增加了過濾驗證,大家也可以直接把那些需要的注入,不需要的干脆不注入Autofac容器,我之所以需要都經過的目的,就是想把它和日志結合,用來記錄Service層的每一個請求,包括CURD的調用情況。
五、基于AOP的Redis緩存
1、核心:Redis緩存切面攔截器
?在上篇文章中,我們已經定義過了一個攔截器,只不過是基于內存Memory緩存的,并不適應于Redis,上邊咱們也說到了Redis必須要存入指定的值,比如字符串,而不能將異步對象 Task 保存到硬盤上,所以我們就修改下攔截器方法,一個專門應用于 Redis 的切面攔截器:
/// /// 面向切面的緩存使用 /// public class BlogRedisCacheAOP : CacheAOPbase { //通過注入的方式,把緩存操作接口通過構造函數注入 private readonly IRedisCacheManager _cache; public BlogRedisCacheAOP(IRedisCacheManager cache) { _cache = cache; } //Intercept方法是攔截的關鍵所在,也是IInterceptor接口中的唯一定義 public override void Intercept(IInvocation invocation) { var method = invocation.MethodInvocationTarget ?? invocation.Method; //對當前方法的特性驗證 var qCachingAttribute = method.GetCustomAttributes(true).FirstOrDefault(x => x.GetType() == typeof(CachingAttribute)) as CachingAttribute; if (qCachingAttribute != null) { //獲取自定義緩存鍵 var cacheKey = CustomCacheKey(invocation); //注意是 string 類型,方法GetValue var cacheValue = _cache.GetValue(cacheKey); if (cacheValue != null) { //將當前獲取到的緩存值,賦值給當前執行方法 var type = invocation.Method.ReturnType; var resultTypes = type.GenericTypeArguments; if (type.FullName == "System.Void") { return; } object response; if (typeof(Task).IsAssignableFrom(type)) { //返回Task if (resultTypes.Any()) { var resultType = resultTypes.FirstOrDefault(); // 核心1,直接獲取 dynamic 類型????????????????????????????dynamic?temp?=?Newtonsoft.Json.JsonConvert.DeserializeObject(cacheValue,?resultType);??????????????????????????????????????????????????????response?=?Task.FromResult(temp); } else { //Task 無返回方法 指定時間內不允許重新運行 response = Task.Yield(); } } else { // 核心2,要進行 ChangeType response = Convert.ChangeType(_cache.Get<object>(cacheKey), type); } invocation.ReturnValue = response; return; } //去執行當前的方法 invocation.Proceed(); //存入緩存 if (!string.IsNullOrWhiteSpace(cacheKey)) { object response; //Type type = invocation.ReturnValue?.GetType(); var type = invocation.Method.ReturnType; if (typeof(Task).IsAssignableFrom(type)) { var resultProperty = type.GetProperty("Result"); response = resultProperty.GetValue(invocation.ReturnValue); } else { response = invocation.ReturnValue; } if (response == null) response = string.Empty; _cache.Set(cacheKey, response, TimeSpan.FromMinutes(qCachingAttribute.AbsoluteExpiration)); } } else { invocation.Proceed();//直接執行被攔截方法 } } }上邊的代碼和memory緩存的整體結構差不多的,相信都能看的懂的,最后我們就可以很任性的在Autofac容器中,進行任意緩存切換了,是不是很棒!
再次感覺小伙伴JoyLing,不知道他博客園地址。
?六、一些其他問題需要考慮
1、時間問題,阻塞,浪費資源問題等
? 定義切面有時候是方便,初次使用會很別扭,使用多了,可能會對性能有些許的影響,因為會大量動態生成代理類,性能損耗,是特別高的請求并發,比如萬級每秒,還是要深入的研究,不可隨意使用,但是基本平時開發的時候,還是可以使用的,畢竟性價比挺高的,我說的也是九牛一毛,大家繼續加油吧!
2、靜態注入
基于Net的IL語言層級進行注入,性能損耗可以忽略不計,Net使用最多的Aop框架PostSharp(好像收費了;)采用的即是這種方式。
大家可以參考這個博文:https://www.cnblogs.com/mushroom/p/3932698.html
?七、CODE
https://github.com/anjoy8/Blog.Core
https://gitee.com/laozhangIsPhi/Blog.Core
總結
以上是生活随笔為你收集整理的切面是异步还是同步操作‘_【 .NET Core 3.0 】框架之十 || AOP 切面思想的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 永磁无刷电机及其驱动技术_扫盲——直流无
- 下一篇: 大数据平台建设方案_工信部:全国范围内逐