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





5.00/5 (27投票s)
在本文中,我将向您展示如何创建拦截器来实现 AOP 技术。我将使用 ASP.NET Boilerplate (ABP) 作为基础应用程序框架,并使用 Castle Windsor 作为拦截库。
- 从 Github 仓库 获取源代码。
目录
引言
在本文中,我将向您展示如何创建拦截器来实现 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 的 Kernel 的 ComponentRegistered 事件是最恰当的方式。
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
	- 首次发布。
 


