项目

拦截器

ABP 提供了一个强大的拦截系统,允许你在不修改原始方法代码的情况下,在方法调用前后执行自定义逻辑。这是通过动态代理实现的,并在整个 ABP 框架中广泛用于实现横切关注点。ABP 的拦截功能基于 Castle DynamicProxy 库实现。

什么是动态代理/拦截?

拦截是一种技术,允许在不直接修改方法代码的情况下,在方法调用前后执行额外的逻辑。这是通过动态代理实现的,运行时生成包装原始类的代理类。

当调用代理对象的方法时:

  1. 调用被代理拦截
  2. 可以执行自定义行为(如日志记录、验证或授权)
  3. 调用原始方法
  4. 方法完成后可以执行额外的逻辑

这使得横切关注点(应用于应用程序多个部分的逻辑)能够以清晰、可重用的方式处理,无需代码重复。

与 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#486abpframework/abp#3180 中的讨论。

ABP 使用拦截器来实现 UOW、审计和授权等功能,这些功能依赖于动态代理类。对于控制器,优先使用中间件或 MVC/页面过滤器来实现横切关注点,而不是动态代理。

为避免为特定类型生成动态代理,使用静态类 DynamicProxyIgnoreTypes 并将类型的基类添加到列表中。任何列出的基类的子类也会被忽略。ABP 框架已经将一些基类添加到列表中(ComponentBase, ControllerBase, PageModel, ViewComponent);如果需要,你可以添加更多基类。

始终使用基于接口的代理而不是基于类的代理,以获得更好的性能。

另请参阅

在本文档中