1. 程式人生 > >國內開源社群鉅作AspectCore-Framework入門

國內開源社群鉅作AspectCore-Framework入門

前些天和張隊(善友),lemon(浩洋),斌哥(項斌)等MVP大咖一塊兒吃飯,大家聊到了lemon名下的AOP這個專案,我這小白聽得一臉懵逼,後面回來做了一下功課,查了下資料,在lemon的Github上把這個專案學習了一下,收穫頗豐,讓我這個沒有接觸過AOP的Coder歎為觀止,陷入了對lemon的深深崇拜,在這裡把學習的新的體會分享給大家.

什麼是AOP?

在軟體業,AOP為Aspect Oriented Programming的縮寫,意為:面向切面程式設計,通過預編譯方式和執行期動態代理實現程式功能的統一維護的一種技術。

有點深奧, 舉個栗子image

如果說之前做的一個系統專門給內部的服務提供介面的,因為是在內網中訪問,所以就沒有加上認證服務,現在這個系統要公開出來,同樣的那套介面要給外部系統服務了,那麼此時,就要進行認證,認證通過才能獲取介面的資料.

傳統的做法是,修改每一個介面.這樣就會造成程式碼改動過大,很恐怖.

 

這個時候AOP就可以登場了,我們可以在這一類服務的前面,加上一個一系列上一刀,在切出來的裂縫裡面插入認證方法.

image

 

然而,怎麼插入這個切面是關鍵.AOP 實現會採用一些常見方法:

  • 使用前處理器(如 C++ 中的前處理器)新增原始碼。
  • 使用後處理器在編譯後的二進位制程式碼上新增指令。
  • 使用特殊編譯器在編譯時新增程式碼。
  • 在執行時使用程式碼攔截器攔截執行並新增所需的程式碼。

但是常見還是後處理和程式碼攔截兩種方式

  • 後處理,或者叫 靜態織入

    指使用 AOP 框架提供的命令進行編譯,從而在編譯階段就可生成 AOP 代理類,因此也稱為編譯時增強或靜態織入。

    在dotnet 中一般在編譯時通過在MSBiuld執行自定義的Build Task來攔截編譯過程,在生成的程式集裡插入自己的IL。

    dotnet 框架代表: PostSharp

     

  • 程式碼攔截,或者叫 動態代理

    在執行時在記憶體中“臨時”生成 AOP 動態代理類,因此也被稱為執行時增強或動態代理。

    在dotnet 中一般在執行時通過Emit技術生成動態程式集和動態代理型別從而對目標方法進行攔截。

    dotnet 框架代表: Castle DynamicProxyAspectCore

引用https://github.com/dotnetcore/AspectCore-Framework/blob/master/docs/0.AOP%E7%AE%80%E5%8D%95%E4%BB%8B%E7%BB%8D.md

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的資料和返回結果.

image

說明介面是正常工作的,成功地把傳過去的Name原樣返回.

那麼攔截器有沒有生效呢?我看看CMD的輸出.

image

如果我們修改一下Name不等於Admin,預期應該是返回空,並且日誌打印出未授權,是不是這樣呢?

image

image

完美,與預期完全相同.

可以發現,這正是我們在攔截器中所作的工作,說明攔截器對該UserService生效了.

2.作用於特定的Service或者Method的全域性攔截器

如果我們不想對所有Servce或是Method都攔截,只攔截指定的Servce或者Method呢?

其實,我們是可以配置全域性攔截器的作用域的.

services.AddDynamicProxy(config =>
            {
                //支援萬用字元,只對IRole開頭的Servce有效
                config.Interceptors.AddTyped<AuthenticateInterceptor>(Predicates.ForService("IRole*"));
            });

我用以上方法配置為該過濾器只對IRole開頭的Servce有效,那麼,當我們讓問IUserServce時,該攔截器肯定是不會生效的,事實是不是這樣呢?

image

即使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,也是有結果返回會的.

image

說明此時配置器對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框架的小夥致敬!!

 

“解锁姿势”的图片搜索结果

 

歡迎訪問

https://github.com/dotnetcore/AspectCore-Framework/blob/master/docs/1.%E4%BD%BF%E7%94%A8%E6%8C%87%E5%8D%97.md

解鎖更多新姿勢!!!