在 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
- 首次发布。