ABP AOP 用例

介紹

在本文中,我將向您展現如何建立攔截器來實現AOP技術。我將使用ASP.NET Boilerplate(ABP)做爲基礎應用程序框架Castle Windsor做爲攔截庫。這裏描述的大多數技術對於使用獨立於ABP框架的Castle Windsor也是有效的。git

什麼是面向方面編程(AOP)和方法攔截?

維基百科:「 在計算中,面向方面的編程(AOP)是一種編程範式,旨在增長模塊性容許的分離橫切關注它經過添加額外的行爲,以現有的代碼(諮詢)這樣作。無需修改代碼而是分別指定哪一個代碼經過「切入點」規範進行修改github

在應用程序中,咱們可能會有一些重複/相似的代碼用於日誌記錄,受權,驗證,異常處理等等...數據庫

手動方式(無AOP)

示例代碼所有手動執行:編程

public class TaskAppService : ApplicationService
{
    private readonly IRepository<Task> _taskRepository;
    private readonly IPermissionChecker _permissionChecker;
    private readonly ILogger _logger;

    public TaskAppService(IRepository<Task> taskRepository, 
        IPermissionChecker permissionChecker, ILogger logger)
    {
        _taskRepository = taskRepository;
        _permissionChecker = permissionChecker;
        _logger = logger;
    }

    public void CreateTask(CreateTaskInput input)
    {
        _logger.Debug("Running CreateTask method: " + input.ToJsonString());

        try
        {
            if (input == null)
            {
                throw new ArgumentNullException("input");
            }

            if (!_permissionChecker.IsGranted("TaskCreationPermission"))
            {
                throw new Exception("No permission for this operation!");
            }

            _taskRepository.Insert(new Task(input.Title, input.Description, input.AssignedUserId));
        }
        catch (Exception ex)
        {
            _logger.Error(ex.Message, ex);
            throw;
        }

        _logger.Debug("CreateTask method is successfully completed!");
    }
}

CreateTask方法中,基本代碼_taskRepository.Insert(...)方法調用。全部其餘代碼重複代碼,並將與咱們其餘方法相同/類似TaskAppService在實際應用中,咱們將有不少應用服務須要相同的功能。另外,咱們可能有其餘相似的數據庫鏈接開關代碼,審覈日誌等等...框架

AOP方式

若是咱們使用AOP和截取技術,TaskAppService能夠用以下所示的相同功能來寫:異步

public class TaskAppService : ApplicationService
{
    private readonly IRepository<Task> _taskRepository;

    public TaskAppService(IRepository<Task> taskRepository)
    {
        _taskRepository = taskRepository;
    }

    [AbpAuthorize("TaskCreationPermission")]
    public void CreateTask(CreateTaskInput input)
    {
        _taskRepository.Insert(new Task(input.Title, input.Description, input.AssignedUserId));
    }
}

如今,它徹底是CreateTask方法惟一的異常處理驗證日誌記錄代碼被徹底刪除,由於它們與其餘方法類似,而且能夠以傳統方式集中。受權代碼被 AbpAuthorize更容易寫入和讀取的屬性所代替async

幸運的是,全部這些和更多的由ABP框架自動完成可是,您可能但願建立一些特定於您本身的應用程序需求的自定義攔截邏輯這就是爲何我建立了這篇文章。ide

關於示例項目

我從ABP 啓動模板(包括模塊零)建立了一個示例項目,並添加到Github倉庫post

建立攔截器

咱們先來看一個簡單的攔截器來測量方法的執行時間:this

 

using System.Diagnostics;
using Castle.Core.Logging;
using Castle.DynamicProxy;

namespace InterceptionDemo.Interceptors
{
    public class MeasureDurationInterceptor : IInterceptor
    {
        public ILogger Logger { get; set; }

        public MeasureDurationInterceptor()
        {
            Logger = NullLogger.Instance;
        }

        public void Intercept(IInvocation invocation)
        {
            //Before method execution
            var stopwatch = Stopwatch.StartNew();

            //Executing the actual method
            invocation.Proceed();

            //After method execution
            stopwatch.Stop();
            Logger.InfoFormat(
                "MeasureDurationInterceptor: {0} executed in {1} milliseconds.",
                invocation.MethodInvocationTarget.Name,
                stopwatch.Elapsed.TotalMilliseconds.ToString("0.000")
                );
        }
    }
}

攔截器是實現IInterceptor接口(Castle Windsor)的類。它定義了Intercept獲取IInvocation參數方法經過這個調用參數,咱們能夠調查執行方法,方法參數,返回值,方法聲明的類,彙編等等。Intercept調用註冊方法時調用方法(請參閱下面的註冊部分)。Proceed()方法執行實際截取的方法。咱們能夠在實際的方法執行以前以後編寫代碼,如本示例所示。

Interceptor類也能夠注入其依賴像其餘類。在這個例子中,咱們將屬性注入一個ILogger寫入日誌的方法執行時間。

註冊攔截器

在咱們建立一個攔截器以後,咱們能夠註冊所需的類。例如,咱們可能想要註冊MeasureDurationInterceptor全部應用程序服務類的全部方法由於全部應用程序服務類都IApplicationService在ABP框架中實現咱們能夠很容易地識別應用程序服務

有一些替代方法來註冊攔截器。可是,ABP處理ComponentRegisteredCastle Windsor事件最合適的方法是Kernel

public static class MeasureDurationInterceptorRegistrar
{
    public static void Initialize(IKernel kernel)
    {
        kernel.ComponentRegistered += Kernel_ComponentRegistered;
    }

    private static void Kernel_ComponentRegistered(string key, IHandler handler)
    {
        if (typeof (IApplicationService).IsAssignableFrom(handler.ComponentModel.Implementation))
        {
            handler.ComponentModel.Interceptors.Add
            (new InterceptorReference(typeof(MeasureDurationInterceptor)));
        }
    }
}

以這種方式,每當一個類註冊到依賴注入系統(IOC)時,咱們能夠處理事件,檢查這個類是不是咱們想攔截的類之一,若是是這樣,添加攔截器。

建立這樣的註冊碼後,咱們須要Initialize從別的地方調用該方法。最好在PreInitialize你的模塊中調用它(由於課程一般在IOC中註冊Initialize):

 

public class InterceptionDemoApplicationModule : AbpModule
{
    public override void PreInitialize()
    {
        MeasureDurationInterceptorRegistrar.Initialize(IocManager.IocContainer.Kernel);
    }

    //...
}

執行這些步驟後,我運行並登陸到應用程序。而後,我查看日誌文件並查看日誌:

INFO 2016-02-23 14:59:28,611 [63 ] .Interceptors.MeasureDurationInterceptor - 
GetCurrentLoginInformations executed in 4,939 milliseconds.

注意:GetCurrentLoginInformations是一個SessionAppService類的方法。你能夠在源代碼中檢查它,但這並不重要,由於咱們的攔截器不知道截取的方法的細節。

攔截異步方法

攔截異步方法與截取同步方​​法不一樣。例如,MeasureDurationInterceptor上面定義的異步方法不能正常工做。由於一個異步方法當即返回一個異步方法Task因此,咱們沒法測量什麼時候實際完成(實際上,GetCurrentLoginInformations上面的例子也是一個異步方法,4,939 ms是錯誤的值)。

咱們來改變MeasureDurationInterceptor以支持異步方法,而後解釋咱們如何實現它:

using System.Diagnostics;
using System.Reflection;
using System.Threading.Tasks;
using Castle.Core.Logging;
using Castle.DynamicProxy;

namespace InterceptionDemo.Interceptors
{
    public class MeasureDurationAsyncInterceptor : IInterceptor
    {
        public ILogger Logger { get; set; }

        public MeasureDurationAsyncInterceptor()
        {
            Logger = NullLogger.Instance;
        }

        public void Intercept(IInvocation invocation)
        {
            if (IsAsyncMethod(invocation.Method))
            {
                InterceptAsync(invocation);
            }
            else
            {
                InterceptSync(invocation);
            }
        }

        private void InterceptAsync(IInvocation invocation)
        {
            //Before method execution
            var stopwatch = Stopwatch.StartNew();

            //Calling the actual method, but execution has not been finished yet
            invocation.Proceed();

            //We should wait for finishing of the method execution
            ((Task) invocation.ReturnValue)
                .ContinueWith(task =>
                {
                    //After method execution
                    stopwatch.Stop();
                    Logger.InfoFormat(
                        "MeasureDurationAsyncInterceptor: {0} executed in {1} milliseconds.",
                        invocation.MethodInvocationTarget.Name,
                        stopwatch.Elapsed.TotalMilliseconds.ToString("0.000")
                        );
                });
        }

        private void InterceptSync(IInvocation invocation)
        {
            //Before method execution
            var stopwatch = Stopwatch.StartNew();

            //Executing the actual method
            invocation.Proceed();

            //After method execution
            stopwatch.Stop();
            Logger.InfoFormat(
                "MeasureDurationAsyncInterceptor: {0} executed in {1} milliseconds.",
                invocation.MethodInvocationTarget.Name,
                stopwatch.Elapsed.TotalMilliseconds.ToString("0.000")
                );
        }
        
        public static bool IsAsyncMethod(MethodInfo method)
        {
            return (
                method.ReturnType == typeof(Task) ||
                (method.ReturnType.IsGenericType && method.ReturnType.GetGenericTypeDefinition() == typeof(Task<>))
                );
        }
    }
}

因爲同步和異步執行邏輯徹底不一樣,我檢查了當前的方法是異步仍是同步(IsAsyncMethod是)。我把之前的代碼移到了InterceptSync方法,並引入了新的 InterceptAsync方法。我使用Task.ContinueWith(...)方法在任務完成後執行動做。ContinueWith即便攔截方法拋出異常,方法仍然有效

如今,我MeasureDurationAsyncInterceptor經過修改MeasureDurationInterceptorRegistrar上面定義來註冊爲應用程序服務的第二個攔截器

 

public static class MeasureDurationInterceptorRegistrar
{
    public static void Initialize(IKernel kernel)
    {
        kernel.ComponentRegistered += Kernel_ComponentRegistered;
    }

    private static void Kernel_ComponentRegistered(string key, IHandler handler)
    {
        if (typeof(IApplicationService).IsAssignableFrom(handler.ComponentModel.Implementation))
        {
            handler.ComponentModel.Interceptors.Add(new InterceptorReference(typeof(MeasureDurationInterceptor)));
            handler.ComponentModel.Interceptors.Add(new InterceptorReference(typeof(MeasureDurationAsyncInterceptor)));
        }
    }
}

若是咱們再次運行應用程序,咱們將會看到, MeasureDurationAsyncInterceptor測量的時間要長得多MeasureDurationInterceptor,由於它實際上等待直到方法徹底執行。

INFO  2016-03-01 10:29:07,592 [10   ] .Interceptors.MeasureDurationInterceptor - MeasureDurationInterceptor: GetCurrentLoginInformations executed in 4.964 milliseconds.
INFO  2016-03-01 10:29:07,693 [7    ] rceptors.MeasureDurationAsyncInterceptor - MeasureDurationAsyncInterceptor: GetCurrentLoginInformations executed in 104,994 milliseconds.

 

這樣,咱們能夠正確攔截異步方法來運行先後的代碼。可是,若是咱們的先後代碼涉及另外一個異步方法調用,事情會變得有點複雜。

首先,我找不到之前執行異步代碼的方法 invocation.Proceed()由於溫莎城堡本身不支持異步(其餘國際奧委會經理也不支持我所知)。因此,若是您須要在實際執行方法以前運行代碼,請同步執行。若是您找到方法,請分享您的解決方案做爲本文的評論。

方法執行後咱們能夠執行異步代碼。我改變了 InterceptAsync,以支持它:

using System.Diagnostics;
using System.Reflection;
using System.Threading.Tasks;
using Castle.Core.Logging;
using Castle.DynamicProxy;

namespace InterceptionDemo.Interceptors
{
    public class MeasureDurationWithPostAsyncActionInterceptor : IInterceptor
    {
        public ILogger Logger { get; set; }

        public MeasureDurationWithPostAsyncActionInterceptor()
        {
            Logger = NullLogger.Instance;
        }

        public void Intercept(IInvocation invocation)
        {
            if (IsAsyncMethod(invocation.Method))
            {
                InterceptAsync(invocation);
            }
            else
            {
                InterceptSync(invocation);
            }
        }

        private void InterceptAsync(IInvocation invocation)
        {
            //Before method execution
            var stopwatch = Stopwatch.StartNew();

            //Calling the actual method, but execution has not been finished yet
            invocation.Proceed();

            //Wait task execution and modify return value
            if (invocation.Method.ReturnType == typeof(Task))
            {
                invocation.ReturnValue = InternalAsyncHelper.AwaitTaskWithPostActionAndFinally(
                    (Task) invocation.ReturnValue,
                    async () => await TestActionAsync(invocation),
                    ex =>
                    {
                        LogExecutionTime(invocation, stopwatch);
                    });
            }
            else //Task<TResult>
            {
                invocation.ReturnValue = InternalAsyncHelper.CallAwaitTaskWithPostActionAndFinallyAndGetResult(
                    invocation.Method.ReturnType.GenericTypeArguments[0],
                    invocation.ReturnValue,
                    async () => await TestActionAsync(invocation),
                    ex =>
                    {
                        LogExecutionTime(invocation, stopwatch);
                    });
            }
        }

        private void InterceptSync(IInvocation invocation)
        {
            //Before method execution
            var stopwatch = Stopwatch.StartNew();

            //Executing the actual method
            invocation.Proceed();

            //After method execution
            LogExecutionTime(invocation, stopwatch);
        }

        public static bool IsAsyncMethod(MethodInfo method)
        {
            return (
                method.ReturnType == typeof(Task) ||
                (method.ReturnType.IsGenericType && method.ReturnType.GetGenericTypeDefinition() == typeof(Task<>))
                );
        }

        private async Task TestActionAsync(IInvocation invocation)
        {
            Logger.Info("Waiting after method execution for " + invocation.MethodInvocationTarget.Name);
            await Task.Delay(200); //Here, we can await another methods. This is just for test.
            Logger.Info("Waited after method execution for " + invocation.MethodInvocationTarget.Name);
        }

        private void LogExecutionTime(IInvocation invocation, Stopwatch stopwatch)
        {
            stopwatch.Stop();
            Logger.InfoFormat(
                "MeasureDurationWithPostAsyncActionInterceptor: {0} executed in {1} milliseconds.",
                invocation.MethodInvocationTarget.Name,
                stopwatch.Elapsed.TotalMilliseconds.ToString("0.000")
                );
        }
    }
}

若是咱們要在方法執行後執行一個異步方法,咱們應該用第二個方法的返回值替換返回值。我創造了一個神奇的InternalAsyncHelper課程來完成它。InternalAsyncHelper以下所示:

 

internal static class InternalAsyncHelper
{
    public static async Task AwaitTaskWithPostActionAndFinally(Task actualReturnValue, Func<Task> postAction, Action<Exception> finalAction)
    {
        Exception exception = null;

        try
        {
            await actualReturnValue;
            await postAction();
        }
        catch (Exception ex)
        {
            exception = ex;
            throw;
        }
        finally
        {
            finalAction(exception);
        }
    }

    public static async Task<T> AwaitTaskWithPostActionAndFinallyAndGetResult<T>(Task<T> actualReturnValue, Func<Task> postAction, Action<Exception> finalAction)
    {
        Exception exception = null;

        try
        {
            var result = await actualReturnValue;
            await postAction();
            return result;
        }
        catch (Exception ex)
        {
            exception = ex;
            throw;
        }
        finally
        {
            finalAction(exception);
        }
    }

    public static object CallAwaitTaskWithPostActionAndFinallyAndGetResult(Type taskReturnType, object actualReturnValue, Func<Task> action, Action<Exception> finalAction)
    {
        return typeof (InternalAsyncHelper)
            .GetMethod("AwaitTaskWithPostActionAndFinallyAndGetResult", BindingFlags.Public | BindingFlags.Static)
            .MakeGenericMethod(taskReturnType)
            .Invoke(null, new object[] { actualReturnValue, action, finalAction });
    }
}

更多

我會經過添加一些用例來改進這篇文章:

  • 定義屬性來控制攔截邏輯
  • 使用方法參數
  • 操縱返回值
  • ...

雖然您能夠從MeasureDurationInterceptor示例開始,但請遵循本文的更新以獲取具體示例。

相關文章
相關標籤/搜索