日志、事务、缓存、权限、重试、性能监控------这些代码散落在每个业务方法中,和核心业务逻辑纠缠在一起,改一个日志格式要动 100 个文件。AOP(Aspect-Oriented Programming,面向切面编程)就是解决这个问题的武器。本文从代理模式讲起,覆盖 .NET 中三种主流 AOP 方案(RealProxy、DispatchProxy、Castle DynamicProxy),结合 PMS 项目实现事务、缓存、日志、重试四个切面,最后讲清楚什么时候该用 AOP、什么时候不该用。
目录
- [什么是 AOP:从"重复代码"说起](#什么是 AOP:从"重复代码"说起)
- [AOP 核心概念全解析](#AOP 核心概念全解析)
- [代理模式:AOP 的底层基石](#代理模式:AOP 的底层基石)
- 方案一:RealProxy(经典透明代理)
- [方案二:DispatchProxy(.NET Core 原生)](#方案二:DispatchProxy(.NET Core 原生))
- [方案三:Castle DynamicProxy(最成熟)](#方案三:Castle DynamicProxy(最成熟))
- 实战切面:事务管理
- 实战切面:缓存拦截
- 实战切面:日志与性能监控
- 实战切面:自动重试
- [AOP 与 DI 容器集成](#AOP 与 DI 容器集成)
- [Source Generator 编译时 AOP](#Source Generator 编译时 AOP)
- [AOP 的边界:什么时候不该用](#AOP 的边界:什么时候不该用)
- 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 框架主要实现 Before、Around、After 三种。
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 的"隐性成本"
- 调试困难:方法被代理包裹,断点可能不命中,堆栈中有代理类
- 自调用失效 :类内部
this.Method()不经过代理 - 性能开销:每个方法调用多一层反射/委托调用(通常可忽略,但高频路径注意)
- 理解门槛 :新人看到
[Transactional]特性,需要先学 AOP 才能理解发生了什么 - 错误隐藏:切面吞掉异常或修改返回值,问题排查困难
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 在线上潜伏了两周才被发现。评论区聊聊你的故事。