前些天和張隊(善友),lemon(浩洋),斌哥(項斌)等MVP大咖一塊兒吃飯,大家聊到了lemon名下的AOP這個項目,我這小白聽得一臉懵逼,后面回來做了一下功課,查了下資料,在lemon的Github上把這個項目學習了一下,收獲頗豐,讓我這個沒有接觸過AOP的Coder嘆為觀止,陷入了對lemon的深深崇拜,在這里把學習的新的體會分享給大家.
什么是AOP?
在軟件業,AOP為Aspect Oriented Programming的縮寫,意為:面向切面編程,通過預編譯方式和運行期動態代理實現程序功能的統一維護的一種技術。
如果說之前做的一個系統專門給內部的服務提供接口的,因為是在內網中訪問,所以就沒有加上認證服務,現在這個系統要公開出來,同樣的那套接口要給外部系統服務了,那么此時,就要進行認證,認證通過才能獲取接口的數據.
傳統的做法是,修改每一個接口.這樣就會造成代碼改動過大,很恐怖.
這個時候AOP就可以登場了,我們可以在這一類服務的前面,加上一個一系列上一刀,在切出來的裂縫里面插入認證方法.
然而,怎么插入這個切面是關鍵.AOP 實現會采用一些常見方法:
- 使用預處理器(如 C++ 中的預處理器)添加源代碼。
- 使用后處理器在編譯后的二進制代碼上添加指令。
- 使用特殊編譯器在編譯時添加代碼。
- 在運行時使用代碼攔截器攔截執行並添加所需的代碼。
但是常見還是后處理和代碼攔截兩種方式
-
后處理,或者叫 靜態織入
指使用 AOP 框架提供的命令進行編譯,從而在編譯階段就可生成 AOP 代理類,因此也稱為編譯時增強或靜態織入。
在dotnet 中一般在編譯時通過在MSBiuld執行自定義的Build Task來攔截編譯過程,在生成的程序集里插入自己的IL。
dotnet 框架代表: PostSharp
-
代碼攔截,或者叫 動態代理
在運行時在內存中“臨時”生成 AOP 動態代理類,因此也被稱為運行時增強或動態代理。
在dotnet 中一般在運行時通過Emit技術生成動態程序集和動態代理類型從而對目標方法進行攔截。
dotnet 框架代表: Castle DynamicProxy 和 AspectCore
AspectCore-Framework的代碼攔截
我這里直接應用AOP Demo中的一段代碼來說說這個攔截.
public class CustomInterceptor : AbstractInterceptor { public async override Task Invoke(AspectContext context, AspectDelegate next) { try { Console.WriteLine("Before service call"); await next(context); } catch (Exception) { Console.WriteLine("Service threw an exception!"); throw; } finally { Console.WriteLine("After service call"); } } }
代碼中,其實執行到 await next(context)的時候,才會真正去調用那個被攔截的方法.
這樣,我們就可以靈活地在代碼調用前,調用后做我們想做的事情了.甚至可以把代碼包在一個try…catch...中來捕獲異常.
開始AspcetCore的表演
新建一個web應用程序后,從 Nuget 安裝 AspectCore.Extensions.DependencyInjection
包.
PM> Install-Package AspectCore.Extensions.DependencyInjection
然后.我們就可以來定義我們的攔截器了,我定義了一個這樣的攔截器.
public override async Task Invoke(AspectContext context, AspectDelegate next) { var logger = context.ServiceProvider.GetService<ILogger<AuthenticateInterceptor>>(); try { var apiRequest = (ApiRequest) context.Parameters.FirstOrDefault(); if (apiRequest == null || apiRequest.Name != "admin") { logger.LogWarning("unauthorized"); return; } logger.LogInformation(apiRequest.Message); await next(context); } catch (Exception e) { logger?.LogWarning("Service threw an exception!"); throw; } finally { logger.LogInformation("Finished service call"); } }
當ApiRequest為空或者Name不等於admin的時候之家返回並記錄未授權.
否則,調用該調用的方法並記錄ApiRequest中的Message.
然后,我定義一個UserService.
using System; namespace AspceptCoreDemo { public interface IUserService { String GetUserName(ApiRequest req); } public class UserService : IUserService { public string GetUserName(ApiRequest req) { if (req == null) return null; Console.WriteLine($"The User Name is {req.Name}"); return req.Name; } } }
在Controler中調用注入UserServce並調用該Service.
using Microsoft.AspNetCore.Mvc; namespace AspceptCoreDemo.Controllers { [Route("api/[controller]")] [ApiController] public class ValuesController : ControllerBase { private readonly IUserService _userService; public ValuesController(IUserService userService) { _userService = userService; } [HttpPost] public ActionResult<string> Post(ApiRequest req) { return _userService.GetUserName(req); } } }
注冊IUserservice並在ConfigureServices
中配置創建代理類型的容器:
public IServiceProvider ConfigureServices(IServiceCollection services) { services.AddSingleton<IUserService, UserService>(); services.AddMvc(); services.AddDynamicProxy(config => { config.Interceptors.AddTyped<AuthenticateInterceptor>(); }); return services.BuildAspectInjectorProvider(); }
需要注意的是紅色背景處,默認的ConfigureService返回類型是空的,我們要修改成為返回類型是IServiceProvider.
1.全局攔截
我們在上面的ConfigureService配置的AuthenticateInterceptor默認情況下是全局的,即這里的IUserService它會攔截,當然如果新增了一個IRoleServce它也是會攔截的.
我把程序運行起來用PostMan訪問Api進行測試.下圖是Post的數據和返回結果.
說明接口是正常工作的,成功地把傳過去的Name原樣返回.
那么攔截器有沒有生效呢?我看看CMD的輸出.
如果我們修改一下Name不等於Admin,預期應該是返回空,並且日志打印出未授權,是不是這樣呢?
完美,與預期完全相同.
可以發現,這正是我們在攔截器中所作的工作,說明攔截器對該UserService生效了.
2.作用於特定的Service或者Method的全局攔截器
如果我們不想對所有Servce或是Method都攔截,只攔截指定的Servce或者Method呢?
其實,我們是可以配置全局攔截器的作用域的.
services.AddDynamicProxy(config => { //支持通配符,只對IRole開頭的Servce有效 config.Interceptors.AddTyped<AuthenticateInterceptor>(Predicates.ForService("IRole*")); });
我用以上方法配置為該過濾器只對IRole開頭的Servce有效,那么,當我們讓問IUserServce時,該攔截器肯定是不會生效的,事實是不是這樣呢?
即使Name不是admin,結果也返回了,說明確實是沒有生效的.
還可以用以下方法指定過濾器作用於的Method.
//支持通配符,只對Name結尾的方法有效 config.Interceptors.AddTyped<AuthenticateInterceptor>(Predicates.ForMethod("*Name"));
3.全局過濾器忽略配置
忽略配置有兩種方法
一種是為Service或者Method打上[NonAspect] 標簽,那個過濾器就不會對該處生效了.
public interface IUserService { [NonAspect] String GetUserName(ApiRequest req); }
此時,即使Name不等於Admin,也是有結果返回會的.
說明此時配置器對GetUserName方法確實沒有生效.
另外一種是 全局忽略配置,亦支持通配符:
services.AddDynamicProxy(config => { //App1命名空間下的Service不會被代理 config.NonAspectPredicates.AddNamespace("App1"); //最后一級為App1的命名空間下的Service不會被代理 config.NonAspectPredicates.AddNamespace("*.App1"); //ICustomService接口不會被代理 config.NonAspectPredicates.AddService("ICustomService"); //后綴為Service的接口和類不會被代理 config.NonAspectPredicates.AddService("*Service"); //命名為Query的方法不會被代理 config.NonAspectPredicates.AddMethod("Query"); //后綴為Query的方法不會被代理 config.NonAspectPredicates.AddMethod("*Query"); });
4.攔截器中的依賴注入
對攔截器中有get和set權限的屬性標記[AspectCore.Injector.FromContainerAttribute]
特性,即可自動注入該屬性.
[NonAspect] public class AuthenticateInterceptor : AbstractInterceptor { [FromContainer]public ILogger<AuthenticateInterceptor> Logger { get; set
; } public override async Task Invoke(AspectContext context, AspectDelegate next) { try { var apiRequest = (ApiRequest) context.Parameters.FirstOrDefault(); if (apiRequest == null || apiRequest.Name != "admin") { Logger.LogWarning("unauthorized"); return; } Logger.LogInformation(apiRequest.Message); await next(context); } catch (Exception e) { Logger?.LogWarning("Service threw an exception!"); throw; } finally { Logger.LogInformation("Finished service call"); } } }
也可以攔截器上下文AspectContext
可以獲取當前Scoped的ServiceProvider
利用該ServiceProvider來對依賴項賦值.
[NonAspect] public class AuthenticateInterceptor : AbstractInterceptor { public override async Task Invoke(AspectContext context, AspectDelegate next) {var logger = context.ServiceProvider.GetService<ILogger<AuthenticateInterceptor>>
(); try { var apiRequest = (ApiRequest) context.Parameters.FirstOrDefault(); if (apiRequest == null || apiRequest.Name != "admin") { logger.LogWarning("unauthorized"); return; } logger.LogInformation(apiRequest.Message); await next(context); } catch (Exception e) { logger?.LogWarning("Service threw an exception!"); throw; } finally { logger.LogInformation("Finished service call"); } } }
本文只是對AsceptCore最簡單的一套流程end to end 地進行了敘述,這還只是AsceptCore的冰山一角.在此向開發處如此牛逼AOP框架的小伙致敬!!
歡迎訪問
解鎖更多新姿勢!!!
本博客Demo地址
https://github.com/liuzhenyulive/AspceptCoreDemo
AsceptCore地址
https://github.com/dotnetcore/AspectCore-Framework