拦截器
ABP 提供了一个强大的拦截系统,允许你在不修改原始方法代码的情况下,在方法调用前后执行自定义逻辑。这是通过动态代理实现的,并在整个 ABP 框架中广泛用于实现横切关注点。ABP 的拦截功能基于 Castle DynamicProxy 库实现。
什么是动态代理/拦截?
拦截是一种技术,允许在不直接修改方法代码的情况下,在方法调用前后执行额外的逻辑。这是通过动态代理实现的,运行时生成包装原始类的代理类。
当调用代理对象的方法时:
- 调用被代理拦截
- 可以执行自定义行为(如日志记录、验证或授权)
- 调用原始方法
- 方法完成后可以执行额外的逻辑
这使得横切关注点(应用于应用程序多个部分的逻辑)能够以清晰、可重用的方式处理,无需代码重复。
与 MVC 操作/页面过滤器的异同
如果你熟悉 ASP.NET Core MVC,可能使用过操作过滤器或页面过滤器。拦截器在概念上类似,但有一些关键区别:
相同点
- 两者都允许在方法执行前后执行代码
- 两者都用于实现横切关注点,如验证、日志记录、缓存或异常处理
- 两者都支持异步操作
不同点
- 范围:过滤器与 MVC 的请求管道绑定,而拦截器可以应用于应用程序中的任何类或服务
- 配置:过滤器通过 MVC 中的属性或中间件配置,而 ABP 中的拦截器通过依赖注入和动态代理应用
- 目标:拦截器可以针对应用服务、领域服务、存储库以及几乎所有从 IoC 容器解析的服务——而不仅仅是 Web 控制器
ABP 如何使用拦截器
ABP 框架广泛利用拦截来提供内置功能,无需样板代码。以下是一些关键示例:
工作单元(UOW)
在进入或退出应用服务方法时自动开始和提交/回滚数据库事务。这确保了数据一致性,无需手动管理事务。
输入验证
在执行服务逻辑之前,输入 DTO 会根据数据注解属性和自定义验证规则自动验证,为所有服务提供一致的验证行为。
授权
在允许执行应用服务方法之前检查用户权限,确保安全策略一致执行。
功能 和 全局功能 检查
在执行服务逻辑之前检查功能是否启用,允许你为租户或应用程序有条件地启用或限制功能。
审计
自动记录谁执行了操作、何时发生、使用了哪些参数以及涉及哪些数据,提供全面的审计跟踪。
构建自定义拦截器
你可以在 ABP 中创建自定义拦截器来实现自己的横切关注点。
创建拦截器
创建一个继承自 AbpInterceptor 的类:
using System.Threading.Tasks;
using Volo.Abp.Aspects;
using Volo.Abp.DependencyInjection;
using Volo.Abp.DynamicProxy;
public class ExecutionTimeLogInterceptor : AbpInterceptor, ITransientDependency
{
private readonly ILogger<ExecutionTimeLogInterceptor> _logger;
public ExecutionTimeLogInterceptor(ILogger<ExecutionTimeLogInterceptor> logger)
{
_logger = logger;
}
public override async Task InterceptAsync(IAbpMethodInvocation invocation)
{
var sw = Stopwatch.StartNew();
_logger.LogInformation($"执行 {invocation.TargetObject.GetType().Name}.{invocation.Method.Name}");
// 继续执行实际的目标方法
await invocation.ProceedAsync();
sw.Stop();
_logger.LogInformation($"执行 {invocation.TargetObject.GetType().Name}.{invocation.Method.Name} 完成,耗时 {sw.ElapsedMilliseconds} 毫秒");
}
}
注册拦截器
创建一个包含 RegisterIfNeeded 方法的静态类,并在模块的 PreConfigureServices 方法中注册拦截器。
ShouldIntercept 方法用于确定是否为给定类型注册拦截器。你可以添加一个 IExecutionTimeLogEnabled 接口,并在要拦截的类中实现它。
DynamicProxyIgnoreTypes是一个静态类,包含应被拦截器忽略的类型。更多信息请参见性能考虑。
// 定义一个接口来标记应被拦截的类
public interface IExecutionTimeLogEnabled
{
}
using System.Threading.Tasks;
using Volo.Abp.DependencyInjection;
// 一个简单的服务,添加到 DI 容器中,由于实现了 `IExecutionTimeLogEnabled` 接口,将被拦截
public class SampleExecutionTimeService : IExecutionTimeLogEnabled, ITransientDependency
{
public virtual async Task DoWorkAsync()
{
// 模拟长时间运行的任务以测试拦截器
await Task.Delay(1000);
}
}
using System;
using Volo.Abp.DependencyInjection;
using Volo.Abp.DynamicProxy;
public static class ExecutionTimeLogInterceptorRegistrar
{
public static void RegisterIfNeeded(IOnServiceRegistredContext context)
{
if (ShouldIntercept(context.ImplementationType))
{
context.Interceptors.TryAdd<ExecutionTimeLogInterceptor>();
}
}
private static bool ShouldIntercept(Type type)
{
return !DynamicProxyIgnoreTypes.Contains(type) && typeof(IExecutionTimeLogEnabled).IsAssignableFrom(type);
}
}
public override void PreConfigureServices(ServiceConfigurationContext context)
{
context.Services.OnRegistered(ExecutionTimeLogInterceptorRegistrar.RegisterIfNeeded);
}
限制和重要说明
始终使用异步方法
为了最佳性能和可靠性,将服务方法实现为异步方法,以避免异步包装同步,这可能导致意外问题。更多信息,请参见是否应为异步方法暴露同步包装器?
虚方法要求
对于基于类的代理,方法需要标记为 virtual,以便它们可以被代理重写。否则,拦截不会发生。
public class MyService : IExecutionTimeLogEnabled, ITransientDependency
{
// 此方法无法被拦截(非 virtual)
public void CannotBeIntercepted()
{
}
// 此方法可以被拦截(virtual)
public virtual void CanBeIntercepted()
{
}
}
此限制不适用于基于接口的代理。如果你的服务实现了一个接口并通过该接口注入,则所有方法都可以被拦截,无论是否使用
virtual关键字。
依赖注入范围
拦截器仅在从依赖注入容器解析服务时有效。使用 new 直接实例化会绕过拦截:
// 这不会被拦截
var service = new MyService();
service.CannotBeIntercepted();
// 这会被拦截(如果 MyService 已注册到 DI)
var service = serviceProvider.GetService<MyService>();
service.CanBeIntercepted();
性能考虑
拦截器通常是高效的,但每个拦截器都会增加方法调用开销。在热路径上保持拦截器数量最少。
Castle DynamicProxy 可能对某些组件(尤其是 ASP.NET Core MVC 控制器)的性能产生负面影响。参见 castleproject/Core#486 和 abpframework/abp#3180 中的讨论。
ABP 使用拦截器来实现 UOW、审计和授权等功能,这些功能依赖于动态代理类。对于控制器,优先使用中间件或 MVC/页面过滤器来实现横切关注点,而不是动态代理。
为避免为特定类型生成动态代理,使用静态类 DynamicProxyIgnoreTypes 并将类型的基类添加到列表中。任何列出的基类的子类也会被忽略。ABP 框架已经将一些基类添加到列表中(ComponentBase, ControllerBase, PageModel, ViewComponent);如果需要,你可以添加更多基类。
始终使用基于接口的代理而不是基于类的代理,以获得更好的性能。
抠丁客


