AOP 实战:面向切面编程从理论到落地

日志、事务、缓存、权限、重试、性能监控------这些代码散落在每个业务方法中,和核心业务逻辑纠缠在一起,改一个日志格式要动 100 个文件。AOP(Aspect-Oriented Programming,面向切面编程)就是解决这个问题的武器。本文从代理模式讲起,覆盖 .NET 中三种主流 AOP 方案(RealProxy、DispatchProxy、Castle DynamicProxy),结合 PMS 项目实现事务、缓存、日志、重试四个切面,最后讲清楚什么时候该用 AOP、什么时候不该用。


目录

  1. [什么是 AOP:从"重复代码"说起](#什么是 AOP:从"重复代码"说起)
  2. [AOP 核心概念全解析](#AOP 核心概念全解析)
  3. [代理模式:AOP 的底层基石](#代理模式:AOP 的底层基石)
  4. 方案一:RealProxy(经典透明代理)
  5. [方案二:DispatchProxy(.NET Core 原生)](#方案二:DispatchProxy(.NET Core 原生))
  6. [方案三:Castle DynamicProxy(最成熟)](#方案三:Castle DynamicProxy(最成熟))
  7. 实战切面:事务管理
  8. 实战切面:缓存拦截
  9. 实战切面:日志与性能监控
  10. 实战切面:自动重试
  11. [AOP 与 DI 容器集成](#AOP 与 DI 容器集成)
  12. [Source Generator 编译时 AOP](#Source Generator 编译时 AOP)
  13. [AOP 的边界:什么时候不该用](#AOP 的边界:什么时候不该用)
  14. Checklist

1. 什么是 AOP:从"重复代码"说起

1.1 一个典型的业务方法

看看 PMS 中备件入库的代码:

csharp 复制代码
public class SparePartService
{
    private readonly IRepository<SparePart> _repository;
    private readonly ICacheService _cache;
    private readonly ILogger<SparePartService> _logger;

    public SparePartService(
        IRepository<SparePart> repository,
        ICacheService cache,
        ILogger<SparePartService> logger)
    {
        _repository = repository;
        _cache = cache;
        _logger = logger;
    }

    public async Task<Result> StockInAsync(SparePartDto dto, CancellationToken ct = default)
    {
        var sw = Stopwatch.StartNew();
        _logger.LogInformation("开始备件入库: PartNo={PartNo}, Qty={Qty}", dto.PartNo, dto.Quantity);

        try
        {
            // 缓存检查
            var cacheKey = $"sparepart:{dto.PartNo}";
            var cached = await _cache.GetAsync<SparePart>(cacheKey, ct);
            if (cached != null)
            {
                _logger.LogInformation("命中缓存: {CacheKey}", cacheKey);
            }

            // 业务逻辑
            var part = cached ?? new SparePart { PartNo = dto.PartNo };
            part.StockQuantity += dto.Quantity;
            part.UpdatedAt = DateTime.UtcNow;

            if (part.Id == 0)
                await _repository.AddAsync(part, ct);
            else
                _repository.Update(part);

            await _repository.UnitOfWork.SaveChangesAsync(ct);

            // 更新缓存
            await _cache.SetAsync(cacheKey, part, TimeSpan.FromMinutes(10), ct);

            sw.Stop();
            _logger.LogInformation(
                "备件入库成功: PartNo={PartNo}, 耗时 {ElapsedMs}ms",
                dto.PartNo, sw.ElapsedMilliseconds);

            return Result.Success(part);
        }
        catch (Exception ex)
        {
            sw.Stop();
            _logger.LogError(ex,
                "备件入库失败: PartNo={PartNo}, 耗时 {ElapsedMs}ms",
                dto.PartNo, sw.ElapsedMilliseconds);
            throw;
        }
    }
}

这个方法里,真正的业务逻辑只有 5 行(查询、加库存、保存),剩下的 30 行全是:

  • 日志记录
  • 缓存检查和更新
  • 性能计时
  • 异常处理

这些"横切关注点(Cross-Cutting Concerns)"在每个 Service 方法中重复出现。

1.2 AOP 的解决方案

AOP 的做法是:把横切逻辑抽离成独立的"切面",在运行时(或编译时)自动织入到目标方法周围

csharp 复制代码
// 业务方法------只关注核心逻辑
public class SparePartService : ISparePartService
{
    [Transactional]
    [CacheInvalidation("sparepart:{dto.PartNo}")]
    [LogPerformance]
    public async Task<Result> StockInAsync(SparePartDto dto, CancellationToken ct = default)
    {
        var part = await _repository.GetByPartNoAsync(dto.PartNo, ct)
                    ?? new SparePart { PartNo = dto.PartNo };
        part.StockQuantity += dto.Quantity;
        part.UpdatedAt = DateTime.UtcNow;

        if (part.Id == 0)
            await _repository.AddAsync(part, ct);
        else
            _repository.Update(part);

        return Result.Success(part);
    }
}

日志、事务、缓存、性能监控全部由切面自动处理,业务代码干干净净。

💬 互动一下:你的项目中有多少"基础设施代码"散落在业务方法里?如果抽离出来,业务代码能瘦多少?我见过最夸张的项目,一个方法 200 行,其中 180 行在打日志、处理缓存、做权限校验。


2. AOP 核心概念全解析

2.1 术语表

术语 英文 含义 PMS 示例
切面 Aspect 横切逻辑的模块化(一个类) TransactionAspect
连接点 Join Point 程序执行中的某个点(方法调用、异常等) StockInAsync 方法的执行
切点 Pointcut 匹配连接点的表达式/条件 "所有标记了 [Transactional] 的方法"
通知/增强 Advice 切面在连接点执行的动作 方法前开启事务,方法后提交
引入 Introduction 给现有类添加新方法或接口 动态实现 INotifyPropertyChanged
目标对象 Target 被切面增强的原始对象 SparePartService 实例
代理 Proxy AOP 框架创建的包装对象 实现了 ISparePartService 的透明代理
织入 Weaving 将切面应用到目标对象的过程 运行时通过动态代理创建代理类

2.2 五种通知类型

复制代码
方法调用
  │
  ├── [Before] 前置通知:方法执行前(如:权限校验、参数验证)
  │
  ├── [Around] 环绕通知:包裹方法(如:事务、缓存、性能计时)
  │     │
  │     ├── 调用目标方法
  │     │
  │     └── 方法返回后
  │
  ├── [After] 后置通知:方法结束后(无论成功失败,如:日志记录)
  │
  ├── [AfterReturning] 返回通知:方法成功返回后(如:缓存更新)
  │
  └── [AfterThrowing] 异常通知:方法抛异常后(如:异常日志、事务回滚)

在 .NET 中,大多数 AOP 框架主要实现 BeforeAroundAfter 三种。

2.3 织入时机

织入时机 原理 优点 缺点 .NET 代表
编译时 编译时修改 IL,注入切面代码 无运行时开销,可调试 需要特殊编译器 PostSharp、Source Generator
运行时(动态代理) 运行时生成代理类 灵活、无侵入 有性能开销,只拦截虚方法/接口方法 Castle DynamicProxy、DispatchProxy
加载时 程序集加载时修改 IL 透明 复杂、启动慢 .NET 中少见

3. 代理模式:AOP 的底层基石

AOP 的本质就是代理模式 + 反射调用。理解代理模式是理解一切 AOP 框架的前提。

3.1 静态代理

csharp 复制代码
// 接口
public interface ISparePartService
{
    Task<Result> StockInAsync(SparePartDto dto, CancellationToken ct = default);
}

// 真实对象
public class SparePartService : ISparePartService
{
    public async Task<Result> StockInAsync(SparePartDto dto, CancellationToken ct = default)
    {
        // 核心业务逻辑
        await Task.Delay(100);
        return Result.Success();
    }
}

// 静态代理:手写一个包装类
public class SparePartServiceProxy : ISparePartService
{
    private readonly SparePartService _target;
    private readonly ILogger _logger;

    public SparePartServiceProxy(SparePartService target, ILogger logger)
    {
        _target = target;
        _logger = logger;
    }

    public async Task<Result> StockInAsync(SparePartDto dto, CancellationToken ct = default)
    {
        var sw = Stopwatch.StartNew();
        _logger.LogInformation("开始执行 StockInAsync");

        try
        {
            var result = await _target.StockInAsync(dto, ct);
            sw.Stop();
            _logger.LogInformation("StockInAsync 完成,耗时 {ElapsedMs}ms",
                sw.ElapsedMilliseconds);
            return result;
        }
        catch (Exception ex)
        {
            sw.Stop();
            _logger.LogError(ex, "StockInAsync 失败,耗时 {ElapsedMs}ms",
                sw.ElapsedMilliseconds);
            throw;
        }
    }
}

静态代理的问题很明显------每个类、每个方法都要手写代理代码,100 个 Service 就要写 100 个 Proxy。这就是动态代理要解决的问题。

3.2 动态代理的核心思路

复制代码
运行时:
  1. 读取目标类型的接口(ISparePartService)
  2. 使用 Reflection.Emit / DispatchProxy 动态生成一个实现了该接口的类
  3. 在生成的类中,每个方法都调用 InvocationHandler(拦截器)
  4. InvocationHandler 决定是否调用目标方法、何时调用、前后加什么逻辑
  5. 返回动态生成的代理类实例

客户端拿到的是代理类,它和真实类实现了相同的接口,调用方完全无感知。


4. 方案一:RealProxy(经典透明代理)

RealProxy 是 .NET Framework 时代的经典 AOP 方案,基于 .NET Remoting 的透明代理机制。在 .NET Core 中不直接支持,但有一个 NuGet 包提供了移植版本。

4.1 基本实现

csharp 复制代码
// 需要安装:System.Reflection.DispatchProxy(RealProxy 在 Core 中不可用,
// 但概念类似,下面用 DispatchProxy 演示更通用的方案)

// .NET Framework 的经典写法
public class LoggingProxy<T> : RealProxy where T : class
{
    private readonly T _target;
    private readonly ILogger _logger;

    public LoggingProxy(T target, ILogger logger) : base(typeof(T))
    {
        _target = target;
        _logger = logger;
    }

    public override IMessage Invoke(IMessage msg)
    {
        var methodCall = (IMethodCallMessage)msg;
        var method = (MethodInfo)methodCall.MethodBase;
        var args = methodCall.Args;

        var sw = Stopwatch.StartNew();
        _logger.LogInformation("调用 {MethodName}", method.Name);

        try
        {
            var result = method.Invoke(_target, args);
            sw.Stop();
            _logger.LogInformation("{MethodName} 完成,{ElapsedMs}ms",
                method.Name, sw.ElapsedMilliseconds);
            return new ReturnMessage(result, args, args.Length,
                methodCall.LogicalCallContext, methodCall);
        }
        catch (Exception ex)
        {
            sw.Stop();
            _logger.LogError(ex, "{MethodName} 失败", method.Name);
            return new ReturnMessage(ex, methodCall);
        }
    }
}

4.2 现状

  • RealProxy 在 .NET Core/.NET 5+ 中不可用(依赖 Remoting,已被移除)
  • 微软的官方替代方案是 DispatchProxy
  • 新项目不需要考虑 RealProxy,这里讲它只是为了理解 AOP 的演进脉络

5. 方案二:DispatchProxy(.NET Core 原生)

DispatchProxy 是 .NET Core 原生提供的动态代理方案,不需要任何第三方依赖。

5.1 基本用法

csharp 复制代码
// 自定义拦截器
public class LoggingDispatchProxy<T> : DispatchProxy
{
    private T? _target;
    private ILogger? _logger;

    public void Initialize(T target, ILogger logger)
    {
        _target = target;
        _logger = logger;
    }

    protected override object? Invoke(
        MethodInfo? targetMethod,
        object?[]? args)
    {
        var sw = Stopwatch.StartNew();
        _logger?.LogInformation("开始执行 {MethodName}", targetMethod?.Name);

        try
        {
            // 调用目标方法
            var result = targetMethod?.Invoke(_target, args);

            // 处理异步方法
            if (result is Task task)
            {
                return AwaitTask(task, targetMethod!, sw);
            }

            sw.Stop();
            _logger?.LogInformation("{MethodName} 完成,耗时 {ElapsedMs}ms",
                targetMethod?.Name, sw.ElapsedMilliseconds);
            return result;
        }
        catch (TargetInvocationException ex)
        {
            sw.Stop();
            _logger?.LogError(ex.InnerException,
                "{MethodName} 失败,耗时 {ElapsedMs}ms",
                targetMethod?.Name, sw.ElapsedMilliseconds);
            throw ex.InnerException!;
        }
    }

    private async Task AwaitTask(Task task, MethodInfo method, Stopwatch sw)
    {
        try
        {
            await task;
            sw.Stop();
            _logger?.LogInformation("{MethodName} 完成,耗时 {ElapsedMs}ms",
                method.Name, sw.ElapsedMilliseconds);
        }
        catch (Exception ex)
        {
            sw.Stop();
            _logger?.LogError(ex, "{MethodName} 失败,耗时 {ElapsedMs}ms",
                method.Name, sw.ElapsedMilliseconds);
            throw;
        }
    }
}

// 工厂方法
public static class ProxyFactory
{
    public static T CreateLoggingProxy<T>(T target, ILogger logger)
        where T : class
    {
        var proxy = Create<T, LoggingDispatchProxy<T>>()
            as LoggingDispatchProxy<T>;
        proxy!.Initialize(target, logger);
        return (proxy as T)!;
    }
}

// 使用
var service = new SparePartService(repository, logger);
var proxiedService = ProxyFactory.CreateLoggingProxy<ISparePartService>(service, logger);
await proxiedService.StockInAsync(dto);

5.2 异步方法的处理

这是 DispatchProxy 最大的坑------MethodInfo.Invoke 返回的是 Task,但默认的 Invoke 方法不是 async 的。你需要手动 await Task 并在 ContinueWith 中处理结果和异常。

对于带返回值的 Task<TResult>,还需要额外处理:

csharp 复制代码
protected override object? Invoke(MethodInfo? targetMethod, object?[]? args)
{
    var result = targetMethod?.Invoke(_target, args);

    if (result is Task task)
    {
        if (targetMethod!.ReturnType.IsGenericType &&
            targetMethod.ReturnType.GetGenericTypeDefinition() == typeof(Task<>))
        {
            // Task<TResult>:需要返回具体类型
            var resultType = targetMethod.ReturnType.GetGenericArguments()[0];
            var tcs = Activator.CreateInstance(
                typeof(TaskCompletionSource<>).MakeGenericType(resultType))!;

            task.ContinueWith(t =>
            {
                if (t.IsFaulted)
                {
                    ((dynamic)tcs).SetException(t.Exception!.InnerExceptions);
                }
                else if (t.IsCanceled)
                {
                    ((dynamic)tcs).SetCanceled();
                }
                else
                {
                    var resultProperty = t.GetType().GetProperty("Result");
                    ((dynamic)tcs).SetResult(resultProperty?.GetValue(t));
                }
            });

            return ((dynamic)tcs).Task;
        }

        return AwaitVoidTask(task);
    }

    return result;
}

5.3 DispatchProxy 的局限性

  • 只能代理接口方法,不能代理类的虚方法
  • 不支持属性注入(代理类由框架生成,无法自定义构造函数)
  • 异步处理繁琐,需要手动处理 Task / Task<T>
  • 无法拦截非虚方法
  • 性能比 Castle DynamicProxy 略差

6. 方案三:Castle DynamicProxy(最成熟)

Castle DynamicProxy 是 .NET 生态中最成熟、使用最广泛的动态代理库,Autofac、Moq、NSubstitute 等框架底层都用它。

6.1 安装

bash 复制代码
dotnet add package Castle.Core

6.2 基本拦截器

csharp 复制代码
using Castle.DynamicProxy;

public class LoggingInterceptor : IInterceptor
{
    private readonly ILogger<LoggingInterceptor> _logger;

    public LoggingInterceptor(ILogger<LoggingInterceptor> logger)
    {
        _logger = logger;
    }

    public void Intercept(IInvocation invocation)
    {
        var methodName = invocation.Method.Name;
        var sw = Stopwatch.StartNew();

        _logger.LogInformation(
            "开始执行 {ClassName}.{MethodName}",
            invocation.TargetType?.Name, methodName);

        try
        {
            // 前置逻辑
            foreach (var param in invocation.Method.GetParameters())
            {
                _logger.LogDebug("  参数 {ParamName}={ParamValue}",
                    param.Name, invocation.Arguments[param.Position]);
            }

            // 调用目标方法
            invocation.Proceed();

            // 处理异步
            if (invocation.ReturnValue is Task task)
            {
                invocation.ReturnValue = AwaitTask(task, methodName, sw);
            }
            else
            {
                sw.Stop();
                _logger.LogInformation(
                    "{MethodName} 完成,耗时 {ElapsedMs}ms",
                    methodName, sw.ElapsedMilliseconds);
            }
        }
        catch (Exception ex)
        {
            sw.Stop();
            _logger.LogError(ex,
                "{MethodName} 异常,耗时 {ElapsedMs}ms",
                methodName, sw.ElapsedMilliseconds);
            throw;
        }
    }

    private async Task AwaitTask(Task task, string methodName, Stopwatch sw)
    {
        try
        {
            await task;
            sw.Stop();
            _logger.LogInformation(
                "{MethodName} 完成,耗时 {ElapsedMs}ms",
                methodName, sw.ElapsedMilliseconds);
        }
        catch (Exception ex)
        {
            sw.Stop();
            _logger.LogError(ex,
                "{MethodName} 异常,耗时 {ElapsedMs}ms",
                methodName, sw.ElapsedMilliseconds);
            throw;
        }
    }
}

6.3 创建代理

csharp 复制代码
var generator = new ProxyGenerator();  // 注意:应单例复用

var proxy = generator.CreateInterfaceProxyWithTarget<ISparePartService>(
    target: new SparePartService(repository, cache, logger),
    interceptors: new LoggingInterceptor(logger));

// 调用时自动经过拦截器
await proxy.StockInAsync(dto);

6.4 类代理(虚方法拦截)

Castle DynamicProxy 还支持代理类(不仅限于接口),但要求方法是 virtual

csharp 复制代码
public class SparePartService
{
    // 必须 virtual,DynamicProxy 才能重写
    public virtual async Task<Result> StockInAsync(SparePartDto dto)
    {
        // 业务逻辑
    }
}

var proxy = generator.CreateClassProxy<SparePartService>(
    new LoggingInterceptor(logger));

6.5 按 Attribute 匹配拦截

csharp 复制代码
// 标记特性
[AttributeUsage(AttributeTargets.Method)]
public class LogPerformanceAttribute : Attribute { }

// 拦截器中判断
public class SelectiveLoggingInterceptor : IInterceptor
{
    public void Intercept(IInvocation invocation)
    {
        var hasAttr = invocation.Method
            .GetCustomAttributes(typeof(LogPerformanceAttribute), true)
            .Any();

        if (!hasAttr)
        {
            invocation.Proceed();  // 不拦截,直接执行
            return;
        }

        // 拦截逻辑
        var sw = Stopwatch.StartNew();
        invocation.Proceed();
        // ...
    }
}

// 使用
public class SparePartService : ISparePartService
{
    [LogPerformance]
    public async Task<Result> StockInAsync(SparePartDto dto) { ... }

    // 不加特性的方法不拦截
    public async Task<Result> GetByIdAsync(long id) { ... }
}

7. 实战切面:事务管理

7.1 事务特性

csharp 复制代码
[AttributeUsage(AttributeTargets.Method, Inherited = true)]
public class TransactionalAttribute : Attribute
{
    public IsolationLevel IsolationLevel { get; set; } = IsolationLevel.ReadCommitted;
    public bool RequireNew { get; set; } = false;
}

7.2 事务拦截器

csharp 复制代码
public class TransactionInterceptor : IInterceptor
{
    private readonly PmsDbContext _dbContext;
    private readonly ILogger<TransactionInterceptor> _logger;

    public TransactionInterceptor(
        PmsDbContext dbContext,
        ILogger<TransactionInterceptor> logger)
    {
        _dbContext = dbContext;
        _logger = logger;
    }

    public void Intercept(IInvocation invocation)
    {
        var attr = invocation.Method
            .GetCustomAttributes(typeof(TransactionalAttribute), true)
            .FirstOrDefault() as TransactionalAttribute;

        if (attr == null)
        {
            invocation.Proceed();
            return;
        }

        // 已经在事务中
        if (_dbContext.Database.CurrentTransaction != null && !attr.RequireNew)
        {
            invocation.Proceed();
            return;
        }

        ExecuteWithTransaction(invocation, attr);
    }

    private void ExecuteWithTransaction(IInvocation invocation, TransactionalAttribute attr)
    {
        using var transaction = _dbContext.Database.BeginTransaction(
            attr.IsolationLevel);

        try
        {
            invocation.Proceed();

            // 处理异步
            if (invocation.ReturnValue is Task task)
            {
                invocation.ReturnValue = AwaitAndCommit(task, transaction);
            }
            else
            {
                transaction.Commit();
            }
        }
        catch
        {
            transaction.Rollback();
            throw;
        }
    }

    private async Task AwaitAndCommit(Task task, IDbContextTransaction transaction)
    {
        try
        {
            await task;
            transaction.Commit();
            _logger.LogDebug("事务提交成功");
        }
        catch (Exception ex)
        {
            transaction.Rollback();
            _logger.LogWarning(ex, "事务回滚");
            throw;
        }
    }
}

7.3 使用

csharp 复制代码
public class SparePartService : ISparePartService
{
    [Transactional(IsolationLevel = IsolationLevel.Serializable)]
    public virtual async Task<Result> StockInAsync(SparePartDto dto)
    {
        var part = await _repository.GetByPartNoAsync(dto.PartNo)
                    ?? new SparePart { PartNo = dto.PartNo };
        part.StockQuantity += dto.Quantity;
        await _repository.UnitOfWork.SaveChangesAsync();
        return Result.Success();
    }

    [Transactional(RequireNew = true)]
    public virtual async Task<Result> AdjustStockWithAuditAsync(...)
    {
        // 独立事务
    }
}

⚠️ 注意事项

  • 事务方法必须是 virtual(类代理)或通过接口调用(接口代理)
  • 同类内部调用 this.StockInAsync() 不会经过代理------这是 AOP 的经典限制
  • 事务内不要调用外部 API(长事务会持有锁)

8. 实战切面:缓存拦截

8.1 缓存特性

csharp 复制代码
[AttributeUsage(AttributeTargets.Method)]
public class CacheAttribute : Attribute
{
    public string KeyTemplate { get; set; } = string.Empty;
    public int ExpirationSeconds { get; set; } = 600;
}

[AttributeUsage(AttributeTargets.Method)]
public class CacheInvalidationAttribute : Attribute
{
    public string[] KeyPatterns { get; set; } = Array.Empty<string>();
}

8.2 缓存拦截器

csharp 复制代码
public class CachingInterceptor : IInterceptor
{
    private readonly ICacheService _cache;
    private readonly ILogger<CachingInterceptor> _logger;

    public CachingInterceptor(ICacheService cache, ILogger<CachingInterceptor> logger)
    {
        _cache = cache;
        _logger = logger;
    }

    public void Intercept(IInvocation invocation)
    {
        // 缓存读取
        var cacheAttr = invocation.Method
            .GetCustomAttribute<CacheAttribute>();
        if (cacheAttr != null)
        {
            HandleCacheGet(invocation, cacheAttr);
            return;
        }

        // 缓存失效
        var invalidationAttr = invocation.Method
            .GetCustomAttribute<CacheInvalidationAttribute>();
        if (invalidationAttr != null)
        {
            HandleCacheInvalidation(invocation, invalidationAttr);
            return;
        }

        invocation.Proceed();
    }

    private void HandleCacheGet(IInvocation invocation, CacheAttribute attr)
    {
        var cacheKey = BuildCacheKey(attr.KeyTemplate, invocation);
        var returnType = invocation.Method.ReturnType;

        // 处理 Task<T> 返回类型
        if (returnType.IsGenericType &&
            returnType.GetGenericTypeDefinition() == typeof(Task<>))
        {
            var resultType = returnType.GetGenericArguments()[0];
            invocation.ReturnValue = GetFromCacheOrExecuteAsync(
                invocation, cacheKey, resultType, attr.ExpirationSeconds);
        }
        else
        {
            // 同步方法
            var cached = _cache.Get(cacheKey, returnType);
            if (cached != null)
            {
                invocation.ReturnValue = cached;
                _logger.LogDebug("缓存命中: {CacheKey}", cacheKey);
                return;
            }

            invocation.Proceed();
            if (invocation.ReturnValue != null)
            {
                _cache.Set(cacheKey, invocation.ReturnValue,
                    TimeSpan.FromSeconds(attr.ExpirationSeconds));
            }
        }
    }

    private async Task<T?> GetFromCacheOrExecuteAsync<T>(
        IInvocation invocation, string cacheKey, int expirationSeconds)
    {
        // 先查缓存
        var cached = await _cache.GetAsync<T>(cacheKey);
        if (cached != null)
        {
            _logger.LogDebug("缓存命中: {CacheKey}", cacheKey);
            return cached;
        }

        // 执行目标方法
        invocation.Proceed();
        var task = (Task<T>)invocation.ReturnValue!;
        var result = await task;

        // 写入缓存
        if (result != null)
        {
            await _cache.SetAsync(cacheKey, result,
                TimeSpan.FromSeconds(expirationSeconds));
        }

        return result;
    }

    private void HandleCacheInvalidation(
        IInvocation invocation, CacheInvalidationAttribute attr)
    {
        invocation.Proceed();

        if (invocation.ReturnValue is Task task)
        {
            invocation.ReturnValue = InvalidateAfterAsync(task, attr, invocation);
        }
        else
        {
            InvalidateKeys(attr, invocation);
        }
    }

    private async Task InvalidateAfterAsync(
        Task task, CacheInvalidationAttribute attr, IInvocation invocation)
    {
        await task;
        InvalidateKeys(attr, invocation);
    }

    private void InvalidateKeys(
        CacheInvalidationAttribute attr, IInvocation invocation)
    {
        foreach (var pattern in attr.KeyPatterns)
        {
            var key = BuildCacheKey(pattern, invocation);
            _cache.Remove(key);
            _logger.LogDebug("缓存失效: {CacheKey}", key);
        }
    }

    private string BuildCacheKey(string template, IInvocation invocation)
    {
        // 支持 {0} {1} 参数占位符,以及 {propertyName} 属性占位符
        var key = template;
        var parameters = invocation.Method.GetParameters();

        for (int i = 0; i < parameters.Length; i++)
        {
            key = key.Replace($"{{{i}}}", invocation.Arguments[i]?.ToString() ?? "");
            key = key.Replace($"{{{parameters[i].Name}}}",
                invocation.Arguments[i]?.ToString() ?? "");
        }

        return key;
    }
}

8.3 使用

csharp 复制代码
public interface ISparePartService
{
    [Cache("sparepart:partno:{0}", ExpirationSeconds = 600)]
    Task<SparePart?> GetByPartNoAsync(string partNo);

    [Cache("sparepart:id:{0}", ExpirationSeconds = 300)]
    Task<SparePart?> GetByIdAsync(long id);

    [CacheInvalidation("sparepart:partno:{dto.PartNo}", "sparepart:list:*")]
    Task<Result> StockInAsync(SparePartDto dto);
}

9. 实战切面:日志与性能监控

9.1 综合日志拦截器

csharp 复制代码
public class MonitoringInterceptor : IInterceptor
{
    private readonly ILogger<MonitoringInterceptor> _logger;
    private readonly IMetricsService _metrics;

    public MonitoringInterceptor(
        ILogger<MonitoringInterceptor> logger,
        IMetricsService metrics)
    {
        _logger = logger;
        _metrics = metrics;
    }

    public void Intercept(IInvocation invocation)
    {
        var methodName = $"{invocation.TargetType?.Name}.{invocation.Method.Name}";
        var sw = Stopwatch.StartNew();

        // 记录请求参数(生产环境注意脱敏)
        if (_logger.IsEnabled(LogLevel.Debug))
        {
            var args = FormatArguments(invocation);
            _logger.LogDebug("→ {Method}({Args})", methodName, args);
        }

        try
        {
            invocation.Proceed();

            if (invocation.ReturnValue is Task task)
            {
                invocation.ReturnValue = HandleAsyncResult(
                    task, methodName, sw);
            }
            else
            {
                sw.Stop();
                RecordSuccess(methodName, sw.Elapsed.TotalMilliseconds);
            }
        }
        catch (Exception ex)
        {
            sw.Stop();
            RecordFailure(methodName, sw.Elapsed.TotalMilliseconds, ex);
            throw;
        }
    }

    private async Task HandleAsyncResult(
        Task task, string methodName, Stopwatch sw)
    {
        try
        {
            await task;
            sw.Stop();
            RecordSuccess(methodName, sw.Elapsed.TotalMilliseconds);
        }
        catch (Exception ex)
        {
            sw.Stop();
            RecordFailure(methodName, sw.Elapsed.TotalMilliseconds, ex);
            throw;
        }
    }

    private void RecordSuccess(string methodName, double elapsedMs)
    {
        _logger.LogInformation(
            "✓ {Method} 完成 [{ElapsedMs:F2}ms]", methodName, elapsedMs);

        _metrics.ObserveHistogram(
            "method_duration_ms", elapsedMs,
            ("method", methodName), ("result", "success"));

        if (elapsedMs > 1000)
        {
            _logger.LogWarning(
                "慢方法: {Method} 耗时 {ElapsedMs:F2}ms", methodName, elapsedMs);
        }
    }

    private void RecordFailure(string methodName, double elapsedMs, Exception ex)
    {
        _logger.LogError(ex,
            "✗ {Method} 失败 [{ElapsedMs:F2}ms]: {Error}",
            methodName, elapsedMs, ex.Message);

        _metrics.IncrementCounter(
            "method_errors_total",
            ("method", methodName),
            ("exception", ex.GetType().Name));
    }

    private string FormatArguments(IInvocation invocation)
    {
        var parameters = invocation.Method.GetParameters();
        var parts = new List<string>();

        for (int i = 0; i < parameters.Length; i++)
        {
            var value = invocation.Arguments[i];
            var formatted = value switch
            {
                null => "null",
                string s => $"\"{s}\"",
                CancellationToken => "[CT]",
                _ => value.ToString()
            };
            parts.Add($"{parameters[i].Name}={formatted}");
        }

        return string.Join(", ", parts);
    }
}

10. 实战切面:自动重试

10.1 重试特性

csharp 复制代码
[AttributeUsage(AttributeTargets.Method)]
public class RetryAttribute : Attribute
{
    public int MaxRetries { get; set; } = 3;
    public int DelayMilliseconds { get; set; } = 1000;
    public Type[]? RetryOnExceptions { get; set; }
}

10.2 重试拦截器

csharp 复制代码
public class RetryInterceptor : IInterceptor
{
    private readonly ILogger<RetryInterceptor> _logger;

    public RetryInterceptor(ILogger<RetryInterceptor> logger)
    {
        _logger = logger;
    }

    public void Intercept(IInvocation invocation)
    {
        var attr = invocation.Method
            .GetCustomAttribute<RetryAttribute>();

        if (attr == null)
        {
            invocation.Proceed();
            return;
        }

        if (invocation.Method.ReturnType.IsGenericType &&
            invocation.Method.ReturnType.GetGenericTypeDefinition() == typeof(Task<>))
        {
            var resultType = invocation.Method.ReturnType.GetGenericArguments()[0];
            invocation.ReturnValue = RetryAsync(invocation, attr, resultType);
        }
        else if (invocation.Method.ReturnType == typeof(Task))
        {
            invocation.ReturnValue = RetryVoidAsync(invocation, attr);
        }
        else
        {
            RetrySync(invocation, attr);
        }
    }

    private void RetrySync(IInvocation invocation, RetryAttribute attr)
    {
        for (int attempt = 0; ; attempt++)
        {
            try
            {
                invocation.Proceed();
                return;
            }
            catch (Exception ex) when (ShouldRetry(ex, attr, attempt))
            {
                LogRetry(invocation, attempt + 1, attr.MaxRetries, ex);
                Thread.Sleep(attr.DelayMilliseconds * (attempt + 1)); // 线性退避
            }
        }
    }

    private async Task RetryVoidAsync(IInvocation invocation, RetryAttribute attr)
    {
        for (int attempt = 0; ; attempt++)
        {
            try
            {
                invocation.Proceed();
                await (Task)invocation.ReturnValue!;
                return;
            }
            catch (Exception ex) when (ShouldRetry(ex, attr, attempt))
            {
                LogRetry(invocation, attempt + 1, attr.MaxRetries, ex);
                await Task.Delay(attr.DelayMilliseconds * (attempt + 1));
            }
        }
    }

    private async Task<T> RetryAsync<T>(IInvocation invocation, RetryAttribute attr, Type resultType)
    {
        for (int attempt = 0; ; attempt++)
        {
            try
            {
                invocation.Proceed();
                return await (Task<T>)invocation.ReturnValue!;
            }
            catch (Exception ex) when (ShouldRetry(ex, attr, attempt))
            {
                LogRetry(invocation, attempt + 1, attr.MaxRetries, ex);
                await Task.Delay(attr.DelayMilliseconds * (int)Math.Pow(2, attempt));
                // 指数退避: 1s, 2s, 4s, 8s...
            }
        }
    }

    private bool ShouldRetry(Exception ex, RetryAttribute attr, int attempt)
    {
        if (attempt >= attr.MaxRetries) return false;

        if (attr.RetryOnExceptions == null || attr.RetryOnExceptions.Length == 0)
            return true;  // 默认重试所有异常

        return attr.RetryOnExceptions.Any(t => t.IsInstanceOfType(ex));
    }

    private void LogRetry(IInvocation invocation, int attempt, int maxAttempts, Exception ex)
    {
        _logger.LogWarning(
            "方法 {Method} 第 {Attempt}/{MaxAttempts} 次重试,原因: {Error}",
            invocation.Method.Name, attempt, maxAttempts, ex.Message);
    }
}

10.3 使用------船岸同步的卫星闪断重试

csharp 复制代码
public interface IShipSyncService
{
    [Retry(MaxRetries = 3, DelayMilliseconds = 2000,
        RetryOnExceptions = new[] {
            typeof(SatelliteDisconnectException),
            typeof(TimeoutException),
            typeof(HttpRequestException)
        })]
    Task<SyncResult> TransmitPackageAsync(SyncPackage package, CancellationToken ct);
}

💡 这个切面直接对应了之前线上排障博客中"卫星闪断导致数据丢失"的场景------把重试逻辑从业务代码中抽离,用声明式特性控制。


11. AOP 与 DI 容器集成

11.1 Autofac + Castle DynamicProxy

Autofac 原生支持 Castle DynamicProxy 拦截器:

csharp 复制代码
builder.Host.UseServiceProviderFactory(new AutofacServiceProviderFactory());
builder.Host.ConfigureContainer<ContainerBuilder>(cb =>
{
    // 注册拦截器
    cb.RegisterType<LoggingInterceptor>().AsSelf().InstancePerLifetimeScope();
    cb.RegisterType<TransactionInterceptor>().AsSelf().InstancePerLifetimeScope();
    cb.RegisterType<CachingInterceptor>().AsSelf().InstancePerLifetimeScope();

    // 注册服务并启用拦截
    cb.RegisterType<SparePartService>()
        .As<ISparePartService>()
        .EnableInterfaceInterceptors()
        .InterceptedBy(
            typeof(LoggingInterceptor),
            typeof(TransactionInterceptor),
            typeof(CachingInterceptor))
        .InstancePerLifetimeScope();
});

11.2 原生 DI + 手动代理工厂

如果不想用 Autofac,可以用工厂注册:

csharp 复制代码
// 注册代理生成器(单例)
builder.Services.AddSingleton(new ProxyGenerator());

// 注册拦截器
builder.Services.AddScoped<LoggingInterceptor>();
builder.Services.AddScoped<TransactionInterceptor>();
builder.Services.AddScoped<CachingInterceptor>();

// 注册服务------使用工厂创建代理
builder.Services.AddScoped<ISparePartService>(sp =>
{
    var generator = sp.GetRequiredService<ProxyGenerator>();
    var target = new SparePartService(
        sp.GetRequiredService<IRepository<SparePart>>(),
        sp.GetRequiredService<ICacheService>(),
        sp.GetRequiredService<ILogger<SparePartService>>());

    return generator.CreateInterfaceProxyWithTarget<ISparePartService>(
        target,
        sp.GetRequiredService<LoggingInterceptor>(),
        sp.GetRequiredService<TransactionInterceptor>(),
        sp.GetRequiredService<CachingInterceptor>());
});

11.3 拦截器执行顺序

多个拦截器按注册顺序形成洋葱管道

复制代码
请求 → LoggingInterceptor.Before
         → TransactionInterceptor.Before
              → CachingInterceptor.Before
                   → 目标方法执行
              → CachingInterceptor.After
         → TransactionInterceptor.After(提交事务)
     → LoggingInterceptor.After(记录耗时)
     → 响应
  • 注册在最前面的拦截器在最外层
  • 最内层拦截器最先接触到请求,但最后处理响应
  • 事务拦截器通常放在日志拦截器内侧------这样事务只包裹业务逻辑,不包含日志写入

12. Source Generator 编译时 AOP

12.1 动态代理的痛点

Castle DynamicProxy 很强大,但有几个固有问题:

  • 启动时生成代理类有性能开销
  • 只能拦截 virtual 方法或接口方法
  • 堆栈跟踪中有代理类名,调试不直观
  • AOT 编译不友好(.NET NativeAOT 不支持 Reflection.Emit)

12.2 Source Generator 方案

C# Source Generator 在编译时分析代码并生成额外的 C# 源文件,可以用来实现编译时 AOP:

csharp 复制代码
// 用户写的------标记了 [GenerateProxy]
[GenerateProxy]
public partial class SparePartService : ISparePartService
{
    [LogPerformance]
    public virtual async Task<Result> StockInAsync(SparePartDto dto)
    {
        // 业务逻辑
    }
}

// Source Generator 在编译时自动生成:
public partial class SparePartService
{
    // 生成的代理类/包装代码
    // 在编译时直接注入日志逻辑,无需运行时反射
}

12.3 现有库

方式 特点
Metalama Source Generator + Roslyn 最成熟,支持完整 AOP,免费版有限制
MrAdvice 编译时 IL Weaving 开源,性能好
DispatchProxy 运行时 原生但功能有限
Castle DynamicProxy 运行时 生态最好,第三方依赖

12.4 手写 Source Generator(简化示例)

csharp 复制代码
[Generator]
public class LogGenerator : ISourceGenerator
{
    public void Initialize(GeneratorInitializationContext context)
    {
        context.RegisterForSyntaxNotifications(() => new SyntaxReceiver());
    }

    public void Execute(GeneratorExecutionContext context)
    {
        // 分析语法树,找到标记了 [LogPerformance] 的方法
        // 生成包含日志逻辑的 partial 类
        // ...
    }
}

Source Generator 是未来的方向,但目前 Castle DynamicProxy 仍然是最实用的选择。


13. AOP 的边界:什么时候不该用

AOP 很强大,但不是银弹。滥用 AOP 会让代码变成"魔法",出了问题难以排查。

13.1 适合 AOP 的场景

场景 原因 推荐度
日志/性能监控 完全与业务无关,模式统一 ⭐⭐⭐⭐⭐
事务管理 声明式事务是行业标准做法 ⭐⭐⭐⭐⭐
缓存 模式固定,但要注意缓存失效策略 ⭐⭐⭐⭐
权限校验 声明式权限清晰,但复杂权限建议用中间件/过滤器 ⭐⭐⭐
重试 适合幂等操作的网络重试 ⭐⭐⭐
异常处理 统一异常包装可以,但不要吞异常 ⭐⭐⭐
参数验证 简单验证可以,复杂验证建议用 FluentValidation ⭐⭐
领域事件发布 可以,但 SaveChanges 拦截器更合适 ⭐⭐

13.2 不适合 AOP 的场景

csharp 复制代码
// ❌ 业务逻辑不要放进切面
public class OrderDiscountInterceptor : IInterceptor
{
    public void Intercept(IInvocation invocation)
    {
        // 折扣计算是核心业务逻辑,不是横切关注点
        // 放在切面里会让业务逻辑分散、不可追踪
        if (invocation.Method.Name == "CalculatePrice")
        {
            // 这里改了价格,但业务代码里完全看不到
        }
        invocation.Proceed();
    }
}

// ❌ 复杂的条件分支不要放进切面
// 如果切面里出现大量 if (methodName == "xxx"),说明设计有问题

13.3 AOP 的"隐性成本"

  1. 调试困难:方法被代理包裹,断点可能不命中,堆栈中有代理类
  2. 自调用失效 :类内部 this.Method() 不经过代理
  3. 性能开销:每个方法调用多一层反射/委托调用(通常可忽略,但高频路径注意)
  4. 理解门槛 :新人看到 [Transactional] 特性,需要先学 AOP 才能理解发生了什么
  5. 错误隐藏:切面吞掉异常或修改返回值,问题排查困难

13.4 自调用问题的解决方案

csharp 复制代码
public class SparePartService : ISparePartService
{
    private readonly ISparePartService _self;  // 注入自身的代理

    public SparePartService(ISparePartService self)
    {
        _self = self;  // Autofac 支持,Castle 需要特殊配置
    }

    public async Task MethodA()
    {
        // ❌ 直接调用,不经过代理([Transactional] 不生效)
        MethodB();

        // ✅ 通过代理调用,切面生效
        await _self.MethodB();
    }

    [Transactional]
    public virtual Task MethodB()
    {
        // 事务逻辑
        return Task.CompletedTask;
    }
}

但注入自身代理是一种代码异味,更好的做法是把 MethodB 拆到另一个类中。


14. Checklist

设计

  • 切面只处理真正的横切关注点(日志、事务、缓存、权限)
  • 切面中不写业务逻辑
  • 每个切面职责单一,一个拦截器只做一件事
  • 切面的执行顺序明确(外→内:日志 → 事务 → 缓存 → 业务)
  • 标记特性命名清晰([Transactional] 而非 [Tx]

实现

  • 优先使用接口代理(让方法非 virtual 也没关系)
  • 正确处理异步方法(Task / Task<T>)
  • 拦截器注册为 Scoped(与目标对象生命周期一致)
  • ProxyGenerator 注册为 Singleton(内部有缓存,不要每次创建)
  • 拦截器中的异常不要吞掉,要么处理要么重新抛出
  • 日志中记录方法名和耗时,但不记录敏感参数

安全

  • 缓存切面不缓存包含敏感数据的方法返回值
  • 日志切面对密码、Token 等参数脱敏
  • 事务切面的异常一定触发回滚
  • 重试切面只重试幂等操作
  • 权限切面在方法执行前拦截,不依赖方法内部判断

调试与测试

  • 单元测试直接测试目标类,不走代理
  • 集成测试验证切面是否正确织入
  • 关键方法确认 virtual 修饰符或接口暴露
  • 注意同类内部方法调用不走代理的限制
  • AOP 相关的 bug 优先检查代理是否正确创建、拦截器顺序是否正确

PMS 项目专项

  • 船岸同步的卫星闪断重试用 [Retry] 切面
  • 备件入库/出库的库存变更用 [Transactional] 切面
  • 备件查询用 [Cache] 切面,写操作用 [CacheInvalidation]
  • 所有 Service 方法用 [LogPerformance] 切面记录耗时和异常
  • 权限校验优先用 ASP.NET Core 授权中间件/策略,AOP 作为补充

总结

AOP 的核心价值是分离关注点 ------让业务代码只做业务,让基础设施代码统一管理。但它的代价是增加了间接层,用好了代码干净可维护,用不好就是"黑魔法"。

在 .NET 中做 AOP 的选择路径:

复制代码
需要 AOP?
├── .NET 8+,只需要接口代理 → DispatchProxy(零依赖)
├── 需要类代理/成熟生态 → Castle DynamicProxy(推荐)
├── 用了 Autofac → 原生集成 DynamicProxy,开箱即用
├── 追求极致性能/AOT → Source Generator(Metalama)
└── 只是想加日志/事务 → 考虑 ASP.NET Core 中间件/过滤器/EF Core 拦截器

最重要的一句话:AOP 是工具不是架构。如果你的业务逻辑清晰、接口设计合理,AOP 是锦上添花;如果业务逻辑本身一团糟,AOP 只会让它更难理解。

💬 最后互动 :你在项目中用过 AOP 吗?用的是哪种方案?遇到过什么坑?我先来------曾经用 Castle DynamicProxy 做缓存切面,缓存 Key 模板用了 {0} 占位符,但方法参数是一个 DTO 对象,ToString() 返回的是类名而非内容,结果所有请求都命中了同一个缓存 Key,导致不同备件查到同一条数据。这个 bug 在线上潜伏了两周才被发现。评论区聊聊你的故事。

相关推荐
Lost of 程序猿2 小时前
ASP.NET Core 认证授权实战:从 JWT 到 Policy,设计一套企业级 RBAC 权限系统
后端·asp.net
Lost of 程序猿2 小时前
ASP.NET Core 后台任务全景:从 BackgroundService 到 Channel 队列,再到分布式调度
后端·asp.net·.netcore
倾颜2 小时前
NestJS 核心概念梳理:从 Module、Controller 到 Guard、Interceptor
后端·node.js·nestjs
东风破_2 小时前
JWT 1:从一个登录请求开始,理解 React 项目里的 API 层与 Mock
前端·后端
东风破_3 小时前
JWT 3:为什么 Token 要放进 Authorization?Axios 拦截器到底解决了什么?
前端·后端
东风破_3 小时前
JWT 5:路由守卫是什么?把整个 JWT 登录鉴权流程串起来
前端·后端
东风破_3 小时前
JWT 2:HTTP 是无状态的,为什么登录成功后还要给 Token?
前端·后端
东风破_3 小时前
JWT 4:Zustand 到底解决了什么?为什么登录状态要放进 Store?
前端·后端
IT_陈寒3 小时前
Vite动态导入差点让我秃头,原来问题出在这
前端·人工智能·后端