65.9K
CodeProject 正在变化。 阅读更多。
Home

在 Castle Windsor 和 ABP Framework 中使用拦截器进行面向方面编程

starIconstarIconstarIconstarIconstarIcon

5.00/5 (27投票s)

2016年2月23日

CPOL

5分钟阅读

viewsIcon

81063

downloadIcon

343

在本文中,我将向您展示如何创建拦截器来实现 AOP 技术。我将使用 ASP.NET Boilerplate (ABP) 作为基础应用程序框架,并使用 Castle Windsor 作为拦截库。

目录

引言

在本文中,我将向您展示如何创建拦截器来实现 AOP 技术。我将使用 ASP.NET Boilerplate (ABP) 作为基础应用程序框架,并使用 Castle Windsor 作为拦截库。这里描述的大多数技术也适用于将 Castle Windsor 独立于 ABP 框架使用。

什么是面向切面编程 (AOP) 和方法拦截?

维基百科:“在计算中,面向切面编程 (AOP) 是一种编程范式,旨在通过允许横切关注点的分离来提高模块化。它通过在不修改代码本身的情况下,向现有代码添加附加行为(建议),而是通过“切点”规范单独指定哪些代码被修改”。

在应用程序中,我们可能会有一些重复/相似的代码用于日志记录、授权、验证、异常处理等...

手动方式(无 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 属性替换,该属性更简单易写易读。

幸运的是,所有这些以及更多内容都由 ABP 框架自动完成。但是,您可能希望创建一些特定于您自己应用程序需求的自定义拦截逻辑。这就是我创建本文的原因。

关于示例项目

我从 ABP 启动模板(包括 Module Zero)创建了一个示例项目,并将其添加到 Github 仓库

创建拦截器

让我们从一个简单的拦截器开始,该拦截器测量方法的执行持续时间。

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。由于 ABP 框架中的所有应用程序服务类都实现了 IApplicationService,因此我们可以轻松识别它们。

注册拦截器有一些替代方法。但是,在 ABP 中处理 Castle Windsor 的 KernelComponentRegistered 事件是最恰当的方式。

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 事件中调用它(因为类通常在 Initialize 步骤中注册到 IOC)。

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 类的 a 方法。您可以在源代码中检查它,但这并不重要,因为我们的拦截器不知道被拦截方法的细节。

拦截异步方法

拦截异步方法与拦截同步方法不同。例如,上面定义的 MeasureDurationInterceptor 对异步方法不起作用。因为异步方法会立即返回一个 Task,并且它是异步执行的。所以,我们无法测量它何时实际完成(实际上,上面 GetCurrentLoginInformations 的示例也是一个异步方法,4,939 毫秒是一个错误的值)。

让我们修改 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 方法即使在被拦截的方法抛出异常时也能工作。

现在,我通过修改上面定义的 MeasureDurationInterceptorRegistrar,将 MeasureDurationAsyncInterceptor 注册为应用程序服务的第二个拦截器。

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() 之前执行异步代码的方法。因为 Castle Windsor 不支持异步(据我所知,其他 IOC 管理器也不支持)。所以,如果您需要在实际方法执行之前运行代码,请同步执行。如果您找到了方法,请在文章的评论中分享您的解决方案。

我们可以在方法执行后执行异步代码。我修改了 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 });
    }
}

源代码

您可以在此处获取最新的源代码 https://github.com/aspnetboilerplate/aspnetboilerplate-samples/tree/master/InterceptionDemo

文章历史

  • 2018-02-22
    • 将源代码升级到 ABP v3.4。
  • 2017-06-28
    • 将源代码升级到 ABP v2.1.3。
    • 更新了文章中的链接。
  • 2016-07-20
    • 将源代码升级到 ABP v0.10。
    • 根据更改更新了文章。
  • 2016-03-01
    • 添加了异步方法拦截示例。
  • 2016-02-23
    • 首次发布。
© . All rights reserved.